/* Self-hosted, Latin-subset webfonts.

   Three weights, and each one lists every Inter file as a source
   rather than just its own. A src list is a fallback chain: the browser
   walks it and uses the first file that actually loads. That matters here
   because when a single weight's file is missing, the browser does not
   fall back to another weight of the same family -- it falls back to the
   next family in the stack, so that one weight renders in a completely
   different typeface while the rest of the page stays in Inter. A headline
   in Segoe UI above service names in Inter is exactly what a missing
   inter-latin-600.woff2 looks like, and nothing about it announces itself
   as a missing file. With the chain below, a missing file costs a little
   weight accuracy and never costs the typeface.

   font-display: swap avoids invisible text while they load. If none of the
   files are present the whole app falls back together, to one system face,
   which is consistent even though it is not Inter. */
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/inter-latin-400.woff2') format('woff2'),
       url('fonts/inter-latin-500.woff2') format('woff2'),
       url('fonts/inter-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('fonts/inter-latin-600.woff2') format('woff2'),
       url('fonts/inter-latin-500.woff2') format('woff2'),
       url('fonts/inter-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('fonts/inter-latin-500.woff2') format('woff2'),
       url('fonts/inter-latin-600.woff2') format('woff2'),
       url('fonts/inter-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/ibm-plex-mono-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light;

  --ink: #12171b;
  --on-ink: #ffffff;      /* text/icon color for anything filled with var(--ink) */
  --paper: #e9ecee;
  --card: #ffffff;
  --rule: #c7d0d5;        /* structural border: carries the layout now that shadows do not */
  --hairline: #dfe4e7;    /* subtler than --rule: grid gaps, row dividers */
  --recess: #f2f5f6;      /* rail/editor backgrounds, a hair off the card */
  --muted: #57626a;
  --on-accent: #ffffff;   /* text on a solid severity-colored background */

  --green: #157f4a;
  --amber: #c07f00;
  --red: #b3241a;
  --blue: #2c6ba0;
  /* Solid fills large enough to carry --on-accent text (the overall bar when
     something is wrong, the overdue pill). In light mode the base color is
     already dark enough to double as its own solid; dark mode below picks a
     separate, darker value because the bright dark-mode accent would not
     pass with white text. */
  --green-solid: var(--green);
  --amber-solid: var(--amber);
  --red-solid: var(--red);
  --blue-solid: var(--blue);

  --green-tint: #e7f4ed; --green-tint-border: #b9dfc9; --green-tint-text: #10603a; --green-wash: #f2faf5;
  --amber-tint: #fdf3df; --amber-tint-border: #ecd39a; --amber-tint-text: #7a5200; --amber-wash: #fdf8ec;
  --red-tint:   #fbeae8; --red-tint-border:   #f0c2bd; --red-tint-text:   #8f1c14; --red-wash:   #fdf4f3;
  --blue-tint:  #eaf2fa; --blue-tint-border:  #cfe0f5; --blue-tint-text:  var(--blue); --blue-wash: #f3f8fd;
  --banner-tint: #fdf6e3; --banner-tint-border: #e6cf8a;

  --lamp-off: #ccd4d8;
  --dot-off: #c2ccd2;
  --green-glow: rgba(21,127,74,.15);
  --amber-glow: rgba(192,127,0,.16);
  --red-glow: rgba(179,36,26,.18);
  --red-glow-soft: rgba(179,36,26,.05);
  --blue-glow: rgba(44,107,160,.16);

  --highlight: #fdf3d4;              /* feed row, briefly, when it is new */
  --tile-flash: rgba(44,107,160,.35); /* wallboard tile, briefly, when its status changes */

  /* Surfaces are flat on purpose. --shadow stays a valid, fully transparent
     shadow rather than `none` so the rules that compose it into a longer
     shadow list (the wallboard flash, mainly) still parse. --shadow-pop is
     the one real elevation in the app, for the only thing that genuinely
     floats: the report dialog. */
  --shadow: 0 0 0 0 rgba(0,0,0,0);
  --shadow-pop: 0 24px 60px -20px rgba(10,16,20,.45);

  /* Two faces, and one rule for which is which, applied everywhere without
     exception:
       --mono  machine output and fixed labels -- timestamps, durations,
               counts, IDs, and the small UPPERCASE labels that name a
               region of the page (group rails, section headings, field
               labels, table headers, pills).
       --sans  anything a person wrote or clicks -- service names, headlines,
               body copy, navigation, buttons, links.
     If a string is a sentence, it is sans. If it is a reading off the
     instrument or the name of a dial, it is mono.

     Three weights: 400 for body copy, 500 for the service names on the
     board, 600 for headings and the brand. 500 exists for one reason. A
     board is twenty-odd names read at a glance, and at 600 every one of
     them shouts equally, which on an all-green board is twenty things
     shouting that nothing is wrong. Medium keeps them clearly primary
     without that. Hierarchy comes from size, weight and color -- never
     from a second typeface. */
  --sans: 'Inter', ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Type scale. Six sizes cover the whole app; nothing outside this list.
     Sized for a screen read at arm's length in a corridor, not a laptop on
     a desk: the smallest step is only ever used for uppercase labels, which
     tracking and caps make legible well below body size. */
  --fs-2xs: 10.5px;
  --fs-xs: 11.5px;
  --fs-sm: 12.5px;
  --fs-base: 14px;
  --fs-md: 15.5px;
  --fs-lg: 18px;
  /* Line-height by role, not by element -- every heading uses the same
     value, every paragraph the same, every small secondary line the same. */
  --lh-heading: 1.2;
  --lh-body: 1.55;
  --lh-muted: 1.45;
  --ls-tight: -0.015em;  /* headings */
  --ls-wide: 0.12em;     /* small uppercase mono labels: the panel's own voice */

  /* Corner radii. Three values, all small: this is a panel, not a card
     deck. Anything rounder than --r-lg is a mistake, except a lamp. */
  --r-sm: 2px;   /* chips, buttons, inputs, pills */
  --r-md: 3px;   /* boards, panels, cards */
  --r-lg: 5px;   /* the report dialog, the one floating surface */
}

/* Dark mode: the system preference drives it by default. An explicit choice
   from the masthead toggle (or the wallboard's &theme= param) always wins,
   whichever direction it goes, over the system preference. */
@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) { color-scheme: dark;
    --ink: #e7ebee; --on-ink: #101418; --paper: #101418; --card: #181d22;
    --rule: #39424a; --hairline: #232a31; --recess: #1d2329; --muted: #97a2ab; --on-accent: #ffffff;
    --green: #34c281; --amber: #e8a838; --red: #ff5a4e; --blue: #5b9bd9;
    --green-solid: #1c7a4f; --amber-solid: #8a5c12; --red-solid: #a5342a; --blue-solid: #26608f;
    --green-tint: #123023; --green-tint-border: #1e4d38; --green-tint-text: #7fe3ae; --green-wash: #182620;
    --amber-tint: #332405; --amber-tint-border: #5c430f; --amber-tint-text: #f3c876; --amber-wash: #2a2210;
    --red-tint: #3a1512; --red-tint-border: #5c231d; --red-tint-text: #ffaba3; --red-wash: #2c1a18;
    --blue-tint: #132635; --blue-tint-border: #204059; --blue-tint-text: #a9cdec; --blue-wash: #18232d;
    --banner-tint: #332a0f; --banner-tint-border: #5c4a1a;
    --lamp-off: #454e57; --dot-off: #3e474f;
    --green-glow: rgba(52,194,129,.28); --amber-glow: rgba(232,168,56,.28);
    --red-glow: rgba(255,90,78,.30); --red-glow-soft: rgba(255,90,78,.10); --blue-glow: rgba(91,155,217,.28);
    --highlight: #3a3319; --tile-flash: rgba(91,155,217,.40);
    --shadow: 0 0 0 0 rgba(0,0,0,0);
    --shadow-pop: 0 24px 60px -20px rgba(0,0,0,.7);
  }
}
.theme-dark { color-scheme: dark;
  --ink: #e7ebee; --on-ink: #101418; --paper: #101418; --card: #181d22;
  --rule: #39424a; --hairline: #232a31; --recess: #1d2329; --muted: #97a2ab; --on-accent: #ffffff;
  --green: #34c281; --amber: #e8a838; --red: #ff5a4e; --blue: #5b9bd9;
  --green-solid: #1c7a4f; --amber-solid: #8a5c12; --red-solid: #a5342a; --blue-solid: #26608f;
  --green-tint: #123023; --green-tint-border: #1e4d38; --green-tint-text: #7fe3ae; --green-wash: #182620;
  --amber-tint: #332405; --amber-tint-border: #5c430f; --amber-tint-text: #f3c876; --amber-wash: #2a2210;
  --red-tint: #3a1512; --red-tint-border: #5c231d; --red-tint-text: #ffaba3; --red-wash: #2c1a18;
  --blue-tint: #132635; --blue-tint-border: #204059; --blue-tint-text: #a9cdec; --blue-wash: #18232d;
  --banner-tint: #332a0f; --banner-tint-border: #5c4a1a;
  --lamp-off: #454e57; --dot-off: #3e474f;
  --green-glow: rgba(52,194,129,.28); --amber-glow: rgba(232,168,56,.28);
  --red-glow: rgba(255,90,78,.30); --red-glow-soft: rgba(255,90,78,.10); --blue-glow: rgba(91,155,217,.28);
  --highlight: #3a3319; --tile-flash: rgba(91,155,217,.40);
  --shadow: 0 0 0 0 rgba(0,0,0,0);
  --shadow-pop: 0 24px 60px -20px rgba(0,0,0,.7);
}

* { box-sizing: border-box; }
body { margin: 0; background: var(--paper); color: var(--ink); font: var(--fs-base)/var(--lh-body) var(--sans); }
a { color: inherit; text-underline-offset: 2px; text-decoration-thickness: 1px; }
.wrap { max-width: 1100px; margin: 0 auto; padding: 0 20px 40px; }
.tabular { font-variant-numeric: tabular-nums; }

/* The masthead stays put: on a long board, "which site am I on and am I
   signed in" should not require scrolling back up. Below the report
   dialog (z-index 40/41) and above the history grid's sticky name column
   (z-index 3), so neither can be covered by it. */
.masthead { background: var(--card); border-bottom: 1px solid var(--rule); position: sticky; top: 0; z-index: 20; }
.masthead .wrap { padding: 10px 20px; display: flex; align-items: baseline; gap: 16px; row-gap: 9px; flex-wrap: wrap; }
.brand { font-size: var(--fs-lg); font-weight: 600; letter-spacing: var(--ls-tight); line-height: var(--lh-heading); text-decoration: none; }
.tagline { color: var(--muted); font-size: var(--fs-sm); line-height: var(--lh-muted); }
/* Signed in, the nav carries nine items and no longer fits beside the
   tagline at any width the container allows, so the masthead was breaking
   onto two lines. The tagline is a line of copy for somebody who has never
   seen this board; an owner signing in has. Dropping it there buys back
   more room than the nav needs and costs nothing anyone was reading. */
body.signed-in .tagline { display: none; }
.masthead nav { margin-left: auto; display: flex; align-items: center; gap: 16px; font-size: var(--fs-sm); }
.masthead nav a { color: var(--muted); text-decoration: none; }
.masthead nav .who { color: var(--muted); }
.masthead nav .who b { font-weight: 600; color: var(--ink); }
.masthead nav a:hover { color: var(--ink); text-decoration: underline; }

/* Theme toggle: one button, cycling Auto -> Light -> Dark. PHP already put
   the right class on <html> from the cookie before this ever paints, so
   there is nothing for this control to "catch up" on load. */
.themebtn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--rule);
  background: var(--card); color: var(--muted); border-radius: var(--r-sm); padding: 4px 9px 4px 8px;
  font: var(--fs-2xs) var(--mono); letter-spacing: var(--ls-wide); text-transform: uppercase;
  cursor: pointer; line-height: 1.5; }
.themebtn:hover { color: var(--ink); border-color: var(--muted); }
.themebtn .tb-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); flex: none; }

/* The headline bar. Operating normally is the state this board is in
   almost all of the time, so that state is deliberately quiet: a plain
   card with a green edge, black text, nothing shouting. Every other state
   fills the bar solid in its own color. The page therefore looks calm
   when it is calm and unmistakable when it is not, which is the whole job
   of the element. */
.overall { border-radius: var(--r-md); padding: 12px 16px; margin: 16px 0 10px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--rule); border-left: 4px solid var(--green);
  display: flex; align-items: center; gap: 14px; }
.overall.yellow { background: var(--amber-solid); border-color: var(--amber-solid); color: var(--on-accent); }
.overall.red { background: var(--red-solid); border-color: var(--red-solid); color: var(--on-accent); }
.overall.blue, .overall.info { background: var(--blue-solid); border-color: var(--blue-solid); color: var(--on-accent); }
.overall h1 { margin: 0; font-size: var(--fs-lg); font-weight: 600; letter-spacing: var(--ls-tight); line-height: var(--lh-heading); }
.overall .when { margin-left: auto; font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums;
  line-height: var(--lh-muted); color: var(--muted); text-align: right;
  display: flex; flex-direction: column; gap: 1px; }
.overall.yellow .when, .overall.red .when, .overall.blue .when, .overall.info .when { color: inherit; opacity: .82; }
.when-change { opacity: .78; }
/* A board that has stopped hearing from the server says so. Colored on the
   calm bar; on a solid severity bar there is no color left to use, so it
   goes to full strength and picks up a rule under it instead. */
.when.stale .when-checked { color: var(--red); }
.overall.yellow .when.stale .when-checked, .overall.red .when.stale .when-checked,
.overall.blue .when.stale .when-checked, .overall.info .when.stale .when-checked {
  color: var(--on-accent); opacity: 1; text-decoration: underline; }

.banner { background: var(--banner-tint); border: 1px solid var(--banner-tint-border); border-left: 3px solid var(--amber);
  padding: 8px 12px; border-radius: var(--r-sm); margin: 8px 0; font-size: var(--fs-sm); }
.banner.info { background: var(--blue-tint); border-color: var(--blue-tint-border); border-left-color: var(--blue); }
.banner span { color: var(--muted); }

/* Three-lamp signal: the loud element on an otherwise quiet page. Color never
   carries the state alone -- position (which lamp) and the text label beside
   it do too. */
.signal { display: inline-flex; gap: 4px; align-items: center; flex: none; }
.lamp { width: 11px; height: 11px; border-radius: 50%; background: var(--lamp-off);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.07); }
.lamp.on.green { background: var(--green); box-shadow: 0 0 0 3px var(--green-glow); }
.lamp.on.yellow { background: var(--amber); box-shadow: 0 0 0 3px var(--amber-glow); }
.lamp.on.red { background: var(--red); box-shadow: 0 0 0 3px var(--red-glow); animation: pulse 2s ease-in-out infinite; }
.lamp.on.blue, .lamp.on.info { background: var(--blue); box-shadow: 0 0 0 3px var(--blue-glow); }
@keyframes pulse { 50% { box-shadow: 0 0 0 6px var(--red-glow-soft); } }
@media (prefers-reduced-motion: reduce) { .lamp.on.red { animation: none; } }

/* Inside a solid severity-colored bar the lamp's only job is positional --
   which of the three dots is lit -- since the bar's own background already
   carries the color. A same-hue dot on a same-hue bar (light mode's red bar
   with a light-mode red dot, for one) would be invisible, so there "on" is
   always on-accent instead of severity-colored; the red pulse still runs
   and still flashes its color, just around a dot that never disappears.
   The green bar is not solid, so it is excluded here and keeps the normal
   green lamp. */
.overall.yellow .lamp, .overall.red .lamp, .overall.blue .lamp, .overall.info .lamp {
  background: rgba(255,255,255,.32); box-shadow: none; }
.overall.yellow .lamp.on, .overall.red .lamp.on, .overall.blue .lamp.on, .overall.info .lamp.on {
  background: var(--on-accent); box-shadow: 0 0 0 3px rgba(255,255,255,.3); }

.board { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-md);
  overflow: hidden; }
/* Group name sits in a left rail beside its tiles instead of on its own row. */
.band { display: flex; align-items: stretch; border-top: 1px solid var(--rule); }
.board .band:first-child { border-top: 0; }
.band .glabel { flex: 0 0 126px; display: flex; align-items: center; padding: 8px 11px;
  background: var(--recess); border-right: 1px solid var(--rule);
  font: var(--fs-2xs)/1.35 var(--mono); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--muted); }
/* Recess, not card: when a group's rail label runs taller than its single
   row of tiles, the strip left over below the grid is an empty part of the
   panel and should read the same as any other unfilled slot. */
.band .bandbody { flex: 1; min-width: 0; background: var(--recess); }
.band .grid { min-width: 0; }
.band.hit .glabel { background: var(--amber-tint); color: var(--amber-tint-text); }
.band.unconfirmed-band .glabel { background: var(--amber-tint); color: var(--amber-tint-text); opacity: .8; }
.glabel .count { font: var(--fs-2xs) var(--mono); font-variant-numeric: tabular-nums; opacity: .7; margin-left: 5px; }

/* An owner sees an Update chip; everyone sees a quiet Report link. Both are
   normal flex items in the tile row -- never absolutely positioned, never
   negatively margined -- so nothing can land on top of the lamp or the name.
   The row is: tile (lamp + name) taking all remaining space, then the chips
   at their intrinsic width.

   This overlapped twice before, and both times for the same root cause: the
   tile carried a fixed pixel min-width (130px), which is a floor that
   assumes a viewport. Flex cannot shrink an item below its min-width, so at
   the board's narrowest column -- ~230px, which is what a 1440px screen
   actually produces, see .grid -- the row's minimum content was wider than
   its own grid cell and simply painted over the neighbouring tile. The
   first fix hid that behind flex-wrap: wrap (the chip dropped to its own
   line instead of overlapping); removing the wrap to fix the mobile
   regression turned the wrap straight back into an overlap.

   So: no pixel floor at all. The tile is min-width: 0 and absorbs whatever
   is left after the chips, and overflow: hidden makes escaping the cell
   structurally impossible rather than merely unlikely -- a longer chip
   label in future can now only cost the name width, never paint outside. */
.tilewrap { display: flex; flex-wrap: nowrap; align-items: center; column-gap: 7px;
  background: var(--card); min-width: 0; overflow: hidden;
  box-shadow: 1px 0 0 var(--hairline), 0 1px 0 var(--hairline); }
.tilewrap .tile { flex: 1 1 auto; min-width: 0; }

/* Shown at rest, not revealed on hover, and set as a plain text link
   rather than a chip. It is a flex item either way, so it held its width
   whether or not it was painted -- an invisible ~50px control in every
   tile is exactly the gap that made the board look padded out with
   nothing. As a bare link it costs half that, it stops competing with the
   status light for attention, and it matches the Report link it sits
   beside instead of being a second, louder kind of control in the same
   two centimetres. */
.editchip { flex: none; display: inline-flex; align-items: center; min-height: 42px;
  padding: 4px 2px; font-size: var(--fs-xs); line-height: var(--lh-heading); color: var(--muted);
  text-decoration: none; white-space: nowrap; }
.editchip:hover, .editchip:focus-visible { color: var(--ink); text-decoration: underline; }

/* report_status_line(): plain text, never a status light -- no lamp, no
   severity color, no pill. Secondary text under the service name, inside
   the tile's own link on the board; its own line on the service page.
   "unconfirmed" is the one case worth a hair more contrast (it is the
   leading indicator this exists for); "known" stays as quiet as the tile
   around it. */
.repline { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-size: var(--fs-2xs);
  color: var(--muted); line-height: var(--lh-muted); }
.repline.unconfirmed { color: var(--amber-tint-text); font-weight: 600; }
.repline-note { font-weight: 400; color: var(--muted); font-style: italic; }
.repline-wrap { margin: 6px 0 0; }
.repline-wrap .repline { font-size: var(--fs-xs); }

/* report_control(): a quiet text link, not a button -- secondary to the
   status lights, which are the point of this page. Sized only by padding,
   never by font-size, so it still meets a 44px touch target on phones.
   position: relative scopes the visually-hidden toggle checkbox below; the
   dialog and its backdrop are position: fixed and do not depend on it.
   No margin-left: auto -- the tile beside it has flex-grow: 1 and has
   already absorbed the free space, so the chip is pinned right by flow. */
.reportctl { position: relative; flex: none; }
.reportdlg-toggle { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.reportlink { display: inline-flex; align-items: center; justify-content: center; min-height: 42px;
  padding: 4px 2px; font-size: var(--fs-xs); line-height: var(--lh-heading); color: var(--muted); text-decoration: none;
  cursor: pointer; white-space: nowrap; }
.repline-wrap + .reportctl .reportlink, .panel .reportlink { font-size: var(--fs-sm); }
.reportlink:hover, .reportdlg-toggle:focus-visible + .reportlink { color: var(--ink); text-decoration: underline; }

/* One-shot confirmation after a successful submit -- gone on the next page
   load via the session flash that drives it, never a persistent label:
   these are shared workstations, so the link itself must stay live for
   whoever uses it next. */
.reportlink-confirm { font-size: var(--fs-2xs); color: var(--green-tint-text); margin-right: 4px; white-space: nowrap; }

/* The dialog itself: a checkbox/label pair opens and closes it in pure CSS
   -- no JavaScript is involved in reaching, filling in, or submitting it.
   assets/status.js only adds a fetch-and-swap on top so a submit does not
   reload the whole board when JS is available. This is the one surface in
   the app that really is floating above the page, so it is the one surface
   that gets a real shadow. */
.reportdlg-backdrop { display: none; position: fixed; inset: 0; background: rgba(8,12,15,.55); z-index: 40; }
.reportdlg { display: none; position: fixed; z-index: 41; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(92vw, 420px); max-height: 88vh; overflow-y: auto; background: var(--card); border: 1px solid var(--rule);
  border-radius: var(--r-lg); padding: 18px 20px; box-shadow: var(--shadow-pop); }
.reportdlg-toggle:checked ~ .reportdlg-backdrop, .reportdlg-toggle:checked ~ .reportdlg { display: block; }
.reportdlg-head { margin-bottom: 10px; }
.reportdlg-head b { display: block; font-size: var(--fs-md); line-height: var(--lh-heading); letter-spacing: var(--ls-tight); }
.reportdlg-head .hint { margin: 2px 0 0; }
.reportdlg-disclaimer { font-size: var(--fs-sm); line-height: var(--lh-body); background: var(--amber-tint);
  border: 1px solid var(--amber-tint-border); color: var(--amber-tint-text); border-radius: var(--r-sm);
  padding: 8px 10px; margin: 0 0 12px; }
.reportdlg-disclaimer a { color: inherit; }
.reportdlg-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }

/* report_list() on service.php: public, its own section, deliberately
   plainer than an incident card (no left border, no severity color) so it
   never reads as an official update. */
.report-list-label { font-style: italic; margin-top: -4px; }
.report-list { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-md); overflow: hidden; }
.report-item { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px;
  padding: 8px 12px; border-top: 1px solid var(--hairline); font-size: var(--fs-sm); }
.report-item:first-child { border-top: 0; }
.report-item-meta { flex: none; font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
.report-item-note { flex: 1 1 200px; min-width: 0; }
.report-item-redacted { flex: 1 1 200px; min-width: 0; font-style: italic; color: var(--muted); }
.report-item-hide { flex: none; margin-left: auto; }

/* A hidden report stays in the reports list so an owner can see and undo
   what they hid, but reads as inactive: dimmed, never bold or colored. */
.reportrow-hidden { opacity: .55; }
.reportrow-hidden td { color: var(--muted); }

/* Inline status editor, identical on the board, a service page, and the dashboard. */
.editor { border-top: 1px solid var(--rule); background: var(--recess); padding: 12px 14px; }
.panel > .editor { border: 1px solid var(--rule); border-radius: var(--r-md); margin-top: 10px; }
.ed-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 7px; }
.ed-head b { font-size: var(--fs-md); line-height: var(--lh-heading); letter-spacing: var(--ls-tight); }
.ed-head .hint { margin: 0; }
.ed-error { margin: 0 0 8px; font-size: var(--fs-sm); }
.ed-note { margin-bottom: 8px; }
.lights { display: flex; gap: 6px; flex-wrap: wrap; }
.lbtn { flex: 1 1 120px; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 42px; padding: 8px 10px; border: 1px solid var(--rule); border-radius: var(--r-sm);
  background: var(--card); color: var(--ink); font: 600 var(--fs-sm) var(--sans); cursor: pointer; }
.lbtn::before { content: ""; width: 10px; height: 10px; border-radius: 50%; flex: none; }
.lbtn.green::before { background: var(--green); }
.lbtn.yellow::before { background: var(--amber); }
.lbtn.red::before { background: var(--red); }
.lbtn.blue::before { background: var(--blue); }
.lbtn.green:hover { border-color: var(--green); background: var(--green-wash); }
.lbtn.yellow:hover { border-color: var(--amber); background: var(--amber-wash); }
.lbtn.red:hover { border-color: var(--red); background: var(--red-wash); }
.lbtn.blue:hover { border-color: var(--blue); background: var(--blue-wash); }
.ed-more { margin-top: 9px; }
.ed-more > summary { font-size: var(--fs-sm); color: var(--muted); cursor: pointer; padding: 3px 0; }
/* The disclosure that opens the full scheduling/announcement form reads as
   a secondary action, not body copy: bordered and padded like .btn.sec,
   with its own chevron standing in for the default marker so it can rotate
   on open instead of just flipping a triangle glyph. */
.ed-more.wide > summary { list-style: none; display: inline-flex; align-items: center; gap: 8px;
  min-height: 42px; font: 600 var(--fs-sm) var(--sans); color: var(--ink); background: var(--card);
  border: 1px solid var(--rule); border-radius: var(--r-sm); padding: 0 14px; }
.ed-more.wide > summary::-webkit-details-marker { display: none; }
.ed-more.wide > summary:hover { border-color: var(--muted); }
.ed-more.wide > summary::after { content: ""; width: 7px; height: 7px; flex: none;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg); transition: transform .15s ease; }
.ed-more.wide[open] > summary::after { transform: rotate(-135deg); }
.ed-open { margin-top: 10px; }
.ed-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: var(--fs-sm); padding: 6px 0; border-top: 1px solid var(--hairline); }
.ed-line a { flex: 1; min-width: 120px; }
.ed-foot { margin-top: 9px; font-size: var(--fs-xs); line-height: var(--lh-muted); }
.ed-foot a { color: var(--muted); }

/* Hairline dividers come from the 1px gap over a ruled background.

   The track minimum is a minimum, not a width: auto-fill still reflows at
   every viewport. It is larger for a signed-in owner because their tiles
   carry a second chip (Update as well as Report), and the service name is
   the more important content -- so the board trades a column for name
   width rather than starving the name to seat both chips. Signed out, at
   1440px this is 4 columns; signed in, 3. */
/* The separators used to be the grid's own background showing through a
   1px gap. That also colored every slot a band did not fill -- a group of
   two services in a three-track row left a rule-colored void beside it that
   read as a rendering fault. Now each tile paints its own right and bottom
   hairline into the gap, and the grid's background is the recess tone, so
   an unfilled slot reads as an empty slot on the panel and nothing more.
   Only right and bottom, so a tile's hairline never doubles up against the
   band's own border above it or the group rail beside it. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1px; background: var(--recess); }
body.signed-in .grid { grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); }
.tile { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
  background: var(--card); text-decoration: none; min-width: 0; }
.tile:hover { background: var(--recess); }
.tile .tt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* Up to two lines before ellipsizing, so long names like "Heart Brain
   Vascular" or "Environmental Services" read in full even at 380px. The
   lamp stays a fixed, un-shrinkable flex sibling, so it is never crowded
   out by a wrapped name. */
.tile .tname { font-size: var(--fs-base); font-weight: 500; line-height: var(--lh-heading); min-width: 0; width: 100%;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  line-clamp: 2; overflow: hidden; text-overflow: ellipsis; }
.tile .tnote { font-size: var(--fs-sm); color: var(--muted); line-height: var(--lh-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile.red .tnote { color: var(--red); }
.tile.yellow .tnote { color: var(--amber-tint-text); }
.tile.blue .tnote, .tile.info .tnote { color: var(--blue); }
.tile .lamp { flex: none; width: 9px; height: 9px; }
/* A colored edge on the tile itself, so a band of affected services reads
   as a stripe down the left of the board from across the room and not only
   as a 9px lamp. Operational tiles get no edge at all -- on a normal day
   that is every tile, and a board of green stripes would be noise. It is an
   inset shadow rather than a border so it costs no layout: the tile keeps
   its exact padding, and the overflow rules above still hold. */
.tile.red { box-shadow: inset 3px 0 0 var(--red); }
.tile.yellow { box-shadow: inset 3px 0 0 var(--amber); }
.tile.blue, .tile.info { box-shadow: inset 3px 0 0 var(--blue); }
/* The status light itself never changes for an unconfirmed report spike --
   only an edge and a hairline, so it can never be mistaken for a real open
   incident. */
.tile.unconfirmed { box-shadow: inset 3px 0 0 var(--amber), inset 0 0 0 1px var(--amber-tint-border); }

/* A tile stands out for a moment when a poll finds its light has moved,
   then settles. Never a loop, and never on first paint: the class is only
   ever added by the poller. */
.tilewrap.tile-flash { animation: tileedge 2s ease-out; }
@keyframes tileedge { from { background: var(--highlight); } to { background: var(--card); } }
@media (prefers-reduced-motion: reduce) { .tilewrap.tile-flash { animation: none; } }

/* Section headings are labels on an instrument, not headlines: uppercase,
   monospaced, tracked, with a rule running out to the right margin so the
   page reads as a stack of labelled registers. Set in --ink rather than
   --muted because on the pages that have no status bar above them
   (Reports, My services, Admin, Sign in) this same element is the page's
   own heading and has to carry that weight. The rule is a flex child of
   the heading itself, so it always ends exactly at the content width
   without anything having to be added to the markup. */
.section-title { font: var(--fs-xs) var(--mono); letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink); margin: 22px 0 9px; display: flex; align-items: center; gap: 9px; }
.section-title a { color: inherit; }
.section-title .count { font-variant-numeric: tabular-nums; color: var(--muted); }
.section-title::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--rule); }

.incident { background: var(--card); border: 1px solid var(--rule); border-left: 3px solid var(--rule);
  border-radius: var(--r-md); padding: 12px 14px; margin-bottom: 8px; line-height: var(--lh-body); }
.incident.red { border-left-color: var(--red); }
.incident.yellow { border-left-color: var(--amber); }
.incident.blue, .incident.info { border-left-color: var(--blue); }
.incident.green { border-left-color: var(--green); }
/* Heading sits tight against the meta line right under it -- they read as
   one cluster -- then a clearly bigger gap opens before the body starts, so
   spacing alone marks where the card's actual content begins. */
.incident h3 { margin: 0 0 4px; font-size: var(--fs-md); font-weight: 600; letter-spacing: var(--ls-tight); line-height: var(--lh-heading); }
.incident h3 a { text-decoration: none; }
.incident h3 a:hover { text-decoration: underline; }
.meta { font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums; line-height: var(--lh-muted); color: var(--muted); display: flex; flex-wrap: wrap;
  gap: 4px 10px; align-items: center; }
.field { margin-top: 12px; font-size: var(--fs-base); line-height: var(--lh-body); font-variant-numeric: tabular-nums;
  max-width: 68ch; }
.field b, .workaround b { display: block; font: var(--fs-2xs)/1.6 var(--mono); letter-spacing: var(--ls-wide);
  text-transform: uppercase; color: var(--muted); }
.workaround { background: var(--blue-wash); border: 1px solid var(--blue-tint-border); border-radius: var(--r-sm);
  padding: 8px 10px; margin-top: 12px; font-size: var(--fs-base); line-height: var(--lh-body); max-width: 68ch; }
.workaround b { color: var(--blue); }

.pill { display: inline-block; font: var(--fs-2xs)/1 var(--mono); letter-spacing: var(--ls-wide);
  text-transform: uppercase; padding: 5px 7px; border-radius: var(--r-sm); background: var(--recess);
  border: 1px solid var(--rule); color: var(--muted); }
.pill.red { background: var(--red-tint); border-color: var(--red-tint-border); color: var(--red-tint-text); }
.pill.yellow { background: var(--amber-tint); border-color: var(--amber-tint-border); color: var(--amber-tint-text); }
.pill.blue, .pill.info { background: var(--blue-tint); border-color: var(--blue-tint-border); color: var(--blue-tint-text); }
.pill.green { background: var(--green-tint); border-color: var(--green-tint-border); color: var(--green-tint-text); }
.pill.overdue { background: var(--red-solid); border-color: var(--red-solid); color: var(--on-accent); }

.filters { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 8px 0 10px; }
.filters select { width: auto; }
.chip { border: 1px solid var(--rule); background: var(--card); color: var(--ink); border-radius: var(--r-sm);
  padding: 5px 10px; font-size: var(--fs-sm); cursor: pointer; user-select: none; text-decoration: none; display: inline-block; }
.chip:hover { border-color: var(--muted); }
.chip.active { background: var(--ink); color: var(--on-ink); border-color: var(--ink); }
.feed { border-left: 1px solid var(--rule); margin-left: 6px; padding-left: 0; list-style: none; }
.feed li { position: relative; padding: 6px 0 6px 20px; }
.feed li::before { content: ""; position: absolute; left: -4px; top: 12px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--dot-off); box-shadow: 0 0 0 3px var(--paper); }
.feed li.red::before { background: var(--red); }
.feed li.yellow::before { background: var(--amber); }
.feed li.green::before { background: var(--green); }
.feed li.blue::before, .feed li.info::before { background: var(--blue); }
.feed .t { font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums; line-height: var(--lh-muted); color: var(--muted); }
.feed .ttl { font-size: var(--fs-base); }
.feed .bd { font-size: var(--fs-sm); line-height: var(--lh-muted); color: var(--muted); max-width: 68ch; }
/* A new row briefly stands out, then settles into the same look as the rest
   of the list -- never a loop, and skipped entirely on first load since the
   class is only added by the poller, not the initial render. */
.feed li.isnew { animation: rowflash 2s ease-out; }
@keyframes rowflash { from { background: var(--highlight); } to { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .feed li.isnew { animation: none; } }

/* Skeleton rows fill the feed's shape until the first fetch resolves, so the
   page never shows a bare blank list. */
.feed .skel { padding: 6px 0 6px 20px; }
.feed .skel::before { background: var(--rule); }
.skel-bar { display: block; height: 9px; border-radius: var(--r-sm); background: var(--rule); }
.skel-bar.t { width: 38%; margin-bottom: 6px; }
.skel-bar.ttl { width: 72%; height: 11px; }
@media (prefers-reduced-motion: no-preference) {
  .skel-bar { animation: shimmer 1.4s ease-in-out infinite; }
  @keyframes shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
}

.emptystate { color: var(--muted); font-size: var(--fs-sm); line-height: var(--lh-muted); }

.panel { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-md);
  padding: 14px 16px; margin-bottom: 12px; }
.svc-head { display: flex; align-items: center; gap: 12px; }
.svc-head b { font-size: var(--fs-md); line-height: var(--lh-heading); letter-spacing: var(--ls-tight); }
label.f { display: block; margin: 12px 0 4px; font: var(--fs-2xs)/1.6 var(--mono);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--muted); }
input[type=text], input[type=password], input[type=email], input[type=datetime-local], select, textarea {
  width: 100%; padding: 9px 11px; border: 1px solid var(--rule); border-radius: var(--r-sm);
  font: var(--fs-base) var(--sans); background: var(--card); color: var(--ink); }
input[type=text]:hover, input[type=password]:hover, input[type=email]:hover,
input[type=datetime-local]:hover, select:hover, textarea:hover { border-color: var(--muted); }
textarea { min-height: 72px; resize: vertical; }
input:focus, select:focus, textarea:focus, button:focus-visible, .chip:focus-visible,
summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
/* admin.php and dashboard.php close nearly every form with a bare <p> around
   its submit button(s) rather than a styled wrapper. Unstyled, that <p>
   carries the browser's default ~1em top+bottom margin -- roughly 27px of
   unaccounted space around every single Save/Add/Post button, which is the
   other half (with .hint below) of why those pages read taller than the
   signed-out board. A form's own fields already carry their own spacing
   (label.f, .grid2's gap), so only a small top margin is needed here. */
.btn { display: inline-block; background: var(--ink); color: var(--on-ink); border: 1px solid var(--ink);
  border-radius: var(--r-sm); padding: 10px 16px; font: 600 var(--fs-sm) var(--sans); cursor: pointer; text-decoration: none; }
form > p { margin: 10px 0 0; }
.btn:hover { background: var(--muted); border-color: var(--muted); }
.btn.sec { background: var(--card); color: var(--ink); border: 1px solid var(--rule); }
.btn.sec:hover { background: var(--recess); border-color: var(--muted); }
.btn.danger { background: var(--red-solid); border-color: var(--red-solid); color: var(--on-accent); }
.btn.danger:hover { background: var(--red-tint-text); border-color: var(--red-tint-text); }
.btn.sm { padding: 6px 11px; font-size: var(--fs-xs); }
summary.btn { list-style: none; }
summary.btn::-webkit-details-marker { display: none; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* Full shorthand, not margin-top alone -- on a bare <p class="hint"> (by far
   its most common home, admin.php especially) an unset bottom would still
   carry the browser's default ~1em paragraph margin. That stray margin,
   repeated across every hint and every lone submit button (see the
   form > p rule above, by .btn), is what made the signed-in panels read so
   much taller than the signed-out board. */
.hint { font-size: var(--fs-sm); line-height: var(--lh-muted); font-variant-numeric: tabular-nums; color: var(--muted); margin: 4px 0 0; }
.ok-text { color: var(--green); }
.err-text { color: var(--red); }
.notice { background: var(--green-tint); border: 1px solid var(--green-tint-border); color: var(--green-tint-text);
  border-left: 3px solid var(--green); padding: 10px 14px; border-radius: var(--r-sm); margin: 14px 0;
  font-size: var(--fs-base); line-height: var(--lh-body); }
.notice.warn { background: var(--amber-tint); border-color: var(--amber-tint-border); border-left-color: var(--amber); color: var(--amber-tint-text); }
.notice.err { background: var(--red-tint); border-color: var(--red-tint-border); border-left-color: var(--red); color: var(--red-tint-text); }
table.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
table.tbl th { text-align: left; font: var(--fs-2xs)/1.6 var(--mono); letter-spacing: var(--ls-wide);
  text-transform: uppercase; color: var(--muted); border-bottom: 1px solid var(--rule); padding: 8px 6px; }
table.tbl td { padding: 9px 6px; border-bottom: 1px solid var(--hairline); vertical-align: middle; font-variant-numeric: tabular-nums; }
table.tbl tr:last-child td { border-bottom: 0; }
/* .meta is a flex row of chips inside an incident card, but in a table cell
   it is just text. As a flex container the cell drops out of the table's own
   width distribution, which is what collapsed the date column on the Past
   table and left those rows running several times taller than their
   content. Inside a table, .meta is a cell again. */
td.meta { display: table-cell; }
footer.foot { border-top: 1px solid var(--rule); margin-top: 28px; padding-top: 12px;
  font-size: var(--fs-sm); line-height: var(--lh-muted); color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; }

.webhookrow { padding: 10px 0; border-top: 1px solid var(--hairline); }
.webhookrow:first-of-type { border-top: 0; padding-top: 6px; }

/* History grid: one row per service, a time axis header, and small squares
   per bucket, all in a single CSS grid so the header labels are guaranteed
   to line up with their columns -- nothing here is aligned by hand. The
   name column stays sticky while the grid scrolls horizontally; cells never
   go below 11px, so a dense range scrolls instead of shrinking further. */
.rangepick { display: flex; gap: 6px; margin: 10px 0 10px; flex-wrap: wrap; }
.rangechip { text-decoration: none; display: flex; flex-direction: column; align-items: flex-start;
  gap: 1px; padding: 6px 12px; line-height: var(--lh-heading); }
.rangechip .rc-span { font-weight: 600; }
.rangechip .rc-res { font: var(--fs-2xs) var(--mono); line-height: var(--lh-muted); color: var(--muted); }
/* on-ink at reduced opacity rather than a hardcoded light-mode white --
   .chip.active fills with --ink, so its text needs --on-ink in both themes,
   dark mode's near-white fill included. Still >6:1 against the fill either
   way, comfortably past WCAG AA for this size text. */
.rangechip.active .rc-res { color: var(--on-ink); opacity: .7; }
.rangepick .rangechip:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }

.histlegend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 12px;
  font-size: var(--fs-sm); color: var(--muted); }
.histlegend .lg { display: inline-flex; align-items: center; gap: 5px; }
.histlegend .sw { width: 11px; height: 11px; border-radius: 1px; flex: none;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.08); }
.histlegend .sw.red { background: var(--red); }
.histlegend .sw.yellow { background: var(--amber); }
.histlegend .sw.blue { background: var(--blue); }
.histlegend .sw.green { background: var(--green); }
.histlegend .sw.neutral-light { background: var(--paper); }
.histlegend .sw.neutral-dark { background: var(--rule); }

.histscroll { overflow-x: auto; overflow-y: hidden; background: var(--card); border: 1px solid var(--rule);
  border-radius: var(--r-md); position: relative; }
/* No min-width here: a grid container's automatic minimum already respects
   each track's minmax() floor, so cells hold their 11px minimum on a narrow
   screen without help. min-width: max-content was doing the opposite of
   what it looked like -- it forced an intrinsic-sizing pass where the 1fr
   tracks stopped distributing the container's actual width and every cell
   locked to a fixed content-based size instead, well past 11px even on a
   1440px screen with room to spare.
   The right padding is deliberately wider than the left: an axis label is
   free to overflow its own column when its span is too narrow for its text
   (never truncate below 10px), and the interior always has an unlabeled
   neighbor cell to spill into, but the last label in the row does not --
   there is nothing to its right except the grid's own edge. That edge
   recurs by design, not by accident: whenever "now" lands on the range's
   own boundary (every few hours for the 24-hour view, daily for the 7-day
   view), the final column becomes a one-cell-wide label with nowhere to
   spill. 38px covers the widest label text (a 6-character date needs up to
   36px) even at the 11px column floor, so that overflow lands inside the
   grid's own box instead of past it -- the 1fr tracks absorb the extra
   padding by redistributing a fraction of a pixel each, not by growing the
   container past its parent. */
.histgrid { display: grid; column-gap: 2px; row-gap: 3px; align-items: center;
  padding: 10px 38px 10px 12px; position: relative; }

.hname { grid-column: 1; position: sticky; left: 0; z-index: 3; background: var(--card);
  font: var(--fs-2xs)/1.6 var(--mono); letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--muted); padding: 4px 10px 4px 0; }
.haxis { font: var(--fs-2xs) var(--mono); font-variant-numeric: tabular-nums; line-height: var(--lh-muted); color: var(--muted);
  white-space: nowrap; overflow: visible; padding-bottom: 3px; align-self: end; }

.gsep { background: var(--recess); margin: 2px calc(-1 * 38px) 0 calc(-1 * 12px); padding: 5px 12px 4px;
  border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); }
.gsep span { position: sticky; left: 12px;
  font: var(--fs-2xs)/1.4 var(--mono); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--muted); }

/* Names here are the row's only label, so they get the width to be read
   in full. The name column is 172px, set with the grid template in
   history.php: "Echocardiography" and "Environmental Services" both fit at
   this size, which they did not at 140. */
.rowname { position: sticky; left: 0; z-index: 3; background: var(--card);
  display: flex; align-items: center; gap: 6px; min-width: 0; text-decoration: none;
  color: var(--ink); font-size: var(--fs-sm); font-weight: 500; line-height: var(--lh-heading); padding: 3px 10px 3px 0; }
.rowname span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rowname:hover { text-decoration: underline; }

/* aspect-ratio keeps every cell square as its track width grows from the
   11px floor up to its 1fr share on a wide screen, with no fixed height to
   keep in sync by hand. */
.cell { display: block; width: 100%; aspect-ratio: 1 / 1; min-width: 11px; border-radius: 1px;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.05); }
.cell.neutral { box-shadow: inset 0 0 0 1px rgba(0,0,0,.04); }
span.cell { cursor: default; }
a.cell:hover, a.cell:focus-visible { box-shadow: inset 0 0 0 1px rgba(0,0,0,.4); }
a.cell:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }

/* A subtle full-height rule at natural time boundaries, so a 48- or 60-cell
   row still reads as a sequence of days rather than a wall of squares. One
   element per boundary, spanning every row via an explicit grid-row (the
   caller knows the real last row; -1 does not reliably reach it when every
   row here is implicit), rather than a border repeated by hand on every
   cell in every row. */
.coldiv { border-left: 1px solid var(--rule); pointer-events: none; }

/* ===================================================================
   Wallboard (index.php?tv=1): a wall-mounted monitor read from three
   metres, not a hand. No masthead, no forms -- just the signal. */
.tv { background: var(--paper); color: var(--ink); font: var(--fs-base)/var(--lh-body) var(--sans); margin: 0; }
.tv-wrap { padding: 20px 28px 26px; }
.tv-overall { border-radius: var(--r-md); padding: 22px 30px; margin: 0 0 20px; gap: 22px; border-left-width: 7px; }
.tv-overall .signal { gap: 10px; }
.tv-overall .lamp { width: 26px; height: 26px; box-shadow: inset 0 0 0 2px rgba(0,0,0,.1); }
.tv-overall .lamp.on.green { box-shadow: 0 0 0 7px var(--green-glow); }
.tv-overall .lamp.on.yellow { box-shadow: 0 0 0 7px var(--amber-glow); }
.tv-overall .lamp.on.red { box-shadow: 0 0 0 7px var(--red-glow); }
.tv-overall .lamp.on.blue, .tv-overall .lamp.on.info { box-shadow: 0 0 0 7px var(--blue-glow); }
.tv-overall h1 { font-size: 40px; letter-spacing: var(--ls-tight); font-weight: 600; line-height: var(--lh-heading); }
.tv-overall .when { font-size: 20px; font-family: var(--mono); font-variant-numeric: tabular-nums; line-height: var(--lh-muted); }

.tv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 10px; }
.tv-tile { display: flex; align-items: center; gap: 18px; background: var(--card); border: 1px solid var(--rule);
  border-left: 7px solid var(--rule); border-radius: var(--r-md); padding: 18px 22px; }
.tv-tile.green { border-left-color: var(--green); }
.tv-tile.yellow { border-left-color: var(--amber); }
.tv-tile.red { border-left-color: var(--red); }
.tv-tile.blue, .tv-tile.info { border-left-color: var(--blue); }
.tv-tile.unconfirmed { border-left-color: var(--amber); border-left-style: dashed; }
.tv-tile .signal { gap: 7px; }
.tv-tile .lamp { width: 18px; height: 18px; }
.tv-tt { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
/* A wallboard exists to be read from across the room, so a long service
   name wraps to a second line rather than ellipsizing: "Clinical Labora..."
   is exactly the thing this screen must never say. The tile has no fixed
   height, so the row simply grows. */
.tv-name { font-size: 26px; font-weight: 600; letter-spacing: var(--ls-tight); line-height: var(--lh-heading);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-clamp: 2;
  overflow: hidden; text-overflow: ellipsis; }
.tv-note { font-size: 16px; color: var(--muted); line-height: var(--lh-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tv-tile.red .tv-note { color: var(--red); }
.tv-tile.yellow .tv-note { color: var(--amber-tint-text); }
.tv-tile.blue .tv-note, .tv-tile.info .tv-note { color: var(--blue); }

.tv-foot { display: flex; align-items: center; gap: 18px; margin-top: 20px;
  border-top: 1px solid var(--rule); padding-top: 12px;
  font: 15px var(--mono); line-height: var(--lh-muted); color: var(--muted); }
.tv-clock { font-variant-numeric: tabular-nums; color: var(--ink); }
.tv-age { font-variant-numeric: tabular-nums; }
/* Only the poll clock reddens. "Last change" is quieter than the line
   beside it because on a good week it reads in days, and days of nothing
   changing is the result this board exists to show, not a fault. */
.tv-age.stale { color: var(--red); }
.tv-change { font-variant-numeric: tabular-nums; opacity: .75; }

/* A tile briefly stands out when a poll changes its status, then settles
   into its new, permanent look -- never a loop, and only ever triggered by
   a change, so the first paint of the page is always still. */
.tv-tile.tv-flash { animation: tileflash 2s ease-out; }
@keyframes tileflash { from { box-shadow: 0 0 0 4px var(--tile-flash), var(--shadow); } to { box-shadow: var(--shadow); } }
@media (prefers-reduced-motion: reduce) { .tv-tile.tv-flash { animation: none; } }

/* ===================================================================
   Responsive. The board itself needs no breakpoint to reflow -- every
   grid here is auto-fill with a track minimum -- so these only handle
   what auto-fill cannot: touch target sizes, the 16px input floor that
   stops iOS zooming on focus, and the group rail, which has to stop
   being a rail once there is no width to spare beside it. */

/* Wide screens: the board earns another column rather than stretching
   four tiles across 1100px of card. */
@media (min-width: 1500px) {
  .wrap { max-width: 1280px; }
}

@media (max-width: 760px) { .tagline { display: none; } }

@media (max-width: 620px) {
  input[type=text], input[type=password], input[type=email], input[type=datetime-local],
  select, textarea { font-size: 16px; }
  .btn { min-height: 42px; }
  .btn.sm { min-height: 36px; }
  .chip { min-height: 34px; display: inline-flex; align-items: center; }
  .filters select { width: 100%; }
  .lbtn { flex: 1 1 calc(50% - 3px); }
  /* The bar wraps here so the headline gets the full width and the
     timestamp drops to its own line underneath, which is what the
     width: 100% below has always been asking for. */
  .overall { padding: 10px 13px; flex-wrap: wrap; }
  .overall h1 { font-size: var(--fs-base); }
  .overall .when { font-size: var(--fs-2xs); }
  .masthead { position: static; }
  .masthead nav { width: 100%; margin-left: 0; gap: 13px; row-gap: 6px; flex-wrap: wrap; }
  .panel { overflow-x: auto; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); }
  /* Signed in on a phone the tile carries both chips, which together with
     the lamp leave a two-column cell nothing at all for the name. One
     column per row instead: an owner gets a readable name and both
     controls, rather than two columns of clipped chips. */
  body.signed-in .grid { grid-template-columns: 1fr; }
  /* Two columns at 380px leaves ~168px per tile, so the tile chrome is
     trimmed to hand the name back what it can: the name still has two
     clamped lines to fall back on, and the chips are already at their
     short labels here. Nothing is floored in pixels -- the tile is
     min-width: 0 at every width. */
  .tile { min-height: 44px; padding: 8px 9px; gap: 7px; }
  .band { display: block; }
  .band .glabel { border-right: 0; border-bottom: 1px solid var(--rule); padding: 6px 12px; }
  .grid2 { grid-template-columns: 1fr; }
  .overall .when { width: 100%; margin-left: 0; text-align: left;
    flex-direction: row; gap: 10px; flex-wrap: wrap; }
  .section-title { margin-top: 18px; }
  .tv-wrap { padding: 14px 16px 18px; }
  .tv-overall { padding: 16px 18px; gap: 14px; }
  .tv-overall h1 { font-size: 24px; }
  .tv-overall .lamp { width: 18px; height: 18px; }
  .tv-name { font-size: 19px; }
  .tv-note { font-size: 13px; }
  .tv-foot { font-size: 13px; }
}

.tv .themebtn, .tv-wrap .themebtn { display: none; }