/* Fleet Ops Wall — 4× 55" TVs as one 3840×2160 surface.
   Palette: dataviz reference instance, dark mode (surface #1a1a19, page #0d0d0d).
   Tail identity colors validated (5 slots, dark, all checks pass).
   Flight-category colors are the aviation-standard set; every use carries a text
   label — color never encodes alone. */

:root {
  /* Liquid glass, dark. Chrome is translucent and cool; DATA colours (tail
     identity, status, flight categories) are untouched — they were validated
     against a dark surface and glass only changes the furniture. */
  --page: #05070c;
  --surface: rgba(255, 255, 255, 0.045);
  --card: rgba(255, 255, 255, 0.05);
  --ink: #ffffff;
  --ink-2: #c9cdd7;
  --muted: #8b93a4;
  --grid: rgba(255, 255, 255, 0.07);
  --line: rgba(255, 255, 255, 0.15);
  --hair: rgba(255, 255, 255, 0.10);
  --glass-edge: rgba(255, 255, 255, 0.10);
  --glass-glint: rgba(255, 255, 255, 0.16);

  --blue: #3987e5;      /* POD1 */
  --orange: #d95926;    /* POD2 */
  --aqua: #199e70;      /* POD3 */
  --yellow: #c98500;    /* POD5 */
  --magenta: #d55181;   /* POD6 */

  --good: #0ca30c;
  --warn: #fab219;
  --serious: #ec835a;
  --crit: #d03b3b;

  --meter-track: #12324f;
  --violet: #9085e9;    /* manual override — distinct from every status colour */

  /* IBM Plex Sans: an engineering typeface — technical, restrained, and not the
     geometric grotesque every dashboard template reaches for. One family for
     the whole wall, prose and figures alike.

     --num is still its own variable even though it now resolves to the same
     face: it marks the slots that hold times and numbers, which is where
     tabular figures and the column widths below have to hold. Plex's digits
     are tabular by default (all ten are exactly 0.6em), so a ticking clock
     does not shimmy — tabular-nums is declared anyway so the fallback stack
     cannot reintroduce the jitter if the webfont fails to load. */
  --sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --num: var(--sans);
  /* Plex Mono, same family cut for the places that should read like avionics —
     currently the sign-in gate's clock, badge and annunciator. */
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--page); }
/* Inherited by everything including SVG text, so every column of figures on the
   wall lines up and a ticking clock never changes width. */
body { font-family: var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }

/* ---- stage scaling ---- */
#fit {
  position: absolute; left: 50%; top: 50%;
  width: 3840px; height: 2160px;
  transform-origin: center;
}
#stage {
  width: 100%; height: 100%;
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* FOUR EQUAL QUADRANTS, always. Each one is a whole TV in the wall, so the
     grid may never be asked to give a row away — an application switch with a
     row of its own made the top two screens shorter than the bottom two and
     broke the one-quadrant-per-screen mapping. The switch floats instead; see
     .app-switch. */
  grid-template-rows: 1fr 1fr;
  gap: 36px;                    /* TV bezel seam */

  padding: 24px;
  background: var(--page);
  animation: drift 240s ease-in-out infinite alternate;  /* burn-in guard */
}
/* ================= CRAFT application switch =================
   THIS BLOCK IS DUPLICATED VERBATIM IN craft-crew-pairing/css/pairing.css.
   The two applications are separate deployments with separate stylesheets, and
   the whole point of the control is that it does not move when you cross
   between them — so the rules have to be identical rather than merely similar.
   Change one, change the other.

   Pinned to the viewport, not to either page's layout: this board scales a
   3840x2160 stage to fit, and anything inside that scaling would have rendered
   at a different size on the other side of the link. Horizontally centred,
   which on the physical wall is the vertical bezel between the two upper TVs,
   so the split between the halves lands in the gap and each screen carries one
   whole label. */
.app-switch {
  position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 60;
  /* EQUAL HALVES, via two 1fr columns rather than by letting each label size
     itself: "Crew Pairing" is a wider word than "Dashboard", so content-sized
     halves put the divider off the centre line. Grid gets it exact. */
  display: inline-grid; grid-template-columns: 1fr 1fr; gap: 3px; padding: 3px;
  border-radius: 999px;
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.025) 45%, rgba(255, 255, 255, 0.05) 100%);
  backdrop-filter: blur(26px) saturate(1.45);
  -webkit-backdrop-filter: blur(26px) saturate(1.45);
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(255, 255, 255, 0.04),
    0 7px 19px rgba(0, 0, 0, 0.42);
}
.app-tab {
  font: 700 11px/1 "IBM Plex Sans", system-ui, sans-serif;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52); text-decoration: none;
  border: 1px solid transparent; border-radius: 999px;
  padding: 6px 20px; cursor: pointer; text-align: center; white-space: nowrap;
  transition: color 0.15s ease, background 0.2s ease;
}
/* The board you are on: a brighter piece of the same glass, lifted off the
   trough by its own highlight rather than by a flat fill. */
.app-tab.on {
  color: #fff; cursor: default;
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.17) 0%, rgba(255, 255, 255, 0.08) 55%, rgba(255, 255, 255, 0.13) 100%);
  border-color: rgba(255, 255, 255, 0.10);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 1px 5px rgba(0, 0, 0, 0.30);
}
@media (hover: hover) { .app-tab:not(.on):hover { color: rgba(255, 255, 255, 0.82); } }
/* No narrow-screen override any more: at half size the control already fits a
   375px phone with room to spare, and a second set of numbers is a second
   thing to keep in step across two repositories. */
/* Hidden while the board is full screen. That is the wall's unattended state —
   four TVs with nobody in front of them — and the switch would be the only
   chrome left floating over an otherwise edge-to-edge board. Harmless on Crew
   Pairing, which never sets the flag. */
:root[data-fs="on"] .app-switch { display: none; }
/* ================= end CRAFT application switch ================= */

/* The light field behind the glass. Dim on purpose — this wall runs all day
   on four TVs; the glow is for the panels to refract, not to be looked at. */
.ambient {
  position: absolute; inset: -80px; z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(46% 58% at 11% 6%,  rgba(64, 104, 186, 0.17), transparent 62%),
    radial-gradient(40% 50% at 87% 10%, rgba(102, 76, 190, 0.13), transparent 60%),
    radial-gradient(52% 62% at 80% 94%, rgba(28, 118, 146, 0.11), transparent 64%),
    radial-gradient(44% 56% at 18% 90%, rgba(128, 82, 168, 0.09), transparent 62%),
    radial-gradient(30% 34% at 50% 46%, rgba(70, 90, 140, 0.07), transparent 70%);
}
@keyframes drift {
  from { transform: translate(0, 0); }
  to   { transform: translate(2px, 1px); }
}

/* bezel seam guides (toggle with "b") */
#seams { position: absolute; inset: 0; pointer-events: none; }
#seams::before, #seams::after {
  content: ""; position: absolute; background: rgba(255, 80, 80, 0.5);
}
#seams::before { left: 50%; top: 0; width: 2px; height: 100%; transform: translateX(-1px); }
#seams::after  { top: 50%; left: 0; height: 2px; width: 100%; transform: translateY(-1px); }

/* ---- quadrant shell: the four glass slabs ---- */
.quad {
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.065) 0%,
    rgba(255, 255, 255, 0.022) 42%,
    rgba(255, 255, 255, 0.045) 100%);
  backdrop-filter: blur(26px) saturate(1.45);
  -webkit-backdrop-filter: blur(26px) saturate(1.45);
  border: 1px solid var(--glass-edge);
  border-radius: 24px;
  box-shadow:
    inset 0 1px 0 var(--glass-glint),               /* specular top edge */
    inset 0 -1px 0 rgba(255, 255, 255, 0.04),
    0 24px 60px rgba(0, 0, 0, 0.45);
  padding: 0 34px 28px;
  display: flex; flex-direction: column;
  overflow: hidden;
  min-height: 0;
  position: relative;   /* anchors the alert banner */
  z-index: 1;           /* above the ambient field */
}
.qhead {
  display: flex; align-items: center; justify-content: space-between;
  padding: 24px 0 18px;
  border-bottom: 1px solid var(--grid);
  flex: none;
}
.qhead h1 {
  font-size: 29px; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--ink-2);
}
.qhead-sub { padding-top: 20px; }
/* How old the panel below is. Lower case and unspaced against the tracked-out
   capitals of the title, so it reads as an annotation rather than part of the
   name. Muted while the feed is keeping up; amber once it is not — at which
   point the figure itself is the thing worth reading. */
.feed-age {
  font-size: 19px; font-weight: 600; letter-spacing: 0.04em;
  text-transform: none; color: var(--muted); margin-left: 16px;
}
.feed-age.old { color: var(--warn); }
[data-layout="mobile"] .feed-age { font-size: 10px; margin-left: 8px; }

/* ---- chips ---- */
.chip {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 22px; font-weight: 600;
  padding: 5px 16px; border-radius: 999px;
  border: 1.5px solid var(--line);
  color: var(--ink-2);
  white-space: nowrap;
}
.chip-demo { color: var(--muted); border-color: var(--line); letter-spacing: 0.1em; }
.chip-good    { color: var(--good);    border-color: rgba(12,163,12,.35);  background: rgba(12,163,12,.10); }
.chip-blue    { color: var(--blue);    border-color: rgba(57,135,229,.35); background: rgba(57,135,229,.10); }
.chip-warn    { color: var(--warn);    border-color: rgba(250,178,25,.35); background: rgba(250,178,25,.10); }
.chip-serious { color: var(--serious); border-color: rgba(236,131,90,.35); background: rgba(236,131,90,.10); }
.chip-muted   { color: var(--muted); }

/* flight-category pills: filled, dark ink on the fill (label always present) */
.cat-pill {
  display: inline-block;
  font-size: 20px; font-weight: 700; letter-spacing: 0.06em;
  padding: 4px 14px; border-radius: 999px;
  color: #0d0d0d;
}
.cat-key { display: flex; gap: 12px; }
.cat-key .cat-pill { font-size: 18px; padding: 3px 12px; }

/* ---- map quadrant: darker pane so the chart reads through it ---- */
.quad-map {
  background: linear-gradient(158deg,
    rgba(10, 15, 26, 0.62) 0%,
    rgba(7, 11, 19, 0.48) 50%,
    rgba(10, 15, 26, 0.58) 100%);
  padding: 0;
  position: relative;
}
.quad-map .qhead {
  position: absolute; top: 0; left: 0; right: 0; z-index: 2;
  padding: 24px 34px 18px; border-bottom: none;
  justify-content: center;      /* the clock block holds the centre */
}
.map-wrap { position: absolute; inset: 0; }
#mapsvg { width: 100%; height: 100%; display: block; }

.map-legend {
  /* Bottom-left, above the ticker. The zoom controls sit top-left, so the two
     do not share a corner. */
  position: absolute; left: 30px; bottom: 92px; z-index: 2;
  display: flex; gap: 28px; align-items: center;
  font-size: 22px; color: var(--muted);
  pointer-events: none;   /* never blocks a map drag */
}
.map-legend span { display: inline-flex; align-items: center; gap: 10px; }
.lg-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--ink-2); display: inline-block; }
.lg-line { width: 34px; height: 3px; background: var(--ink-2); display: inline-block; border-radius: 2px; }
.lg-faint { opacity: 0.35; }
.lg-ring { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--crit); display: inline-block; }
/* NEXRAD composite, drawn as reprojected cells by app.js. Held back so state
   outlines, routes and labels stay readable through the weather. */
#radar { opacity: 0.5; pointer-events: none; }
/* Panning writes a CSS transform (see applyView), so the compositor can move
   the painted map as a layer instead of repainting it every frame. The
   explicit box and origin pin CSS semantics to what the old transform
   ATTRIBUTE meant — without them the default origin is the box centre, and
   every zoom would pivot around the wrong point. */
#mapview { transform-box: view-box; transform-origin: 0 0; will-change: transform; }

.lg-radar {
  width: 22px; height: 13px; border-radius: 4px; display: inline-block;
  background: linear-gradient(90deg, rgba(64,160,72,.7), rgba(255,204,64,.8), rgba(255,84,64,.8));
}

/* TFR rings + radar cells on the map */
.tfr-ring { fill: rgba(208, 59, 59, 0.09); stroke: var(--crit); stroke-width: 2.5; }
.tfr-inner { fill: none; stroke: var(--crit); stroke-width: 1.5; opacity: 0.7; }
/* TFR ring geometry scales with the map; its tag text does not */
.tfr-tag { fill: var(--crit); font: 700 calc(19px / var(--mk, 1)) var(--sans); letter-spacing: 0.08em; }
.tfr-ring, .tfr-inner { vector-effect: non-scaling-stroke; }

.ticker {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: 14px;
  padding: 14px 34px;
  font-size: 24px;
  background: rgba(8, 12, 21, 0.55);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-top: 1px solid var(--glass-edge);
  color: var(--ink-2);
  white-space: nowrap; overflow: hidden;
}
.ticker .tk-label {
  font-size: 20px; font-weight: 700; letter-spacing: 0.12em;
  color: var(--muted); flex: none;
}
.ticker .tk-item { display: inline-flex; gap: 10px; align-items: baseline; }
.ticker .tk-sep { color: var(--line); margin: 0 22px; }
/* The strip scrolls, so its contents must never wrap or be laid out by flex
   against the parent — the track is the only thing the parent sees. */
.ticker .tk-track { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.ticker .tk-move {
  display: inline-flex; white-space: nowrap; will-change: transform;
  animation: tkroll 40s linear infinite;      /* duration set from measured width */
}
.ticker .tk-run { display: inline-flex; align-items: baseline; white-space: nowrap; }
/* Exactly two identical halves, so -50% lands pixel-aligned on the start of the
   second and the loop has no seam. */
@keyframes tkroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.ticker .tk-pod { font-weight: 700; }
.ticker .tk-arrow { color: var(--muted); margin: 0 8px; }
/* A METAR is code. Mono keeps the groups aligned as they slide past, which is
   how anyone actually reads one. */
.ticker .tk-metar { font-family: var(--mono); font-size: 21px; color: var(--ink-2); letter-spacing: 0.01em; }
.ticker .tk-quiet { color: var(--muted); }

/* ---- clock trio (map header): PT | UTC | ET ---- */
.clocks {
  display: flex; align-items: center; gap: 34px;
  background: rgba(10, 15, 26, 0.48);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-edge); border-radius: 18px;
  box-shadow: inset 0 1px 0 var(--glass-glint);
  padding: 10px 26px;
}
.ck { text-align: center; }
.ck-label { font-size: 19px; font-weight: 700; letter-spacing: 0.14em; color: var(--muted); }
.ck-time { font-family: var(--num); font-size: 34px; color: var(--ink-2); line-height: 1.15; }
.ck-utc .ck-time { font-size: 52px; font-weight: 600; color: var(--ink); }
/* Colon spacing is set per-instance by tm() in app.js, which writes explicit
   margins derived from the two digits either side. Nothing to declare here — a
   plain inline span is deliberate. An earlier version used inline-block plus a
   transform; that made each colon an atomic inline box, whose position browsers
   are free to round to whole device pixels, so the spacing drifted between
   platforms and zoom levels. Margins on an ordinary inline stay subpixel. */

/* ---- schedule quadrant: timeline ---- */
.sched-meta { display: flex; align-items: center; gap: 20px; }
.dateline { font-size: 25px; color: var(--ink-2); font-weight: 600; letter-spacing: 0.06em; }
.sched-wrap { flex: 1; min-height: 0; padding-top: 8px; position: relative; }
#schedsvg { width: 100%; height: 100%; display: block; }

/* The tail column's glass. Painted over the timeline, so the fill has to be
   opaque enough to hide a bar sliding beneath it — a translucent pane would
   show the very overlap it exists to remove — and the gradients carry the
   softness instead. Pointer events pass through: the whole SVG is the drag
   surface, and a dead strip down its left edge would be a trap. */
.gut { pointer-events: none; }
/* The phone's sticky tail-card overlay. On the wall the gutter is drawn inside
   the schedule svg itself and this stays empty and hidden. */
#gutsvg { display: none; }
/* Matched to `.card` in the flight-progress panel: same hairline, same 0.10
   top highlight, same radius. The fill is the composited equivalent of that
   card's translucent gradient (see the defs in renderSchedule) because SVG
   content cannot sit on a backdrop it is meant to hide. */
.gut-card { fill: url(#gut-card); stroke: var(--hair); stroke-width: 1; }
.gut-sheen { fill: rgba(255, 255, 255, 0.10); }
.ax-line { stroke: rgba(255, 255, 255, 0.055); stroke-width: 1; }
.ax-label { fill: var(--muted); font: 600 19px var(--sans); }
/* A touch above --grid: these now bisect the gaps between the tail cards
   exactly, and a line placed on purpose should look placed on purpose. */
.lane-sep { stroke: rgba(255, 255, 255, 0.09); stroke-width: 1; }
.g-pod { font: 700 28px var(--sans); fill: var(--ink); }
.g-reg { font: 22px var(--num); fill: var(--ink-2); }
/* The crew on the aeroplane: "Matus | Barber", captain first. The PIC and SIC
   tags came off at the operator's request — the ordering already says it, and
   the tags cost more width on this card than they were worth. */
.g-crew { font: 600 21px var(--sans); fill: var(--ink); }
.g-sep { fill: var(--muted); font-weight: 400; }
.g-nocrew { font: 600 19px var(--sans); fill: var(--muted); letter-spacing: 0.04em; }
.bar-label { font: 600 24px var(--sans); fill: var(--ink); }
.bar-label.dim { fill: var(--ink-2); }
.bar-on { font: 600 18px var(--num); fill: var(--good); }
/* Jet Insight's trip locator on a schedule strip. Quiet by design: it is a
   reference somebody types into another system, not something the board wants
   read at a glance, so it sits below the category in muted ink. */
.bar-trip { font: 600 13px var(--num); letter-spacing: 0.06em; }
/* The same row once the leg is running: EARLY or LATE in minutes, weighted like
   the deltas everywhere else on the board. Fill is set inline per strip. */
.bar-delta { font: 700 13px var(--sans); letter-spacing: 0.08em; }
/* the PLAN: a grey ghost behind the coloured bar, and its times in grey beside
   the actual. Deliberately recessive — it is the baseline, not the news. */
/* ---- rail viewport controls: on the title's own row ---- */
/* GRID, not the flex `space-between` every other .qhead uses. The ◀ LIVE ▶
   cluster has to sit on the CENTRE LINE of the panel, and space-between puts it
   wherever the widths of the title and the status chip happen to leave it. Equal
   outer columns fix that, and both must be able to SHRINK below their content or
   whichever is wider shoves the centre column off the line. */
[data-layout="wall"] .sched-head {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 16px; padding: 18px 0 14px;
}
.sched-title {
  display: flex; align-items: baseline; gap: 18px;
  justify-self: start; min-width: 0; overflow: hidden;
}
/* Date stacked over the buttons, both centred on the same line. */
.rail-centre {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  justify-self: center;
}
.rail-centre .dateline { white-space: nowrap; }
.rail-group { display: flex; align-items: center; gap: 12px; }
[data-layout="wall"] .sched-head .sched-meta { justify-self: end; }
/* Bottom-right corner of the timeline itself (user-specified), overlaid rather
   than given a row of its own — the same treatment as the map's pan hint. */
.rail-hint {
  position: absolute; right: 4px; bottom: 2px; z-index: 2;
  font-size: 19px; color: var(--muted); opacity: 0.65;
  max-width: 46%; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  pointer-events: none;              /* the timeline under it is draggable */
}
.rail-btn {
  font: 700 20px var(--sans); letter-spacing: 0.08em;
  color: var(--ink-2); background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 14px; cursor: pointer; min-width: 44px;
  /* Sized for "GO LIVE", which is the longer of the two labels. Without this it
     wrapped to two lines the moment it stopped saying "LIVE" and shoved the
     range and the status chip off the end of the header. */
  white-space: nowrap; flex: none;
}
.rail-live { min-width: 116px; }
@media (hover: hover) { .rail-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); } }
.rail-btn:disabled { opacity: 0.3; cursor: default; }
/* LIVE lit while following the clock; it reads "GO LIVE" when it is not, so the
   button says what pressing it DOES rather than what the state is. */
.rail-live.on { color: #0d1117; background: var(--good); border-color: var(--good); }
.rail-range { font: 20px var(--num); color: var(--muted); white-space: nowrap; flex: none; }
/* The dateline can now read "WED, AUG 12 → THU, AUG 13" when the 12-hour view
   straddles midnight, so it must not be the thing that wraps either. */
/* Wall layout only. The phone stylesheet deliberately wraps this header and
   gives .sched-meta a full-width row of its own — scoping matters here, and I
   spent a while "fixing" the mobile layout by testing in an 800px window. */
[data-layout="wall"] .sched-meta { flex-wrap: nowrap; min-width: 0; flex: none; }
#schedsvg { cursor: grab; touch-action: pan-y; }
#schedsvg.dragging { cursor: grabbing; }
/* Eastern midnight, so a rail scrolled across three days says which day it is */
.day-line { stroke: rgba(255, 255, 255, 0.30); stroke-width: 2; stroke-dasharray: 2 7; }
.day-lbl { font: 700 17px var(--sans); fill: var(--muted); letter-spacing: 0.12em; }

/* A crew change on the ground between two legs. Letter-spaced caps in the same
   steel as its block, so it reads as a note about the aeroplane rather than as
   another flight. */
.swap-label { font-weight: 700; fill: #cbd3e0; letter-spacing: 0.12em; }

/* Where the leg was SUPPOSED to be. Dashed and unpainted so it reads as a plan
   rather than as a second flight, and neutral so it cannot be mistaken for one
   of the four category colours. Only the part the actual does not cover is ever
   drawn, so what you see is the delay itself. */
.bar-plan {
  fill: rgba(255, 255, 255, 0.05);
  stroke: rgba(255, 255, 255, 0.34);
  stroke-width: 1.5;
  stroke-dasharray: 7 5;
}
/* Its label, at the weight of the dashes rather than of the strips. The box is
   a reference the eye should be able to skip once it has been understood, so
   this stays dimmer than anything the aeroplane actually did. */
.bar-plan-t {
  fill: rgba(255, 255, 255, 0.62);
  font-weight: 700; letter-spacing: 0.1em;
}
.bar-plan-tm {
  fill: rgba(255, 255, 255, 0.46);
  font-family: var(--num); font-weight: 500; letter-spacing: 0.04em;
}

/* A maintenance block wears the operator's pink-red and is drawn at full strip
   height, so a row that cannot fly says so from across the room. Fill and
   stroke are set per-element — a `fill` declared HERE would outrank the
   gradient attribute the renderer puts on the rect, which is exactly how this
   band stayed grey the first time it was given a colour. */
.mx-band { stroke-width: 1.5; }
.mx-text { font: 600 21px var(--sans); fill: var(--ink-2); }
.mx-text .mx-aog { fill: var(--ink); font-weight: 700; }
.now-line { stroke: #ffffff; stroke-width: 2; opacity: 0.55; }
.now-pill { fill: rgba(16, 22, 36, 0.85); stroke: var(--glass-edge); stroke-width: 1.5; }
.now-text { font: 600 20px var(--num); fill: var(--ink); }

/* ---- weather quadrant: exceptions only (VFR fields live on the map) ---- */
/* TILED, not stacked. Each airport was a full-width band three lines deep, so a
   panel this wide held three fields and a summary line for everything else. Two
   columns fit twice as many at the same size, and every field that matters gets
   its own box instead of being folded into an "ALSO" list.
   Same rule as the notification centre: take only what the content needs. */
/* WRAPPING FLEX, not a fixed grid. A two-column grid could only ever put two
   across and left a hole whenever the count was odd; a three-column one would
   leave two. Flex with `flex-grow` on every card fits as many per row as the
   width allows — three here — and the items on whatever row ends up short
   stretch to fill it. No parity rules, no holes at any count. */
.wx-list {
  flex: 0 1 auto; min-height: 0; overflow: hidden; padding-top: 6px;
  display: flex; flex-wrap: wrap; align-content: flex-start; gap: 12px;
}
/* NO GROW. Letting them stretch meant two airports became two half-panel boxes
   with three chips rattling around in each — the row was full and the cards
   were empty, which is the wasteful shape this panel kept finding. A fixed
   width sizes the card to what a card holds; three fit across, and whatever is
   left over is left over (user-specified).

   594px is the WIDEST that still fits three across a 1808px panel
   (3 x 594 + 2 x 12 gap = 1806). It was 580; the extra 14px is all there is to
   give without dropping to two cards a row. KTEB's head needs 600px of content
   against 556px of room — two pod chips plus TAF MVFR is simply more than a
   third of this panel holds on one line — so .wx-head wraps as well. */
.wx-list > .wx-item { flex: 0 1 594px; min-width: 0; }
.wx-list > .wx-empty { flex: 1 1 100%; }
.tail-dot { width: 18px; height: 18px; border-radius: 50%; flex: none; }
.wx-item {
  border: 1px solid var(--hair);
  border-left: 5px solid var(--cc, var(--line));
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025) 60%);
  border-radius: 14px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  padding: 8px 20px 9px 18px;
  /* The grid gap spaces them now, and a card stretches to its row's height so
     two side by side end level instead of one trailing a step of empty border. */
  display: flex; flex-direction: column;
}
.wx-head { gap: 14px; }
.wx-tails {
  display: inline-flex; align-items: center; gap: 16px;
  margin-left: auto; padding-right: 4px;
}
.wx-tails span { display: inline-flex; align-items: center; gap: 8px; font-size: 22px; font-weight: 600; }
.wx-tails i { width: 13px; height: 13px; border-radius: 50%; display: inline-block; }
.wx-wind {
  font-family: var(--num); font-size: 22px; font-weight: 600;
  color: var(--warn); flex: none;
}
.taf-pill {
  font-size: 19px; font-weight: 700; letter-spacing: 0.04em;
  padding: 2px 12px; border-radius: 999px;
  border: 2px solid currentColor;
  flex: none; white-space: nowrap;
}
.wx-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; height: 100%; text-align: center;
}
.wx-empty strong { font-size: 30px; font-weight: 600; color: var(--good); }
.wx-empty span { font-size: 23px; color: var(--muted); }
/* WRAPS. The TAF pill used to hang 13px outside the card at KTEB, because
   .wx-name has flex:1 but keeps its intrinsic min-width and so refuses to give
   ground — everything to its right got pushed out of the box instead.
   Wrapping drops the pill onto its own line rather than outside the card, and
   costs a line of height only on the airports that actually need it. Nothing is
   truncated: an airport name and a category are both worth more than the row
   they sit on. */
.wx-head { display: flex; align-items: baseline; gap: 12px 14px; flex-wrap: wrap; }
/* Baseline alignment would hang the stack off its first pill's baseline and
   drop the second one below the card. The stack aligns to the top instead. */
.wx-head > .wx-cats { align-self: flex-start; }
.wx-icao { font-size: 30px; font-weight: 700; }
/* Shrinkable for real. flex:1 alone leaves min-width:auto in place, which is
   what let the name shove the pills through the wall of the card. */
.wx-name { font-size: 22px; color: var(--muted); flex: 1 1 auto; min-width: 0; }
/* The pills carry the answer the panel exists for; they never shrink and never
   get pushed. */
.wx-head > .wx-cats, .wx-head > .wx-tails { flex: none; }
/* NOW above FORECAST, hard against the right edge. Stacked rather than in the
   row because the row cannot hold them: see the width note on .wx-item. */
.wx-cats {
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
  margin-left: auto;
}
.wx-warn { font-size: 21px; color: var(--serious); margin-top: 8px; font-weight: 600; }

/* decoded conditions — icon + words, never colour alone */
/* Label beside chips, and the chips wrap among THEMSELVES. The row itself no
   longer wraps: it used to, which is how a chip that did not fit ended up
   starting a new line hard against the card's left edge, under the label
   instead of under the chip above it. align-items sits at the top so a
   two-line chip block keeps its label level with the first row. */
.wx-line { display: flex; align-items: flex-start; gap: 9px; margin-top: 5px; }
.wx-chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 9px;
  flex: 1 1 auto; min-width: 0;
}
.wx-when {
  font-size: 18px; font-weight: 700; letter-spacing: 0.12em;
  color: var(--muted); width: 56px; flex: none;
}
.wx-chip {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 21px; font-weight: 600;
  padding: 3px 13px 3px 10px; border-radius: 999px;
  border: 1.5px solid currentColor;
  white-space: nowrap;
}
.chip-i {
  width: 23px; height: 23px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.wx-chip.sev-3 { color: var(--crit);    background: rgba(208, 59, 59, 0.13); }
.wx-chip.sev-2 { color: var(--serious); background: rgba(236, 131, 90, 0.12); }
.wx-chip.sev-1 { color: var(--blue);    background: rgba(57, 135, 229, 0.11); }
.wx-clear { font-size: 22px; color: var(--good); }
/* The "ALSO KISP · KHPN" summary line is gone: every notable field has its own
   box now, and the panel scrolls rather than folding the tail of the list into
   one line of names. */

/* ---- ops quadrant: cards ---- */
/* Own full quadrant now — cards fill the height, so type runs larger */
.cards {
  display: grid; grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: minmax(0, 1fr);
  gap: 24px; padding-top: 22px;
  flex: 1; min-height: 0;
}
/* 5 cards (a tail parked off its last leg while the next one's checklist is
   open): narrower columns, drop the per-item timestamp so labels keep their
   space */
.cards.tight { grid-template-columns: repeat(5, 1fr); gap: 18px; }
.cards.tight .cl-time { display: none; }
.card {
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03) 55%);
  border: 1px solid var(--hair);
  border-radius: 18px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
  padding: 20px 22px;
  display: flex; flex-direction: column;
  min-width: 0; overflow: hidden;
}
.card-head { display: flex; align-items: center; gap: 12px; }
/* which leg of the day. Pushed to the far right of the head so it reads as a
   counter rather than as part of the registration next to it. */
.card-legno {
  margin-left: auto; flex: none;
  font-family: var(--num); font-size: 19px; font-weight: 600;
  letter-spacing: 0.06em; color: var(--muted);
  border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px;
}
.card-head .tail-dot { width: 16px; height: 16px; }
/* A door report with no trip behind it. Amber-edged rather than red: nothing
   has gone wrong with the aeroplane, the board simply does not know which trip
   this is and is asking. */
.up-card { border-color: rgba(250, 178, 25, 0.45); }
.up-card.up-done { border-color: var(--hair); }
.up-card.up-saving { opacity: 0.75; }
.up-card.up-failed { border-color: var(--crit); }
.up-flag {
  margin-left: auto; flex: none; font-family: var(--num); font-size: 17px;
  font-weight: 600; letter-spacing: 0.08em; color: var(--warn);
  border: 1px solid rgba(250, 178, 25, 0.5); border-radius: 6px; padding: 2px 8px;
}
.up-card.up-done .up-flag { color: var(--muted); border-color: var(--line); }
.up-said { font-size: 23px; color: var(--ink-2); margin-top: 8px; }
.up-form { display: flex; flex-wrap: wrap; gap: 10px 18px; margin-top: 12px; }
.up-form label, .up-note {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 20px; color: var(--muted);
}
.up-note { margin-top: 10px; width: 100%; }
.up-in {
  font-family: var(--num); font-size: 22px; color: var(--ink);
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line);
  border-radius: 6px; padding: 4px 8px; min-width: 0;
}
.up-in::placeholder { color: var(--muted); opacity: 0.7; }
.up-in:focus { outline: 2px solid var(--warn); outline-offset: 1px; }
.up-by { margin-top: 8px; font-size: 18px; color: var(--muted); }
/* View-only accounts read the board and change nothing on it — the same rule
   every other control here follows. */
[data-role="viewer"] .up-in { pointer-events: none; opacity: 0.55; }
[data-layout="mobile"] .up-said { font-size: 12px; }
[data-layout="mobile"] .up-in { font-size: 12px; padding: 2px 5px; }
[data-layout="mobile"] .up-form label, [data-layout="mobile"] .up-note { font-size: 11px; }
[data-layout="mobile"] .up-flag { font-size: 10px; padding: 1px 5px; }

/* Cabin internet, live from the terminal. Sits after the leg number, and when
   there is no leg number it takes the same auto margin so it still parks right.

   GREEN up, RED down, and nothing else. Two states, because this badge is read
   from across the room and a third colour makes somebody stop and work out
   which kind of not-green they are looking at.

   Each is a lifted version of the board's own token rather than
   the token itself. --good and --crit were chosen for 28px figures and solid
   fills; at 17px on near-black they go muddy, and this is the one badge whose
   entire job is to be unmistakable at a glance. Same reason the tints are here:
   a bare glyph reads as text, a tinted pill reads as a state. */
/* Own row, so the badge can never be squeezed out of the head and clipped. */
.card-slrow { margin-top: 6px; }
.card-slink {
  display: inline-block;
  font-family: var(--num); font-size: 17px; font-weight: 600;
  letter-spacing: 0.08em; border-radius: 6px; padding: 2px 8px;
}
.card-slink.is-up {
  color: #34c759; border: 1px solid rgba(52, 199, 89, 0.55); background: rgba(52, 199, 89, 0.10);
}
.card-slink.is-down {
  color: #ef4b45; border: 1px solid rgba(239, 75, 69, 0.6); background: rgba(239, 75, 69, 0.12);
}
/* Today's in-flight outage, beside the live badge. Deliberately quieter than
   the badge: the badge is the state now, this is the history behind it. */
.card-slout {
  display: inline-block; margin-left: 8px;
  font-family: var(--num); font-size: 15px; font-weight: 600; letter-spacing: 0.06em;
  color: var(--warn); border: 1px solid rgba(250, 178, 25, 0.45);
  border-radius: 6px; padding: 1px 6px;
}
[data-layout="mobile"] .card-slout { font-size: 9px; padding: 1px 4px; margin-left: 5px; }
[data-layout="mobile"] .card-slink { font-size: 10px; padding: 2px 6px; }
[data-layout="mobile"] .card-slrow { margin-top: 4px; }
.card-pod { font-size: 32px; font-weight: 700; }
.card-reg { font-size: 24px; font-family: var(--num); color: var(--ink-2); }
.card-route {
  font-size: 28px; font-family: var(--num); color: var(--ink-2);
  margin-top: 10px;
}
.card-route strong { color: var(--ink); font-weight: 600; }
.card-when { font-size: 24px; color: var(--muted); margin-top: 4px; }
/* a diverted leg: the route line reads as the NEW destination, so this says why */
.card-divert {
  margin-top: 6px; font-size: 21px; font-weight: 700; color: var(--crit);
  letter-spacing: 0.04em;
}
/* An emergency squawk. The loudest thing on the card, and it flashes for as
   long as the transponder is saying it — this one is not a notification you
   acknowledge and move past, it is a live state. */
.card-squawk {
  margin-top: 8px; padding: 8px 14px; border-radius: 8px;
  font-size: 24px; font-weight: 700; letter-spacing: 0.04em;
  color: #fff; background: var(--crit);
  animation: squawkFlash 1s steps(1, end) infinite;
}
@keyframes squawkFlash {
  0%, 49%   { background: var(--crit); color: #fff; }
  50%, 100% { background: #7d1414; color: #ffd7d7; }
}

/* the published schedule, behind the actuals — grey, and never the headline */
.card-plan {
  font-family: var(--num); font-size: 20px; color: var(--muted); opacity: 0.8;
  margin-top: 4px;
}
/* early / late against the plan. Sign carries meaning, colour carries urgency. */
.vs-plan { font-weight: 700; }
.vs-plan.d-late { color: var(--serious); }
.vs-plan.d-early { color: var(--good); }
.vs-plan.d-ok { color: var(--muted); }
.when-late { color: var(--serious); font-weight: 700; }
.card-sl {
  font-size: 21px; font-weight: 600; color: #6f9fd8;
  margin-top: 6px;
}
.cards-empty {
  grid-column: 1 / -1; align-self: center;
  text-align: center; font-size: 28px; color: var(--good);
}

/* ---- the day, once the day is over ----
   Fills the panel between the last landing and three hours before the next
   day's first trip. Spans every column: it is one statement about the whole
   fleet, not another card. */
.day-summary {
  grid-column: 1 / -1; align-self: center; justify-self: stretch;
  display: flex; flex-direction: column; gap: 22px;
  padding: 8px 10px;
}
.ds-head {
  font-size: 24px; font-weight: 700; letter-spacing: 0.16em;
  color: var(--muted); text-align: center;
}
/* Four across on the wall, and each figure large enough to read from the far
   side of the room — this is the panel's whole content at that hour. */
.ds-grid { display: flex; justify-content: space-around; gap: 20px; flex-wrap: wrap; }
.ds-stat { text-align: center; min-width: 0; }
.ds-val { font: 600 62px var(--num); color: var(--ink); line-height: 1.05; }
.ds-val.d-late { color: var(--serious); }
.ds-val.d-early, .ds-val.d-ok { color: var(--good); }
.ds-label {
  margin-top: 6px; font-size: 20px; letter-spacing: 0.08em;
  color: var(--muted); text-transform: uppercase;
}
/* Which ones were late, because a count of them is a question, not an answer. */
.ds-list { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.ds-row { display: flex; align-items: baseline; gap: 14px; font-size: 22px; }
.ds-pod { font-weight: 700; letter-spacing: 0.06em; }
.ds-leg { color: var(--ink-2); font-family: var(--num); }
.ds-late { color: var(--serious); font-weight: 700; font-family: var(--num); }
/* Whose delay it was. A passenger-induced one is still a delay — it keeps its
   place in the list and its minutes — but it is not the operation's, so the
   figure loses the alarm colour and takes the neutral one the manual-override
   tag already uses. */
.ds-why {
  font-size: 15px; font-weight: 700; letter-spacing: 0.1em;
  color: var(--violet); border: 1px solid var(--violet);
  border-radius: 5px; padding: 1px 6px;
}
.ds-late.d-pax { color: var(--violet); }
/* Repositioning: present in the day's list, and visibly not part of the score. */
.ds-why.ds-repo { color: var(--muted); border-color: var(--line); }
.ds-note { margin-top: 10px; font-size: 19px; color: var(--muted); text-align: center; }

/* The notification centre belongs to FLIGHT PROGRESS and to nothing else
   (operator's, 2026-08-26). It reports on the aircraft the cards above it
   describe; under a page of quarterly figures, or a list of work due next week,
   it is a third unrelated thing in a quadrant that already holds two.

   Hidden rather than emptied: every announcer keeps running, the banner still
   holds the screen for a sticky alert, and switching back to Flight Progress
   shows everything that arrived meanwhile. The face simply gets the room. */
#q-ops:not([data-view="progress"]) .notif-center { display: none; }

/* ---- Plan Ahead: the quadrant's third face ---- */
/* Anchored to the top of the panel, unlike the Statistics face it shares a
   class with. `.day-summary` centres itself vertically, which suits a block of
   figures floating in an empty quadrant and does not suit a page with a header
   row: the whole thing rode up and down as the body's height changed, so the
   MX/CLEANING slider moved every time it was used. */
.plan-page {
  grid-column: 1 / -1; align-self: stretch;
  display: flex; flex-direction: column; min-height: 0;
}
/* Filter | add, on the MX face only — the cleaning face renders no head at all
   rather than an empty one, so its cards start at the top of the panel.

   Still a grid rather than a flex row: two named cells pinned to their own
   ends cannot swap places or drift as their contents change width. */
.plan-head {
  display: grid; grid-template-columns: 1fr 1fr;
  align-items: center; gap: 14px; padding: 2px 2px 10px;
  /* The height of the add button, stated rather than inherited, so the row is
     the same height whichever aircraft the filter is naming. */
  min-height: 56px;
}
.plan-head-l { justify-self: start; }
.plan-head-r { justify-self: end; }
/* MX / CLEANING were a liquid-glass slider here until 2026-08-31, on the
   reasoning that both are Plan Ahead and promoting them would put four things
   in a row that reads as the panel's identity. Four reads fine, and the press
   they cost was one too many for a face used every day: they are quadrant tabs
   now (see .ops-switch, and the note in index.html). */

/* One aeroplane's three cleaning clocks. */
.cln-card { gap: 4px; }
.cln-row { margin-top: 12px; }
.cln-top { display: flex; align-items: baseline; gap: 10px; }
.cln-kind { font: 700 19px var(--sans); letter-spacing: 0.12em; color: var(--ink-2); }
/* Which clock the row runs on — 15D beside INTERIOR, 30D beside the other two —
   so three bars of different lengths read as three rules, not one rule drawn
   badly. */
.cln-every { font: 600 15px var(--num); letter-spacing: 0.08em; color: var(--muted); }
.cln-when { margin-left: auto; font: 600 22px var(--num); color: var(--ink); }
.cln-row.cl-soon .cln-when { color: var(--warn); }
.cln-row.cl-over .cln-when { color: var(--crit); }
/* How much of the clock is spent. Read across five cards it is fifteen fuel
   gauges, which is a shape rather than fifteen dates to subtract. */
.cln-bar { margin-top: 7px; height: 10px; border-radius: 5px; overflow: hidden;
  background: rgba(255, 255, 255, 0.08); }
.cln-bar span { display: block; height: 100%; background: var(--good); border-radius: 5px; }
.cln-row.cl-soon .cln-bar span { background: var(--warn); }
.cln-row.cl-over .cln-bar span { background: var(--crit); }
.cln-last { margin-top: 6px; font: 500 18px var(--num); color: var(--muted); }
/* A figure resting on the fleet's starting assumption rather than on a cleaning
   anybody performed. It says so, so nobody quotes it as an observation. */
.cln-seed { font-size: 15px; letter-spacing: 0.08em; border: 1px solid var(--line);
  border-radius: 5px; padding: 0 6px; }
[data-layout="mobile"] .cln-kind { font-size: 12px; }
[data-layout="mobile"] .cln-every { font-size: 11px; }
[data-layout="mobile"] .cln-when { font-size: 13px; }
[data-layout="mobile"] .cln-last { font-size: 11px; }
.plan-select {
  font: 600 17px var(--num); letter-spacing: 0.05em; color: var(--ink-2);
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line);
  border-radius: 8px; padding: 6px 10px; cursor: pointer;
}
.plan-plus {
  width: 44px; height: 44px; flex: none;
  font: 700 30px/1 var(--sans); color: var(--ink-2); cursor: pointer;
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line); border-radius: 10px;
}
@media (hover: hover) { .plan-plus:hover { color: var(--ink); border-color: var(--ink-2); } }
.plan-cards {
  flex: 1; min-height: 0; overflow-y: auto;
  /* A THIRD OF THE PANEL AND NO WIDER, whatever the card count (operator's,
     2026-08-24) — one card spanning the whole width read as a banner rather
     than as one of a set. Rows still stretch rather than hugging their
     content, so a lone card fills the height instead of floating in it. */
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
  align-content: stretch; grid-auto-rows: minmax(200px, 1fr);
  padding: 4px 2px;
}
/* Only spreads when the row is taller than the rows need — a card with two
   tasks in a short panel still stacks them at the top. */
.plan-card .cl-list { justify-content: space-around; }
.plan-card { position: relative; overflow: visible; }
.plan-grab { cursor: grab; touch-action: none; user-select: none; }
.plan-card.pl-drag { opacity: 0.85; z-index: 5; pointer-events: none; cursor: grabbing; }
/* Rows may wrap: "remove all snacks and drinks..." is a sentence, not a label,
   and an ellipsis would eat the instruction. */
.plan-card { padding: 24px 26px; }
/* The registration is what somebody crossing the room reads first. */
.plan-card .card-pod { font-size: 40px; }
.plan-card .card-reg { font-size: 31px; }
.plan-card .card-legno { font-size: 22px; padding: 3px 10px; }
.plan-card .card-when { font-size: 26px; margin-top: 6px; }
.plan-card .pl-x { font-size: 32px; }
.plan-card .cl-item { font-size: 26px; padding: 6px 0; gap: 12px; }
.plan-card .cl-item .cl-label { white-space: normal; line-height: 1.3; }
.plan-card .cl-box { width: 30px; height: 30px; font-size: 22px; border-radius: 7px; }
.pl-title { margin-top: 10px; font-size: 30px; font-weight: 600; color: var(--ink); }
.pl-x {
  flex: none; margin-left: 4px; padding: 0 2px; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--muted); background: none; border: 0;
}
@media (hover: hover) { .pl-x:hover { color: var(--ink); } }
.plan-card.pl-late { border-color: rgba(250, 178, 25, 0.5); }
.plan-card.pl-done { opacity: 0.75; }
.pl-addtask { margin-top: 8px; padding: 2px 0; font-size: 21px; color: var(--muted); cursor: pointer; }
@media (hover: hover) { .pl-addtask:hover { color: var(--ink-2); } }
.pl-newtask { margin-top: 8px; font-size: 20px; padding: 6px 10px; }
[data-role="viewer"] .pl-addtask { display: none; }
.pl-count {
  margin-top: auto; padding-top: 10px; font: 600 20px var(--num); color: var(--muted);
  display: flex; align-items: center; gap: 8px;
}
/* Struck-off rows, said once at the foot rather than left to be counted. */
.pl-void-n::before { content: "\00b7"; margin-right: 8px; }
/* The plan card's rows are taller than a flight checklist's, so its strike
   button is sized to match the text beside it.

   COLLAPSED until somebody reaches for it. An invisible button that still
   occupies its width is the worst of both: on a wall nobody hovers over, it
   silently took ninety pixels off every label and wrapped "Schedule Interior
   Cleaning" onto two lines to hold a control no one could see. A struck-off
   row keeps its RESTORE open, because that row is asking to be undone. */
.plan-card .cl-cancel { font-size: 17px; padding: 2px 10px; overflow: hidden; white-space: nowrap; }
@media (hover: hover) {
  .plan-card .cl-item:not(.cl-void) .cl-cancel {
    max-width: 0; padding-left: 0; padding-right: 0; border-width: 0; margin-right: 0;
  }
  .plan-card .cl-item:hover .cl-cancel, .plan-card .cl-cancel:focus-visible {
    max-width: 9em; padding: 2px 10px; border-width: 1px; margin-right: 2px;
  }
}
/* Who put the card there. Only ever a person — the sweep's own cards carry no
   initials, so the presence of one means somebody typed this in. */
.pl-by {
  margin-left: auto; flex: none; font: 600 17px var(--num); color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 7px;
}
.pl-who {
  flex: none; font: 600 17px var(--num); color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 7px;
}
/* View-only accounts read the panel and change nothing on it. */
[data-role="viewer"] .plan-plus, [data-role="viewer"] .pl-x { display: none; }

/* Crew Pairing is for ops and admin (operator's, 2026-09-09). The tool itself
   refuses anybody else — that is the control — but a link somebody cannot use
   should not be sitting in the switch inviting the press. Hidden for a viewer
   and for the two states that are answers rather than roles, so a person
   waiting on approval is not shown a door that is locked to them. */
[data-role="viewer"] .app-tab[href*="crew-pairing"],
[data-role="pending"] .app-tab[href*="crew-pairing"],
[data-role="blocked"] .app-tab[href*="crew-pairing"] { display: none; }
[data-role="viewer"] .plan-card .cl-item { pointer-events: none; }
[data-layout="mobile"] .plan-cards { grid-template-columns: 1fr; }
[data-layout="mobile"] .plan-card .cl-item { font-size: 13px; }
[data-layout="mobile"] .pl-title { font-size: 15px; }
[data-layout="mobile"] .plan-select { font-size: 12px; }
[data-layout="mobile"] .plan-plus { width: 32px; height: 32px; font-size: 22px; }
/* the + modal's form */
.plan-form { display: flex; flex-direction: column; gap: 12px; }
.plan-f {
  display: flex; flex-direction: column; gap: 5px;
  font: 600 12px var(--mono); letter-spacing: 0.14em; color: var(--muted);
}
.plan-in {
  font: 500 16px var(--sans); color: var(--ink);
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 10px;
}
.plan-in:focus { outline: 2px solid var(--warn); outline-offset: 1px; }

/* ---- the ops quadrant's two faces, and the period slider ---- */
/* The switch replaces the h1, so the active face wears the h1's exact voice
   and the idle one reads as a muted tab beside it. */
.ops-switch { display: flex; align-items: baseline; gap: 26px; }
.ops-mode {
  padding: 0; background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 29px; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--muted); opacity: 0.65;
}
.ops-mode.on { color: var(--ink-2); opacity: 1; cursor: default; }
@media (hover: hover) { .ops-mode:not(.on):hover { color: var(--ink-2); opacity: 0.85; } }

.st-tabs { display: flex; justify-content: center; gap: 10px; margin: 2px 0 16px; }
.st-tab {
  padding: 8px 16px; cursor: pointer;
  font: 600 14px/1 var(--mono); letter-spacing: 0.14em;
  color: var(--muted);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-edge); border-radius: 999px;
}
/* Pressing the period already selected returns the page to the present, so it
   is a control rather than a label. */
.st-tab.on { color: var(--blue); border-color: rgba(57, 135, 229, 0.55); }
@media (hover: hover) { .st-tab:not(.on):hover { color: var(--ink-2); border-color: var(--line); } }

/* The title row, with the arrows that walk it through history.
   The three sit together as one control rather than the arrows being pushed out
   to the panel edges: the title shrinks to its text and the arrows come with
   it, so the pair you press is beside the thing they change. */
/* An aeroplane past its own ETA. Coloured because it is the one number on a
   flight card that means somebody should go and look. */
.eta-over { color: var(--warn); font-weight: 600; }

.st-head { display: flex; align-items: center; justify-content: center; gap: 10px; }
.st-title { flex: 0 1 auto; min-width: 0; }
.st-nav {
  flex: 0 0 auto; width: 38px; height: 38px; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font: 400 26px/1 var(--sans); color: var(--ink-2);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-edge); border-radius: 999px;
}
@media (hover: hover) { .st-nav:not(.off):hover { color: var(--ink); border-color: var(--line); } }
/* Greyed rather than removed: an arrow that vanishes at the start of the record
   reads as a control the board lost, not as a limit it reached. */
.st-nav.off { opacity: 0.25; cursor: default; }
/* Back to the present. Only drawn while the page is NOT on the live period, so
   it is never an invitation to do nothing. */
.st-now {
  vertical-align: middle; margin-left: 10px; padding: 4px 9px 3px; cursor: pointer;
  font: 600 11px/1 var(--mono); letter-spacing: 0.12em;
  color: var(--blue); background: transparent;
  border: 1px solid rgba(57, 135, 229, 0.55); border-radius: 6px;
}
@media (hover: hover) { .st-now:hover { background: rgba(57, 135, 229, 0.12); } }
.st-retry {
  margin-left: 10px; padding: 4px 9px 3px; cursor: pointer;
  font: 600 11px/1 var(--mono); letter-spacing: 0.12em;
  color: var(--ink-2); background: transparent;
  border: 1px solid var(--line); border-radius: 6px;
}
@media (hover: hover) { .st-retry:hover { color: var(--ink); } }

/* A period still running is a part-figure and says so, right in its title. */
.st-live {
  display: inline-block; vertical-align: middle; margin-left: 10px; padding: 4px 8px 3px;
  font: 600 11px/1 var(--mono); letter-spacing: 0.12em;
  color: var(--warn); border: 1px solid rgba(250, 178, 25, 0.5); border-radius: 6px;
}

/* The Statistics face sits at the top of the quadrant rather than centring
   like the after-hours placard — it is a page somebody opened. */
.stats-page { align-self: start; }

/* Day done. One line, readable across the room — the figures are one tap away
   on Statistics, which the board switches to by itself. */
.all-done { align-items: center; justify-content: center; gap: 14px; text-align: center; }
.ad-tick {
  width: 76px; height: 76px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 42px; line-height: 1; color: var(--good);
  background: rgba(12, 163, 12, 0.10);
  border: 1px solid rgba(12, 163, 12, 0.45);
  box-shadow: 0 0 26px rgba(12, 163, 12, 0.16);
}
.ad-head {
  font-size: 30px; font-weight: 600; letter-spacing: 0.04em; color: var(--ink);
}
.all-done .ds-next { margin-top: 0; }
[data-layout="mobile"] .ad-tick { width: 46px; height: 46px; font-size: 24px; }
[data-layout="mobile"] .ad-head { font-size: 16px; }
[data-layout="mobile"] .all-done { gap: 9px; }

/* ---- the figures, as instruments ----
   Four glass tiles cut from the same material as the quadrants, each carrying
   its own visual: pod-coloured segments under the flight count, a route arc
   under the miles, a real dial for the on-time figure, and an activity strip
   underneath — so the panel reads at wall distance before anyone reads a
   number. */
/* Five across once the Starlink tile is in play, four when it is not — the
   tile is omitted entirely on a period with nothing airborne to measure, and a
   fixed five-column grid would leave a hole where it should have been. */
/* Laid out by COUNT, not by which tiles happen to be present: two of the six
   only appear once there is something to measure, and a `:has()` chain for each
   combination is a rule nobody can check. Six across leaves about 145px a tile
   on the wall, narrower than a four-digit mileage at 54px — so six go to two
   rows of three, where each gets 300px and the numbers stay legible. */
.stx-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.stx-grid.n5 { grid-template-columns: repeat(5, 1fr); }
.stx-grid.n6 { grid-template-columns: repeat(3, 1fr); }
.t-hr .stx-ico { color: var(--warn); }
.t-hr::before { background: radial-gradient(70% 60% at 20% 0%, rgba(250, 178, 25, 0.11), transparent 70%); }
.t-hr .ds-val { margin-bottom: auto; }
/* The denominator under a figure: quieter than the figure, same baseline grid. */
.stx-sub { font-size: 20px; color: var(--muted); margin-top: 2px; }
.t-sl.is-clean .stx-ico { color: var(--good); }
.stx-tile {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 14px 16px 16px;
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.055) 0%, rgba(255, 255, 255, 0.02) 55%, rgba(255, 255, 255, 0.04) 100%);
  border: 1px solid var(--glass-edge); border-radius: 16px;
  box-shadow: inset 0 1px 0 var(--glass-glint);
}
/* each tile's accent, as a soft light source behind its glass */
.stx-tile::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.t-fl::before  { background: radial-gradient(70% 60% at 20% 0%, rgba(57, 135, 229, 0.14), transparent 70%); }
.t-nm::before  { background: radial-gradient(70% 60% at 20% 0%, rgba(25, 158, 112, 0.13), transparent 70%); }
.t-avg::before { background: radial-gradient(70% 60% at 50% 0%, rgba(255, 255, 255, 0.05), transparent 70%); }
.t-dl.is-late::before  { background: radial-gradient(70% 60% at 20% 0%, rgba(208, 59, 59, 0.16), transparent 70%); }
.t-dl.is-clean::before { background: radial-gradient(70% 60% at 20% 0%, rgba(12, 163, 12, 0.10), transparent 70%); }
.stx-top {
  display: flex; align-items: center; gap: 8px; align-self: flex-start;
  font-size: 15px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted);
}
.stx-ico { width: 18px; height: 18px; flex: none; }
.t-fl  .stx-ico { color: var(--blue); }
.t-nm  .stx-ico { color: var(--aqua); }
.t-dl.is-late  .stx-ico { color: var(--crit); }
.t-dl.is-clean .stx-ico { color: var(--good); }
.stx-tile .ds-val { font-size: 54px; margin-top: auto; }
.t-dl .ds-val, .t-fl .ds-val, .t-nm .ds-val { margin-bottom: auto; }

/* who flew them: one segment per aircraft, in its own colour */
.stx-pods {
  display: flex; gap: 3px; width: 100%; height: 7px; margin-top: 2px;
}
.stx-pods span { border-radius: 4px; min-width: 7px; }

/* the miles tile's route arc, dashes crawling like the sign-in chart */
.stx-route { width: 100%; height: 26px; margin-top: 2px; }
.stx-arc {
  fill: none; stroke: rgba(25, 158, 112, 0.6);
  stroke-width: 1.6; stroke-dasharray: 5 6;
  animation: stx-crawl 30s linear infinite;
}
@keyframes stx-crawl { to { stroke-dashoffset: -220; } }
.stx-wpt { fill: rgba(25, 158, 112, 0.75); }

/* the on-time dial */
.stx-gauge { width: 120px; height: 70px; margin-top: auto; }
.stx-gauge path { fill: none; stroke-width: 7; stroke-linecap: round; }
.gb-early { stroke: rgba(57, 135, 229, 0.55); }
.gb-ok    { stroke: rgba(12, 163, 12, 0.75); }
.gb-warn  { stroke: rgba(250, 178, 25, 0.7); }
.gb-late  { stroke: rgba(208, 59, 59, 0.75); }
.stx-needle { stroke: var(--ink); stroke-width: 3; stroke-linecap: round; }
.stx-hub { fill: var(--ink-2); }
.stx-gauge-na .stx-needle, .stx-gauge-na .stx-hub { opacity: 0.3; }
.stx-avg { font: 600 26px var(--num); margin-bottom: auto; color: var(--ink); }
.stx-avg.d-late { color: var(--serious); }
.stx-avg.d-early, .stx-avg.d-ok { color: var(--good); }

/* the activity strip: the period's shape at a glance */
.stx-series { margin-top: 16px; }
.stx-bars {
  display: flex; align-items: flex-end; gap: 4px;
  height: 64px; padding: 0 2px;
  border-bottom: 1px solid var(--hair);
}
.stx-bar {
  flex: 1; height: var(--h);
  background: linear-gradient(180deg, rgba(57, 135, 229, 0.85), rgba(57, 135, 229, 0.35));
  border-radius: 3px 3px 0 0;
  transform-origin: bottom;
  animation: stx-grow 0.45s ease-out backwards;
}
/* a day with nothing keeps its slot — a gap in the week should look like one */
.stx-bar.nil { height: 3px; background: rgba(255, 255, 255, 0.08); border-radius: 2px; }
@keyframes stx-grow { from { transform: scaleY(0); } }
.stx-xlab { display: flex; gap: 4px; padding: 5px 2px 0; }
.stx-xlab span {
  flex: 1; text-align: center; overflow: hidden;
  font: 500 11px var(--mono); letter-spacing: 0.04em; color: var(--muted);
}

/* delay rows carry a proportional bar behind the text — the list is a chart.
   The list stretches its rows to one shared width and pins the minutes to the
   right edge, or the bar percentages would be measured against nothing. */
.ds-list { align-items: stretch; width: min(560px, 100%); margin: 0 auto; }
.ds-late { margin-left: auto; }
.ds-row { position: relative; z-index: 0; padding: 3px 10px; border-radius: 6px; overflow: hidden; }
.ds-fill {
  position: absolute; inset: 0 auto 0 0; z-index: -1;
  background: linear-gradient(90deg, rgba(208, 59, 59, 0.18), rgba(208, 59, 59, 0.04));
  border-left: 3px solid rgba(208, 59, 59, 0.6);
  border-radius: 6px 0 0 6px;
}
.ds-fill.d-pax {
  background: linear-gradient(90deg, rgba(144, 133, 233, 0.16), rgba(144, 133, 233, 0.04));
  border-left-color: rgba(144, 133, 233, 0.6);
}
/* early and on-time legs in the day's flight list */
.ds-fill.ok {
  background: linear-gradient(90deg, rgba(12, 163, 12, 0.14), rgba(12, 163, 12, 0.03));
  border-left-color: rgba(12, 163, 12, 0.55);
}
.fl-when { color: var(--muted); font: 500 16px var(--num); }
.fl-delta.d-ok { color: var(--good); }
.fl-delta.d-late { color: var(--serious); }
.fl-delta.d-pax { color: var(--violet); }
[data-layout="mobile"] .fl-when { font-size: 10px; }

@media (prefers-reduced-motion: reduce) {
  .stx-arc, .stx-bar { animation: none; }
}

[data-layout="mobile"] .stx-grid,
[data-layout="mobile"] .stx-grid.n5,
[data-layout="mobile"] .stx-grid.n6 { grid-template-columns: 1fr 1fr; gap: 8px; }
[data-layout="mobile"] .stx-sub { font-size: 11px; }
[data-layout="mobile"] .stx-tile { padding: 10px 12px 12px; border-radius: 12px; gap: 5px; }
[data-layout="mobile"] .stx-top { font-size: 10px; gap: 5px; }
[data-layout="mobile"] .stx-ico { width: 13px; height: 13px; }
[data-layout="mobile"] .stx-tile .ds-val { font-size: 28px; }
[data-layout="mobile"] .stx-gauge { width: 86px; height: 50px; }
[data-layout="mobile"] .stx-avg { font-size: 16px; }
[data-layout="mobile"] .stx-bars { height: 42px; gap: 2px; }
[data-layout="mobile"] .stx-xlab span { font-size: 8px; }
[data-layout="mobile"] .stx-series { margin-top: 10px; }
[data-layout="mobile"] .ds-row { padding: 2px 7px; }

/* Four tabs on a phone. Three fitted at 14px; the fourth does not, and the
   row would have run under the alarm button rather than wrapping.
   Three things buy the room, in the order that costs the reader least: the
   word FLIGHT goes (PROGRESS alone is unambiguous next to CLEANING and MX),
   MAINTENANCE shortens to MX — the tab's full name on the wall, the
   abbreviation everyone here uses on a phone — and the type comes down a
   point. Measured at 350px of the 360 the narrowest phone still in use gives,
   alarm button included. */
[data-layout="mobile"] .ops-mode { font-size: 13px; letter-spacing: 0.12em; }
[data-layout="mobile"] .ops-switch { gap: 12px; }
.ops-abbr { display: none; }
[data-layout="mobile"] .ops-word { display: none; }
[data-layout="mobile"] .ops-abbr { display: inline; }
[data-layout="mobile"] .st-tabs { gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
[data-layout="mobile"] .st-tab { font-size: 11px; padding: 6px 11px; }
[data-layout="mobile"] .st-live { font-size: 9px; padding: 3px 6px 2px; margin-left: 6px; }
[data-layout="mobile"] .st-head { gap: 6px; }
[data-layout="mobile"] .st-nav { width: 30px; height: 30px; font-size: 18px; }
[data-layout="mobile"] .st-now { font-size: 9px; padding: 3px 6px 2px; margin-left: 6px; }
/* Why the mileage carries a "+". Amber rather than muted grey: this is a gap in
   the board's own data, and it should read as something to fix rather than as
   another line of commentary. */
/* Delays and lost cabins, side by side. One column until there is actually a
   second list to show — a lone list stretched across half the panel reads as a
   layout that lost something. */
.ds-cols { display: grid; grid-template-columns: 1fr; gap: 18px; align-items: start; }
.ds-cols.has-sl { grid-template-columns: 1fr 1fr; }
[data-layout="mobile"] .ds-cols.has-sl { grid-template-columns: 1fr; gap: 10px; }
/* The second list needs a heading; the delay list does not, because the tile
   above it already says what it is. */
.ds-sub {
  font-family: var(--num); font-size: 20px; letter-spacing: 0.04em;
  color: var(--muted); margin-bottom: 4px;
}
[data-layout="mobile"] .ds-sub { font-size: 11px; }
/* The qualifier on a list heading: the scope of the list, not part of its name.
   Set back so "Starlink lost in flight" still reads as the heading. */
.ds-sub .ds-qual { opacity: 0.62; letter-spacing: 0.02em; }
/* Amber, matching the OUT badge on the aircraft card — the same fact in two
   places should not be two colours. Deliberately NOT the delay red beside it:
   these are different failures and the eye should be able to tell the columns
   apart without reading either. */
.ds-fill.sl-fill { background: rgba(250, 178, 25, 0.20); }
.ds-late.sl-late { color: var(--warn); }
/* The trip locator beside an outage row — the reference somebody carries over
   into Jet Insight. Same muted weight as the date next to it. */
.ds-trip {
  font-family: var(--num); font-size: 18px; color: var(--muted);
  margin-left: 8px; letter-spacing: 0.05em; white-space: nowrap;
}
[data-layout="mobile"] .ds-trip { font-size: 10px; margin-left: 5px; }
.ds-when {
  font-family: var(--num); font-size: 19px; color: var(--muted);
  margin-left: 8px; white-space: nowrap;
}
[data-layout="mobile"] .ds-when { font-size: 10px; margin-left: 5px; }
.ds-note.ds-gap { color: var(--warn); }
.ds-next { text-align: center; font-size: 21px; color: var(--muted); font-family: var(--num); }
/* The week / month / year, on the day that closes them. Ruled off and set
   smaller than the day's own figures: the day is what the room is standing
   there for, and these are the wider context behind it. */
.ds-periods {
  display: flex; flex-direction: column; gap: 10px;
  margin-top: 4px; padding-top: 16px; border-top: 1px solid var(--grid);
}
.ds-period { display: flex; align-items: baseline; gap: 20px; justify-content: center; }
.ds-plabel {
  flex: none; width: 150px; text-align: right;
  font-size: 19px; font-weight: 700; letter-spacing: 0.14em; color: var(--muted);
}
.ds-pstats { display: flex; gap: 26px; flex-wrap: wrap; font-size: 21px; color: var(--muted); }
.ds-pstats b { color: var(--ink); font-family: var(--num); font-weight: 600; margin-right: 5px; }
.ds-pstats b.d-late { color: var(--serious); }
.ds-pstats b.d-early, .ds-pstats b.d-ok { color: var(--good); }
[data-layout="mobile"] .ds-periods { gap: 6px; padding-top: 10px; }
[data-layout="mobile"] .ds-period { flex-direction: column; align-items: center; gap: 2px; }
[data-layout="mobile"] .ds-plabel { width: auto; text-align: center; font-size: 10px; }
[data-layout="mobile"] .ds-pstats { gap: 12px; font-size: 11px; justify-content: center; }

[data-layout="mobile"] .day-summary { gap: 12px; padding: 4px 2px; }
[data-layout="mobile"] .ds-head { font-size: 11px; letter-spacing: 0.14em; }
/* Two by two rather than four across — four columns on a phone leaves each
   figure about forty pixels. */
[data-layout="mobile"] .ds-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 8px; }
[data-layout="mobile"] .ds-val { font-size: 30px; }
[data-layout="mobile"] .ds-label { font-size: 10px; margin-top: 2px; }
[data-layout="mobile"] .ds-row { font-size: 12px; gap: 8px; }
[data-layout="mobile"] .ds-next { font-size: 11px; }

.edct-banner {
  margin-top: 14px;
  border-left: 5px solid var(--warn);
  background: rgba(250, 178, 25, 0.09);
  padding: 9px 14px;
  font-size: 23px; color: var(--warn); font-weight: 600;
  border-radius: 4px;
}
.edct-banner small { display: block; font-weight: 400; font-size: 19px; color: var(--ink-2); }

/* One fixed gap, not an even spread over whatever height is left.
   space-around made the spacing a function of how many rows a card happened to
   have: a three-row repo leg sat with more than twice the air between its rows
   as the five-row passenger leg beside it, and two cards on one screen read at
   two different rhythms. A constant gap costs a band of empty card under a short
   list, which is the cheaper thing to give up. */
.cl-list {
  margin-top: 16px; flex: 1; min-height: 0;
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: 6px;
}
.cl-item {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 0;
  font-size: 26px;   /* keeps "Catering on Board" off the ellipsis at 4 cards */
}
.cl-box {
  width: 30px; height: 30px; flex: none;
  border: 2px solid var(--line); border-radius: 7px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 22px; font-weight: 700; color: #0d0d0d;
}
.cl-item.done .cl-box { background: var(--good); border-color: var(--good); }
.cl-item .cl-label { flex: 1; color: var(--ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-item.done .cl-label { color: var(--ink); }
.cl-item.next .cl-label { color: var(--warn); font-weight: 600; }
/* click to override */
.cl-item { cursor: pointer; border-radius: 6px; padding-left: 6px; margin-left: -6px; }
@media (hover: hover) { .cl-item:hover { background: rgba(255, 255, 255, 0.06); } }
@media (hover: hover) { .cl-item:hover .cl-box { border-color: var(--ink-2); } }
/* a manual override announces itself — the MAN tag plus a violet edge */
/* Struck off as not applicable — catering cancelled. The line through the
   label is the whole point: the row is still there, so the record still shows
   catering was on this trip and somebody dropped it, but nothing about it reads
   as outstanding any more. */
.cl-item.cl-void .cl-label { text-decoration: line-through; color: var(--muted); font-weight: 400; }
.cl-item.cl-void .cl-box { border-color: var(--muted); color: var(--muted); background: none; }
.cl-item.cl-void .cl-src { color: var(--muted); border-color: var(--muted); }
/* Quiet until the row is worth acting on, and never louder than the label it
   sits beside. Appears on hover/focus at a desk and is always shown on touch,
   where there is no hover to reveal it with. */
.cl-cancel {
  flex: none; cursor: pointer;
  font: 700 15px var(--sans); letter-spacing: 0.1em;
  color: var(--muted); background: none;
  border: 1px solid var(--line); border-radius: 6px;
  padding: 2px 9px; margin-right: 2px;
  opacity: 0; transition: opacity 0.15s ease, color 0.15s ease;
}
.cl-item:hover .cl-cancel, .cl-cancel:focus-visible { opacity: 1; }
.cl-item.cl-void .cl-cancel { opacity: 1; }
@media (hover: none) { .cl-cancel { opacity: 1; } }
@media (hover: hover) { .cl-cancel:hover { color: var(--serious); border-color: var(--serious); } }
.cl-item.manual { box-shadow: inset 3px 0 0 var(--violet); }
.cl-item.manual .cl-src { color: var(--violet); border-color: var(--violet); }
.cl-item.manual.done .cl-box { background: var(--violet); border-color: var(--violet); }
.cl-src {
  font-family: var(--num); font-size: 19px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; flex: none;
}
/* Fixed columns so the stamps line up down the card. Widths are the measured
   worst case in Plex Sans plus a hair: "1730Z" is 56.6px here. */
.cl-time { font-family: var(--num); font-size: 19px; color: var(--muted); flex: none; width: 60px; text-align: right; }

/* A quick turn puts two legs on one card: the inbound flight's Pax Off on top,
   captioned with the flight it belongs to, then a rule, then the next
   departure's own rows. Without the caption the top box reads as part of the
   departure in the card header, which is a different aeroplane-load of people. */
.cl-from {
  font-family: var(--num); font-size: 18px; color: var(--muted);
  letter-spacing: 0.04em; padding-left: 6px;
}
.cl-split { height: 1px; background: var(--line); margin: 8px 0 4px; }
/* Pax Off sits between the landing time and the departure time, so it needs a
   rule on BOTH sides — it is its own band, not the end of the card. It is also
   the one thing on this card anybody can act on, so it gets the space. */
.cl-list.cl-tail {
  flex: none; margin: 14px 0 4px; padding: 12px 0 4px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
/* an override the server refused: it is on screen but it is not recorded */
.cl-item.cl-unsaved { box-shadow: inset 3px 0 0 var(--crit); }
.cl-item.cl-unsaved .cl-src { color: var(--crit); border-color: var(--crit); }

/* airborne card */
.card-air .air-status {
  display: flex; align-items: center; gap: 14px;
  margin-top: 20px;
  font-size: 38px; font-weight: 700; letter-spacing: 0.06em;
  color: var(--blue);
}
.pulse {
  width: 20px; height: 20px; border-radius: 50%; background: var(--blue);
  animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.air-eta { margin-top: 24px; }
.air-eta .eta-label { font-size: 23px; color: var(--muted); letter-spacing: 0.1em; }
.air-eta .eta-value { font-size: 76px; font-weight: 600; line-height: 1.1; }
.air-eta .eta-in { font-size: 28px; color: var(--ink-2); margin-top: 4px; }
/* `flex: none` is load-bearing. The card is a flex column with `overflow:
   hidden`, so when its content is taller than the card the browser shrinks
   whatever it may — and a 14px bar is the cheapest thing in the box. On the
   fullest card that took it to a computed height of ZERO: the meter was in the
   DOM, correctly coloured, with a correct fill width, and simply not there to
   look at. Adding one row to these cards was enough to trigger it, and it took
   out the single most-read element on the wall. */
.meter {
  flex: none;
  margin-top: 22px; height: 14px; border-radius: 7px;
  background: var(--meter-track); overflow: hidden;
}
.meter-fill { height: 100%; background: var(--blue); border-radius: 7px; }
.air-stats {
  display: flex; justify-content: space-between;
  margin-top: 14px;
  font-family: var(--num); font-size: 25px; color: var(--ink-2);
}
.air-off { font-size: 23px; color: var(--muted); margin-top: auto; padding-top: 12px; }

/* ---- the live tracking box: one card from the ramp to the ramp ----
   AFTER the .card-air rules above, deliberately. Both selectors are two
   classes, so on a tie the later one wins — put this block earlier and the
   status text stays blue while the dot beside it turns green, which is exactly
   what happened. Same trap as the banner pulse. */
.card-track .pulse { background: var(--tk); }
.card-track .air-status { color: var(--tk); }
.card-track .meter-fill { background: var(--tk); }
/* The sequence in colour: white on the ramp, amber once it moves, blue in the
   air, green down, grey done. Reading left to right across the panel tells you
   where every aeroplane is without reading a word of it. */
.track-door { --tk: var(--ink); }
.track-taxi { --tk: var(--warn); }
.track-airborne { --tk: var(--blue); }
.track-landed { --tk: var(--good); }
.track-parked { --tk: var(--ink-2); }
/* Parked is the end of the sequence, not a live state — it should not throb. */
.track-parked .pulse { animation: none; opacity: 0.7; }
/* Door closed is a state the aeroplane is HELD in, not one it is passing
   through, so the marker sits steady rather than pulsing at it. */
.track-door .pulse { animation: none; }
/* When the phase started. Every one of these has a source behind it now — the
   crew's message, or the first ADS-B fix over the taxi threshold — so it is set
   in foreground ink rather than hedged. */
.track-obs { color: var(--ink-2); }
/* Measured off ADS-B rather than derived from the schedule — brighter, because
   the difference between the two matters to whoever is reading it. */
.air-stats.ac-live span { color: var(--ink); font-variant-numeric: tabular-nums; }
.ac-src {
  font: 700 17px var(--sans); letter-spacing: 0.14em; color: var(--muted);
  margin-top: 2px; display: flex; gap: 12px;
}
/* A fix that has gone quiet says how quiet. Silently stale is the failure this
   whole feed exists to end. */
.ac-age { color: var(--warn); letter-spacing: 0.04em; }
/* Long enough that the aeroplane is not on this feed any more. Amber, not red:
   the board still has FlightAware's ETA and the schedule, so this is a degraded
   card rather than a lost aeroplane — but it must not read as a normal one. */
.ac-lost { color: var(--warn); }
.ac-lost-for { color: var(--ink); letter-spacing: 0.04em; }

/* The dormant "checklist opens in 9h" card is gone — the panel only shows what
   is happening or starts within 3 hours. Its styles went with it. */

/* ---- notification center (bottom of Flight Progress) ---- */
/* Sizes to whatever it is holding — an empty or half-full centre hands the
   space back to the checklist cards above it. */
.notif-center {
  flex: none;
  display: flex; flex-direction: column;
  border-top: 1px solid var(--grid);
  margin-top: 20px; padding-top: 14px;
}
.nc-head { display: flex; align-items: center; justify-content: space-between; padding-bottom: 8px; }
.nc-head h2 {
  font-size: 22px; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted);
}
/* Quiet until wanted. Clearing the board is a housekeeping action, not one of
   the things this panel is for, so it does not compete with the rows below it. */
.nc-clear {
  font: 700 18px/1 var(--sans); letter-spacing: 0.14em;
  color: var(--muted); background: none;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 7px 16px; cursor: pointer;
}
@media (hover: hover) { .nc-clear:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-2); } }
/* Nothing clearable — which happens when the panel is empty, and also when the
   only thing left is an alert waiting to be acknowledged. */
.nc-clear:disabled { opacity: 0.3; cursor: default; }
/* Glyph only — sized to be read from across the room, not from the keyboard. */
/* A real control, not a read-out. It was a `<span>` with `cursor: default` and
   no handler, which is exactly what it looked like to anyone who tried to press
   it. Sized as a proper hit target: this gets tapped on a phone and clicked
   from across a desk. */
.nc-audio {
  font-size: 30px; line-height: 1; color: var(--muted);
  cursor: pointer; background: none; border: 0; padding: 4px 6px;
  border-radius: 8px; margin: -4px -6px;
}
.nc-audio.off { color: var(--serious); }
@media (hover: hover) { .nc-audio:hover { background: rgba(255, 255, 255, 0.10); } }
.nc-audio:active { transform: scale(0.92); }
.nc-audio:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
/* FIVE ROWS, then it scrolls (user-specified). The centre now holds two hours
   of traffic, so on a busy afternoon it would otherwise grow until it pushed
   the flight-progress cards off the quadrant.
   Every row is exactly --nc-row tall — .nc-msg is nowrap with an ellipsis, so
   none of them can grow a second line — which is what lets a max-height cut
   cleanly at the fifth rather than halfway through one. */
.nc-rows {
  flex: none;
  --nc-row: 48px;
  max-height: calc(var(--nc-row) * 5);
  overflow-y: auto;
  overscroll-behavior: contain;          /* the wall behind it must not scroll */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
.nc-rows::-webkit-scrollbar { width: 8px; }
.nc-rows::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.nc-rows::-webkit-scrollbar-track { background: transparent; }

.nc-row {
  display: flex; align-items: center; gap: 16px;
  padding: 9px 0 9px 14px;
  border-left: 4px solid var(--tone);
  border-bottom: 1px solid var(--grid);
  font-size: 24px;
  box-sizing: border-box; height: var(--nc-row);
}
.nc-row.fresh { animation: rowIn 0.7s cubic-bezier(.2,.9,.25,1) both; }
/* A row that has to be ANSWERED rather than swept away — anything that is not
   reporting good news. Marked by a faint wash of its own tone rather than by
   motion: the flashing below is kept for the four that hold the whole screen,
   and a panel of strobing rows is one people stop reading. The row already
   carries the louder signal, which is that its cross has been replaced by an
   ACKNOWLEDGE button. */
.nc-row.nc-held { background: color-mix(in srgb, var(--tone) 7%, transparent); }
/* A DIVERSION does not settle down into the record like the others — it keeps
   flashing while it is on the board, because it is a situation somebody is
   still working, not a thing that happened. */
.nc-row.nc-flash {
  /* Flashes AROUND the row, not just inside it: an outline ring plus the fill,
     so it is visible even where the row is clipped by a neighbouring panel. */
  border-radius: 8px;
  animation: ncFlash 1.1s ease-in-out infinite;
}
.nc-row.nc-flash.fresh { animation: rowIn 0.7s cubic-bezier(.2,.9,.25,1) both, ncFlash 1.1s ease-in-out 0.7s infinite; }
@keyframes ncFlash {
  0%, 100% { background: color-mix(in srgb, var(--tone) 6%, transparent);
             box-shadow: 0 0 0 2px color-mix(in srgb, var(--tone) 40%, transparent); }
  50%      { background: color-mix(in srgb, var(--tone) 30%, transparent);
             box-shadow: 0 0 0 5px color-mix(in srgb, var(--tone) 85%, transparent); }
}
/* Answered. It keeps its place in the record and stops shouting: no flashing
   ring, and the tone bar down the left goes quiet. Deliberately still legible —
   "this was dealt with, by someone, at a time" is the useful part. */
.nc-row.nc-acked { opacity: 0.72; }
.nc-row.nc-acked .nc-label { color: var(--muted); }
.nc-by {
  flex: none; font-family: var(--num); font-size: 17px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 7px; padding: 5px 12px;
  white-space: nowrap;
}
[data-layout="mobile"] .nc-by { font-size: 10px; padding: 2px 7px; }

/* ACK, not a clear cross: the cross means "I do not need to see this". */
.nc-ack {
  flex: none; cursor: pointer;
  font-family: var(--sans); font-size: 17px; font-weight: 700; letter-spacing: 0.1em;
  color: #0d0d0d; background: var(--tone); border: 0;
  border-radius: 7px; padding: 7px 14px;
}
@media (hover: hover) { .nc-ack:hover { filter: brightness(1.15); } }
@keyframes rowIn {
  0%   { opacity: 0; transform: translateY(-16px) scale(0.97); background: rgba(255,255,255,0.10); }
  60%  { opacity: 1; background: rgba(255,255,255,0.06); }
  100% { opacity: 1; transform: none; background: transparent; }
}
.nc-time { font-family: var(--num); font-size: 21px; color: var(--muted); flex: none; width: 66px; }
.nc-pod { font-weight: 700; flex: none; width: 78px; }
.nc-label { font-weight: 600; color: var(--tone); flex: none; }
.nc-msg {
  color: var(--ink-2); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nc-src {
  font-family: var(--num); font-size: 18px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 8px; flex: none;
}
/* Sized for "23h 59m ago" (112.7px), not for "12m ago". At 96px anything older
   than an hour spilled its column — invisible today only because the crew feed
   is not connected and the centre is empty. */
.nc-age { font-family: var(--num); font-size: 19px; color: var(--muted); flex: none; width: 116px; text-align: right; }
.nc-empty { padding: 6px 0 2px; font-size: 22px; color: var(--muted); }

/* dismiss buttons — big hit targets, this is driven by a mouse from across a desk */
.nc-x, .banner-x {
  flex: none; cursor: pointer;
  background: none; border: none;
  color: var(--muted); font-family: var(--sans);
  line-height: 1; padding: 0;
}
.nc-x {
  width: 40px; height: 40px; font-size: 32px;
  border-radius: 8px;
}
@media (hover: hover) { .nc-x:hover { color: var(--ink); background: rgba(255, 255, 255, 0.10); } }
.banner-x {
  width: 52px; height: 52px; font-size: 44px; margin-left: 20px;
  border-radius: 10px; color: var(--ink-2);
}
@media (hover: hover) { .banner-x:hover { color: var(--ink); background: rgba(255, 255, 255, 0.12); } }

/* ---- centre-screen alert banner ---- */
.banner {
  position: absolute; inset: 0; z-index: 20;
  display: grid; place-items: center;
  pointer-events: none;
  visibility: hidden;
}
.banner.show { visibility: visible; }
.banner-card {
  width: 76%;
  background: rgba(15, 21, 35, 0.68);
  backdrop-filter: blur(34px) saturate(1.55);
  -webkit-backdrop-filter: blur(34px) saturate(1.55);
  border: 2px solid var(--tone);
  border-left: 14px solid var(--tone);
  border-radius: 22px;
  padding: 30px 40px 26px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 30px 80px rgba(0, 0, 0, 0.75);
}
.banner.show .banner-card { animation: bannerIn 0.45s cubic-bezier(.2,.9,.25,1.25) both; }
.banner.leaving .banner-card { animation: bannerOut 0.7s cubic-bezier(.55,0,.85,.35) both; }
@keyframes bannerIn {
  from { opacity: 0; transform: scale(0.84) translateY(-26px); }
  to   { opacity: 1; transform: none; }
}
/* travels down to the notification center as it shrinks away */
@keyframes bannerOut {
  0%   { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(330px) scale(0.26); }
}
/* `.show` is REQUIRED in these two selectors. `.banner.show .banner-card` above
   is three classes; `.banner-crit .banner-card` is two, so it lost the cascade
   and the critical pulse has never once rendered — the rule was there, matched,
   and silently outranked. Specificity bugs do not throw. */
.banner.show.banner-crit .banner-card { animation: bannerIn .45s cubic-bezier(.2,.9,.25,1.25) both, critPulse 1s ease-in-out 0.45s infinite; }
@keyframes critPulse {
  0%, 100% { box-shadow: 0 30px 80px rgba(0,0,0,.75), 0 0 0 0 rgba(208,59,59,.55); }
  50%      { box-shadow: 0 30px 80px rgba(0,0,0,.75), 0 0 0 22px rgba(208,59,59,0); }
}
/* Sticky: holds the screen until ACKNOWLEDGE, and flashes AROUND the card the
   whole time — an expanding ring plus a border that alternates, so it reads as
   unanswered rather than as one more thing that appeared and settled. Tone-
   driven so an EDCT flashes orange and a diversion red. */
.banner.show.banner-sticky .banner-card {
  animation: bannerIn .45s cubic-bezier(.2,.9,.25,1.25) both, stickyPulse 1.1s ease-in-out .45s infinite;
}
@keyframes stickyPulse {
  0%, 100% { box-shadow: 0 30px 80px rgba(0,0,0,.75),
                         0 0 0 0 color-mix(in srgb, var(--tone) 70%, transparent);
             border-color: var(--tone); }
  50%      { box-shadow: 0 30px 80px rgba(0,0,0,.75),
                         0 0 0 26px color-mix(in srgb, var(--tone) 0%, transparent);
             border-color: #fff; }
}
/* no countdown on something that is not counting down */
.banner-sticky .banner-bar { display: none; }
.banner-ack {
  display: block; width: 100%; margin-top: 22px; cursor: pointer;
  font-family: var(--sans); font-size: 30px; font-weight: 700; letter-spacing: 0.14em;
  color: #0d0d0d; background: var(--tone); border: 0;
  border-radius: 12px; padding: 16px 0;
}
@media (hover: hover) { .banner-ack:hover { filter: brightness(1.15); } }
.banner-ack[hidden] { display: none; }
.banner-top { display: flex; align-items: center; gap: 16px; }
.banner-dot { width: 20px; height: 20px; border-radius: 50%; background: var(--pod); flex: none; }
.banner-pod { font-size: 34px; font-weight: 700; }
.banner-reg { font-size: 27px; font-family: var(--num); color: var(--ink-2); }
.banner-time { margin-left: auto; font-family: var(--num); font-size: 27px; color: var(--muted); }
.banner-card { pointer-events: auto; }   /* the X must be clickable */
.banner-type {
  font-size: 62px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--tone); line-height: 1.1; margin-top: 12px;
}
.banner-msg { font-size: 32px; color: var(--ink); margin-top: 8px; }
.banner-meta {
  display: flex; align-items: center; gap: 20px;
  margin-top: 20px; font-size: 21px; color: var(--muted);
  font-family: var(--num);
}
.banner-bar {
  flex: 1; height: 5px; border-radius: 3px; background: var(--line);
  transform-origin: left center;
}
.banner.show .banner-bar { animation: barDrain 10s linear both; background: var(--tone); }
@keyframes barDrain { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* ---- TFR / flow ---- */
/* STACKED, not side by side (user-specified). Two half-width columns wasted the
   panel twice over: the TFR side is usually one row and sat beside a column of
   five, so half the width was empty and the rows that were there had to wrap in
   a column narrower than the text wanted. Full width, one above the other, each
   scrolling on its own — the same treatment the weather list already had. */
.tfr-flow {
  flex: 0 1 auto; min-height: 0;
  display: flex; flex-direction: column; gap: 18px;
  padding-top: 14px;
}
/* `0 1 auto`: content-sized while it fits, and shrinking in proportion to how
   much each has when it does not — so a long flow list gives way before a
   two-row TFR list does, instead of both being pinned to half the panel. */
.tfr-col, .flow-col { flex: 0 1 auto; min-height: 0; }
.col-title {
  font-size: 20px; font-weight: 700; letter-spacing: 0.14em;
  color: var(--muted); padding-bottom: 6px;
  /* Held at the top of its own scroller. Side by side it did not matter — you
     could always see which column you were in — but stacked, scrolling the top
     list took its heading away and left an unlabelled block of rows above a
     labelled one. The backing is opaque enough that rows pass behind it rather
     than through it. */
  position: sticky; top: 0; z-index: 1;
  background: rgba(8, 12, 20, 0.94);
}
/* The second title needs air above it now that it follows a list rather than
   sitting at the top of its own column. */
.flow-col .col-title { padding-top: 4px; }
/* Tiled, same as the weather boxes. A ruled list down a full-width panel put one
   short entry per line and left the rest of the row empty; two boxes to a row
   fit twice as many in the same height and give each one an edge of its own. */
/* Same wrapping flex as the weather boxes, and for the same reason: the number
   of entries is whatever the FAA sends, so no fixed column count can avoid
   leaving a hole. Cards on a short row grow to close it. */
.tfr-grid {
  display: flex; flex-wrap: wrap; align-content: flex-start;
  /* TOP, not stretch. A two-chip GDP box was being pulled to the height of the
     four-chip closure beside it and carrying the difference as blank card. */
  align-items: flex-start;
  gap: 10px; padding-bottom: 10px;
}
.tfr-grid > .tfr-row { flex: 0 1 520px; min-width: 0; }
/* COLUMN. The head line carries the badge, the airport and the time; the facts
   get the whole width of the card underneath. As one horizontal strip the time
   was an unshrinkable column that squeezed the chips beside it into a shred. */
.tfr-row {
  display: flex; flex-direction: column; gap: 4px;
  padding: 9px 16px 10px 14px;
  border: 1px solid var(--hair); border-radius: 12px;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025) 60%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  font-size: 24px;
}
.tfr-kind {
  font-size: 19px; font-weight: 700; letter-spacing: 0.08em;
  padding: 3px 12px; border-radius: 6px; flex: none;
}
.k-vip     { color: var(--warn);    background: rgba(250,178,25,.12); }
.k-fire    { color: var(--serious); background: rgba(236,131,90,.12); }
.k-stadium { color: var(--muted);   background: rgba(255,255,255,.06); }
.k-gdp     { color: var(--warn);    background: rgba(250,178,25,.12); }
.k-edct    { color: var(--warn);    background: rgba(250,178,25,.12); }
.k-ok      { color: var(--good);    background: rgba(12,163,12,.10); }
/* WRAPS. Three items on one unbreakable line meant that when they did not fit,
   every one of them was shrunk instead — a title cut mid-word and a time cut
   mid-date, both to keep a layout nobody asked for. Flex breaks lines before it
   shrinks anything, so the time now drops to a line of its own (still right, on
   the `margin-left: auto`) and the title gets the whole width back. */
.tfr-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; min-width: 0; }
.tfr-title { font-size: 25px; font-weight: 600; color: var(--ink); min-width: 0; }
/* The altitude band a TFR runs between. One fact, so it never breaks inside
   itself, whatever width the card ends up with. */
.tfr-alt { white-space: nowrap; }
.tfr-main { flex: 1; color: var(--ink-2); min-width: 0; }
.tfr-main strong { color: var(--ink); font-weight: 600; }
/* Pushed right, and allowed to WRAP rather than hold a fixed column open — it
   no longer has to, since the head drops it to a line of its own before it gets
   squeezed, but the cap stays as the guard for anything unexpectedly long. */
.tfr-time {
  font-family: var(--num); font-size: 21px; color: var(--muted);
  margin-left: auto; text-align: right; max-width: 52%;
}
.tfr-note { font-size: 21px; color: var(--muted); min-width: 0; overflow-wrap: break-word; }
/* The facts inside a flow entry, one chip each — the same treatment the weather
   panel gives a METAR, so a run-on line of FAA capitals reads as a short list
   of things that are true. */
.fl-bullets { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 7px; }
.fl-chip {
  font-size: 19px; font-weight: 600; line-height: 1.25;
  padding: 3px 11px; border-radius: 999px;
  border: 1.5px solid currentColor; color: var(--muted);
  /* Now that a chip has the full card to lay out in, normal wrapping keeps
     short ones on one line and lets a long one break at a SPACE instead of
     overflowing the border — which is what `nowrap` was doing to "ground
     stop/delay program possible". */
  white-space: normal; max-width: 100%; overflow-wrap: break-word;
}
.fl-chip.sev-1 { color: var(--ink-2); }
.fl-chip.sev-2 { color: var(--blue);    background: rgba(57, 135, 229, 0.11); }
.fl-chip.sev-3 { color: var(--serious); background: rgba(236, 131, 90, 0.12); }
/* A restriction whose limits sit close enough to ours that the parser declined
   to rule on it. Louder than the chips it sits above, on purpose. */
.fl-check {
  margin-left: 12px; font-size: 18px; font-weight: 700; letter-spacing: 0.08em;
  color: var(--warn); text-transform: uppercase;
}
.tfr-none { padding: 9px 0; font-size: 23px; color: var(--good); }

/* map svg classes */
/* Base map strokes and route lines keep their width when zoomed */
.gratic { stroke: rgba(255, 255, 255, 0.04); stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; }
.land { fill: #121927; stroke: rgba(160, 180, 215, 0.22); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.states { fill: none; stroke: rgba(255, 255, 255, 0.09); stroke-width: 1; vector-effect: non-scaling-stroke; }

/* zoom & pan */
.map-wrap { cursor: grab; }
.map-wrap.grabbing { cursor: grabbing; }
.map-ctl {
  /* Below the floating application switch, which spans the seam and reaches
     about 200px into this quadrant's top-left corner. A control sitting under
     another control is a misclick waiting to happen. */
  position: absolute; left: 30px; top: 104px; z-index: 3;
  display: flex; flex-direction: column; gap: 10px;
}
.map-btn {
  width: 58px; height: 58px;
  font-size: 34px; font-family: var(--sans); line-height: 1;
  color: var(--ink-2); background: rgba(10, 15, 26, 0.48);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  border: 1px solid var(--glass-edge); border-radius: 14px;
  box-shadow: inset 0 1px 0 var(--glass-glint);
  cursor: pointer;
}
@media (hover: hover) { .map-btn:hover { color: var(--ink); background: rgba(48, 60, 88, 0.55); } }
#map-reset { color: var(--muted); }
#map-reset.on { color: var(--warn); border-color: rgba(250, 178, 25, 0.45); }
.apt-dot { fill: var(--muted); }
.apt-label { fill: var(--ink-2); font: 600 23px var(--sans); }
/* diversion: the aircraft and the field it is now heading for both ping */
.map-ping { fill: none; stroke: var(--crit); stroke-width: 3; vector-effect: non-scaling-stroke; }
.apt-label.apt-divert { fill: var(--crit); font-weight: 700; }
.apt-divert-tag { fill: var(--crit); font: 700 16px var(--sans); letter-spacing: 0.12em; }
.apt-cat { fill: #0d0d0d; font: 700 18px var(--sans); letter-spacing: 0.04em; }
.apt-lbl { transition: opacity 0.18s ease; }
.lbl-off .apt-lbl { display: none; }
.route-active { fill: none; stroke-width: 3.5; vector-effect: non-scaling-stroke; }
.route-later { fill: none; stroke-width: 2.5; opacity: 0.35; vector-effect: non-scaling-stroke; }
.plane-label { font-size: 24px; font-weight: 700; fill: var(--ink); font-family: var(--sans); }
.plane-sub { font-size: 19px; fill: var(--ink-2); font-family: var(--num); }
.plane-mx { font-size: 19px; font-weight: 700; fill: var(--serious); }

/* Live-data badge. Deliberately not the demo amber: on a wall you must be able
   to tell at a glance whether you are looking at real trips. */
.chip-live {
  color: #7fd6a8;
  border-color: rgba(127, 214, 168, 0.34);
  background: rgba(127, 214, 168, 0.10);
  display: inline-flex; align-items: baseline; gap: 9px;
}
.chip-live small {
  font-family: var(--num);
  font-size: 0.74em; letter-spacing: 0.02em;
  color: var(--muted); font-weight: 400;
}

/* Flow rows that touch an airport we are actually using. Everything else in the
   NAS is context; this is the part that changes what dispatch does today. */
.tfr-row.row-mine { background: rgba(217, 89, 38, 0.09); box-shadow: inset 2px 0 0 var(--orange); }
/* The forward-looking row, and the only one on this panel about something that
   has not happened yet — so it is the one still worth acting on. Taller and
   tinted rather than left as the quietest thing on a panel of red badges. */
.tfr-row.row-plan {
  background: rgba(159, 180, 216, 0.10);
  box-shadow: inset 4px 0 0 #9fb4d8;
  border-color: rgba(159, 180, 216, 0.28);
  /* Emphasis without a height floor. The floor was the wasted space: two chips
     in a 150px box is a tall empty rectangle, and the tint and the accent were
     already doing the work of making it stand out. */
  padding: 12px 16px 13px 14px;
}
.tfr-row.row-plan .tfr-kind { font-size: 22px; padding: 6px 16px; }
.tfr-row.row-plan .fl-chip { font-size: 23px; padding: 5px 15px; }
.tfr-row.row-plan .tfr-time { font-size: 26px; color: #9fb4d8; font-weight: 700; }
[data-layout="mobile"] .tfr-row.row-plan { min-height: 0; padding-top: 8px; padding-bottom: 8px; }
[data-layout="mobile"] .tfr-row.row-plan .tfr-kind,
[data-layout="mobile"] .tfr-row.row-plan .fl-chip { font-size: 11px; }
[data-layout="mobile"] .tfr-row.row-plan .tfr-time { font-size: 11px; }
.k-gs     { color: #f0a0a0; border-color: rgba(240, 160, 160, 0.34); background: rgba(208, 59, 59, 0.14); }
.k-closed { color: #f0a0a0; border-color: rgba(240, 160, 160, 0.28); background: rgba(208, 59, 59, 0.10); }
.k-plan   { color: #9fb4d8; border-color: rgba(159, 180, 216, 0.28); background: rgba(159, 180, 216, 0.09); }
.k-space  { color: #c3a6e8; border-color: rgba(195, 166, 232, 0.30); background: rgba(195, 166, 232, 0.10); }

/* "+N more" — the panel is capped to fit, and says so rather than truncating
   silently. A wall that hides rows without admitting it teaches people to
   distrust it. */
.tfr-more {
  font-family: var(--num); font-size: 17px; color: var(--muted);
  padding: 7px 4px 2px; letter-spacing: 0.02em;
}

/* ---- trip details, opened from a bar on the schedule ---- */
.bar-hit { fill: transparent; cursor: pointer; }
/* Anchored inside the schedule quadrant, against the bar it describes. Left and
   top are written by placeTrip; the quadrant's own `overflow: hidden` is the
   backstop if a clamp is ever off by a pixel. */
.trip-pop {
  position: absolute; z-index: 40;
  width: 460px; max-width: calc(100% - 24px);
  max-height: calc(100% - 24px); overflow-y: auto;
  padding: 20px 24px 22px;
  border-radius: 20px;
  /* Glass, like the quadrants themselves: a translucent ground with the board
     blurred and brightened through it, one hairline edge and a specular top.
     Deliberately lighter than the panel underneath so it reads as a layer
     ABOVE the schedule rather than a hole cut into it.
     The ground is at 0.88 rather than the 0.62 it started at: enough of the
     schedule still shows through the blur to place the card over its own trip,
     but the type is read against a near-solid panel instead of against
     whatever bars happen to be behind it. */
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.075) 0%,
    rgba(255, 255, 255, 0.035) 45%,
    rgba(255, 255, 255, 0.055) 100%), rgba(12, 17, 27, 0.88);
  backdrop-filter: blur(30px) saturate(1.5);
  -webkit-backdrop-filter: blur(30px) saturate(1.5);
  border: 1px solid var(--glass-edge);
  box-shadow:
    inset 0 1px 0 var(--glass-glint),
    inset 0 -1px 0 rgba(255, 255, 255, 0.04),
    0 26px 70px rgba(0, 0, 0, 0.55);
}
.trip-pop[hidden] { display: none; }
.trip-x {
  position: absolute; top: 12px; right: 14px;
  width: 44px; height: 44px; font: 400 34px/1 var(--sans);
  color: var(--ink-2); background: none; border: none; border-radius: 10px; cursor: pointer;
}
@media (hover: hover) { .trip-x:hover { color: var(--ink); background: rgba(255, 255, 255, 0.10); } }
.tr-head { display: flex; align-items: baseline; gap: 12px; padding-right: 44px; }
.tr-dot { width: 14px; height: 14px; border-radius: 50%; align-self: center; flex: none; }
.tr-pod { font: 700 30px var(--sans); color: var(--ink); letter-spacing: 0.04em; }
.tr-tail { font: 22px var(--num); color: var(--ink-2); }
.tr-cat { margin-left: auto; font: 700 15px var(--sans); letter-spacing: 0.12em; }
/* The crew-change card wears the same steel its block on the rail does, so the
   two are obviously the same thing. */
.tr-cat.cat-swap { color: #8f9bb3; }
.tr-cat.cat-clean { color: #6fb0d4; }
/* One line per seat that changed, and nothing else on the card. The outgoing
   name is dimmed and the incoming one is not, so which way it reads is
   obvious before the arrow is. */
.tr-swaps { display: flex; flex-direction: column; gap: 10px; margin-top: 18px; }
.tr-swap { display: flex; align-items: baseline; gap: 12px; font: 600 30px var(--sans); }
.tr-swap .tr-was { color: var(--muted); }
.tr-swap .tr-to { color: #8f9bb3; font-size: 26px; }
.tr-swap .tr-now { color: var(--ink); }
.tr-swap .tr-seat { margin-left: auto; }
[data-layout="mobile"] .tr-swaps { gap: 6px; margin-top: 10px; }
[data-layout="mobile"] .tr-swap { font-size: 15px; gap: 8px; }
.tr-route { font: 600 34px var(--sans); color: var(--ink); margin: 14px 0 18px; }
.tr-route span { color: var(--muted); }
.tr-rows { display: flex; flex-direction: column; gap: 1px; }
.tr-row {
  display: flex; align-items: baseline; gap: 16px;
  padding: 9px 0; border-top: 1px solid var(--grid);
}
.tr-k { flex: none; width: 132px; font-size: 17px; letter-spacing: 0.1em; color: var(--muted); text-transform: uppercase; }
.tr-v { flex: 1; font: 22px var(--num); color: var(--ink); min-width: 0; }
.tr-v.d-late { color: var(--serious); }
.tr-seat { font: 700 14px var(--sans); color: var(--muted); letter-spacing: 0.1em; }
/* One pilot per line; a full name is too wide to share one. */
.tr-crew { display: block; }
.tr-crew + .tr-crew { margin-top: 3px; }
.tr-note { color: var(--muted); font-size: 18px; }
/* The one field somebody copies into another system, so it is set apart. */
.tr-loc { font-weight: 600; letter-spacing: 0.06em; color: var(--blue); }
/* On a phone the quadrant is only as wide as the screen, so the card takes
   what is there rather than a fixed 460. */
[data-layout="mobile"] .trip-pop { width: auto; left: 10px !important; right: 10px;
  padding: 14px 14px 16px; border-radius: 14px; }
[data-layout="mobile"] .trip-x { width: 36px; height: 36px; font-size: 26px; top: 8px; right: 8px; }
[data-layout="mobile"] .tr-pod { font-size: 18px; }
[data-layout="mobile"] .tr-tail { font-size: 13px; }
[data-layout="mobile"] .tr-cat { font-size: 10px; }
[data-layout="mobile"] .tr-route { font-size: 20px; margin: 10px 0 12px; }
[data-layout="mobile"] .tr-k { width: 86px; font-size: 10px; }
[data-layout="mobile"] .tr-v { font-size: 13px; }
[data-layout="mobile"] .tr-note { font-size: 11px; }
[data-layout="mobile"] .tr-row { padding: 7px 0; gap: 10px; }

/* Feed has stopped answering. Silence must not look like "nothing changed". */
.chip-stale {
  color: #e8b45a;
  border-color: rgba(232, 180, 90, 0.38);
  background: rgba(232, 180, 90, 0.12);
  display: inline-flex; align-items: baseline; gap: 9px;
}
.chip-stale small { font-family: var(--num); font-size: 0.74em; color: var(--muted); font-weight: 400; }

/* Current-conditions icons beside each airport on the map. Colour carries
   severity; the ICAO stays the primary label. */
.apt-wx { overflow: visible; }
.apt-wx use { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

/* A pilot over 10 hours in 24 is a legality problem, not a delay — it gets the
   loudest treatment on the board and sits above the flow programmes. */
/* Crew flight-time cards were removed from the board on 2026-08-18 at the
   operator's request, so nothing currently renders with these classes. Kept
   rather than deleted: the data still arrives on every schedule poll, so
   restoring the panel is putting one block back in renderTfr — and it would
   come back unstyled if these went with it. */
.tfr-row.row-crew { box-shadow: inset 3px 0 0 var(--crit); }
.tfr-row.row-crew .tfr-time { font-weight: 700; letter-spacing: 0.06em; }
/* over 10 h in 24: red. Over 9 h: amber — an hour of headroom is a different
   problem from a breach, and painting them the same colour would either make
   the breach look routine or the warning look like one. */
.tfr-row.crew-over { background: rgba(208, 59, 59, 0.14); box-shadow: inset 3px 0 0 var(--crit); }
.tfr-row.crew-over .tfr-time { color: var(--crit); }
.tfr-row.crew-warn { background: rgba(250, 178, 25, 0.12); box-shadow: inset 3px 0 0 var(--warn); }
.tfr-row.crew-warn .tfr-time { color: var(--warn); }
.tfr-row.crew-warn .k-crew {
  color: #f5d79a; border-color: rgba(250, 178, 25, 0.45); background: rgba(250, 178, 25, 0.20);
}
.k-crew { color: #f2b0b0; border-color: rgba(242, 176, 176, 0.45); background: rgba(208, 59, 59, 0.22); }

/* ============================================================================
   MOBILE
   A 2x2 wall of 55-inch TVs and a phone in someone's hand are not the same
   product. Below 1100px the fixed 3840x2160 stage is abandoned entirely: the
   quadrants become a scrolling single column, reordered map-first, and the
   type is re-specified at phone sizes. Scaling the wall down instead would
   render its 24px labels at about 5px.
   Order is user-specified: map, schedule, flight progress, weather.
   ========================================================================== */
/* NOTE the selector: data-layout sits ON <html>, so `[data-layout] html` would
   be looking for an <html> inside itself and never match — which is exactly how
   `overflow:hidden` survived here and killed scrolling on the phone. */
html[data-layout="mobile"] {
  overflow-x: hidden; overflow-y: auto;
  height: auto; min-height: 100%;
  -webkit-text-size-adjust: 100%;
}
html[data-layout="mobile"] body {
  overflow: visible; height: auto; min-height: 100%;
  overscroll-behavior-y: none;
}

[data-layout="mobile"] #fit {
  position: static; width: 100%; height: auto; transform: none !important;
}
[data-layout="mobile"] #stage {
  display: flex; flex-direction: column;
  width: 100%; height: auto;
  gap: 10px; padding: 10px;
  animation: none;                 /* burn-in drift is a TV problem, not a phone one */
}
/* The phone stacks the panels down the page, so the pinned switch needs the
   page pushed clear of it rather than a layout of its own. */
[data-layout="mobile"] #stage { padding-top: 44px; }
[data-layout="mobile"] .ambient { display: none; }

[data-layout="mobile"] .quad {
  position: relative; height: auto; min-height: 0;
  border-radius: 16px; padding: 0 14px 14px;
  /* Backdrop blur on four stacked full-width panels is a real cost on a phone
     GPU and buys nothing when there is no wall glow behind them. */
  backdrop-filter: none; -webkit-backdrop-filter: none;
  background: rgba(255, 255, 255, 0.045);
  box-shadow: inset 0 1px 0 var(--glass-glint), 0 8px 24px rgba(0, 0, 0, 0.4);
}
[data-layout="mobile"] #q-map   { order: 1; }
[data-layout="mobile"] #q-sched { order: 2; }
[data-layout="mobile"] #q-ops   { order: 3; }
[data-layout="mobile"] #q-wx    { order: 4; }

[data-layout="mobile"] .qhead { padding: 12px 0 9px; }
[data-layout="mobile"] .qhead h1 { font-size: 14px; letter-spacing: 0.14em; }
[data-layout="mobile"] .qhead-sub { padding-top: 12px; }
[data-layout="mobile"] .chip { font-size: 10px; padding: 3px 9px; gap: 5px; border-width: 1px; }
[data-layout="mobile"] .chip-live small, [data-layout="mobile"] .chip-stale small { font-size: 0.82em; }
[data-layout="mobile"] .sched-meta { display: flex; align-items: center; gap: 8px; }

/* ---- map: first, and given real height ---- */
[data-layout="mobile"] .quad-map { height: 58vh; min-height: 320px; max-height: 520px; padding: 0; }
/* One finger scrolls the PAGE; the map takes two fingers (or the buttons).
   The map is the first panel and fills most of the first screen, so if it ate
   one-finger drags there would be nowhere left to start a scroll. */
[data-layout="mobile"] .map-wrap { position: absolute; inset: 0; touch-action: pan-y; }
[data-layout="mobile"] .map-hint {
  /* Right-aligned on the same row as the zoom buttons, which sit bottom-left.
     Centred, it landed straight on top of the reset button. */
  position: absolute; right: 12px; bottom: 48px;
  z-index: 3; pointer-events: none;
  font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); background: rgba(5, 7, 12, 0.62);
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--hair);
  white-space: nowrap;
}
.map-hint { display: none; }
[data-layout="mobile"] .map-hint { display: block; }
[data-layout="mobile"] .quad-map .qhead { padding: 10px 14px 0; }
[data-layout="mobile"] .clocks { gap: 10px; }
[data-layout="mobile"] .ck-label { font-size: 8px; letter-spacing: 0.1em; }
[data-layout="mobile"] .ck-time { font-size: 14px; }
[data-layout="mobile"] .ck-utc .ck-time { font-size: 16px; }
[data-layout="mobile"] .map-legend { display: none; }   /* the icons carry it */
/* The label and time carry their own px sizes, so shrinking .ticker alone
   left "LATEST 2112Z" at wall scale with the message small beside it. */
[data-layout="mobile"] .ticker { font-size: 10px; padding: 5px 10px; gap: 8px; }
[data-layout="mobile"] .ticker .tk-label { font-size: 9px; }
[data-layout="mobile"] .ticker .tk-metar { font-size: 10px; }
[data-layout="mobile"] .ticker .tk-item { gap: 6px; }
[data-layout="mobile"] .ticker .tk-sep { margin: 0 12px; }
[data-layout="mobile"] .ticker .tk-arrow { margin: 0 5px; }
/* Controls move to the bottom on a phone: at the top they sit under the clocks,
   and the bottom is where a thumb already is. */
[data-layout="mobile"] .map-ctl { flex-direction: row; gap: 8px; left: 10px; top: auto; bottom: 44px; }
[data-layout="mobile"] .map-btn { width: 38px; height: 38px; font-size: 20px; }

/* Headers wrap rather than colliding with their own metadata. */
[data-layout="mobile"] .qhead { flex-wrap: wrap; row-gap: 6px; align-items: baseline; }
[data-layout="mobile"] .sched-meta { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
[data-layout="mobile"] .dateline { font-size: 12px; letter-spacing: 0.08em; white-space: nowrap; }
[data-layout="mobile"] .chip { max-width: 60%; overflow: hidden; text-overflow: ellipsis; }

/* ---- schedule: scrolls sideways rather than shrinking to illegibility ---- */
[data-layout="mobile"] #q-sched { height: auto; }
[data-layout="mobile"] .sched-wrap {
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  /* Hitting either end of the schedule must stop there, not chain into the
     browser's own back/forward swipe. */
  overscroll-behavior-x: contain;
  padding-top: 4px;
}
/* NATIVE scrolling owns the finger here. The wall's `pan-y` (above) reserves
   horizontal movement for the JS drag — which on a phone meant the browser
   cancelled the gesture at the first vertical wobble, and what drags survived
   ran a full SVG rebuild per pointermove. `pan-x pan-y` hands both axes to
   the scroller this wrap already is: sideways scrolls the schedule with the
   platform's own momentum, upward scrolls the page, and the browser makes
   the horizontal-or-vertical call. The JS drag stands down on mobile — see
   the pointerdown guard in app.js. */
[data-layout="mobile"] #schedsvg { width: 980px; height: 500px; touch-action: pan-x pan-y; }
/* The tail cards, stuck to the wrap's left edge by the browser's own sticky
   positioning — perfectly stationary while the timeline scrolls beneath. The
   counter-translated <g> this replaces chased the scroll from events, one
   frame behind the compositor, and visibly stuttered. In flow after the
   schedule svg and pulled back over it by the negative margin; the width is
   150 viewBox units at the schedule's own scale (980/1810), so the two svgs
   stay in registration. */
[data-layout="mobile"] #gutsvg {
  display: block;
  position: sticky; left: 0; z-index: 2;
  width: 81.2px; height: 500px;
  margin-top: -500px;
  pointer-events: none;      /* the timeline under it is the scroller */
}
/* Its card gradient lives in its own defs (see app.js) — a fill referencing a
   gradient defined in a DIFFERENT inline svg is the kind of cross-svg lookup
   Safari is flaky about. The id-prefixed selector outweighs the .gut-card rule. */
[data-layout="mobile"] #gutsvg .gut-card { fill: url(#gut-card-g); }
/* "no crew assigned" at the wall's 19px is wider than the narrow card. */
[data-layout="mobile"] .g-nocrew { font-size: 15px; }
/* "Matus | Barber" on one line inside a 130-unit card: it fits at 15, not at
   the wall's 21. */
[data-layout="mobile"] .g-crew { font-size: 15px; }

/* ---- flight progress ---- */
/* A DECK, not a list (user-specified). One card across the screen, flicked
   left and right; the browser's scroll-snap does the paging, so it carries the
   platform's own momentum and rubber-banding and there is no animation code to
   go wrong. Stacked down the page these were an endless scroll — four aircraft
   deep before you reached the weather. */
[data-layout="mobile"] .cards {
  display: flex; grid-template-columns: none;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* Hitting the last card stops there rather than chaining into the browser's
     back-swipe, and the page still scrolls vertically through the deck. */
  overscroll-behavior-x: contain; touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
  gap: 10px; padding-top: 10px;
  scrollbar-width: none;
}
[data-layout="mobile"] .cards::-webkit-scrollbar { display: none; }
/* 91% wide and centre-snapped, so the neighbouring cards show a sliver at each
   edge: that peek is what says "there is another one" without a word of
   instruction. `snap-stop: always` makes a hard flick advance ONE card rather
   than skidding past three. */
[data-layout="mobile"] .cards > .card {
  flex: 0 0 91%; scroll-snap-align: center; scroll-snap-stop: always;
}
/* Nothing to page through — these take the full width and no snap. */
[data-layout="mobile"] .cards > .cards-empty,
[data-layout="mobile"] .cards > .day-summary { flex: 0 0 100%; }
[data-layout="mobile"] .cards.tight { grid-template-columns: none; }
[data-layout="mobile"] .cards.tight .cl-time { display: block; }

/* Which card of how many, in each aircraft's own colour. */
.card-dots { display: none; }
[data-layout="mobile"] .card-dots {
  display: flex; justify-content: center; align-items: center;
  gap: 4px; padding: 9px 0 2px;
}
[data-layout="mobile"] .card-dots:empty { display: none; }
.card-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--dot, var(--ink)); opacity: 0.3;
  /* The dot is 7px; the tappable box around it is not. */
  box-sizing: content-box; padding: 8px 5px; background-clip: content-box;
  cursor: pointer;
  transition: width 0.18s ease, opacity 0.18s ease;
}
.card-dot.on { width: 20px; border-radius: 4px; opacity: 1; }
[data-layout="mobile"] .card { padding: 12px 14px; border-radius: 12px; gap: 8px; }
[data-layout="mobile"] .card-pod { font-size: 17px; }
[data-layout="mobile"] .card-reg { font-size: 13px; }
[data-layout="mobile"] .card-route { font-size: 16px; }
[data-layout="mobile"] .card-when { font-size: 12px; }
[data-layout="mobile"] .card-divert { font-size: 12px; }
[data-layout="mobile"] .card-plan { font-size: 11px; }
/* Fixed rather than absolute here: the phone scrolls the whole document, and a
   sync button that scrolls off the top is one you have to scroll back up to
   reach — which is the gesture that already syncs. */
[data-layout="mobile"] .top-ctl { position: fixed; top: 10px; right: 10px; z-index: 55; gap: 6px; }
[data-layout="mobile"] .sync-btn {
  font-size: 11px; letter-spacing: 0.12em; gap: 6px; padding: 7px 13px;
}
[data-layout="mobile"] .fs-btn { padding: 6px; }
[data-layout="mobile"] .fs-ico { width: 14px; height: 14px; }
[data-layout="mobile"] .sync-ico { font-size: 13px; }
[data-layout="mobile"] .nc-clear { font-size: 10px; padding: 5px 10px; }
[data-layout="mobile"] .cl-list { margin-top: 10px; gap: 3px; }
[data-layout="mobile"] .cl-item { font-size: 13px; gap: 8px; padding: 3px 0; }
[data-layout="mobile"] .cl-box { width: 17px; height: 17px; }
[data-layout="mobile"] .cl-src { font-size: 9px; padding: 1px 5px; }
[data-layout="mobile"] .cl-cancel { font-size: 9px; padding: 2px 6px; letter-spacing: 0.06em; }
[data-layout="mobile"] .cl-time { font-size: 11px; width: 36px; }
[data-layout="mobile"] .cl-from { font-size: 10px; padding-left: 0; }
[data-layout="mobile"] .cl-split { margin: 4px 0 2px; }
/* The airborne/tracking card, complete. This list drifted: the card was
   rebuilt around .eta-value/.eta-in/.air-stats and the old mobile override
   went on pointing at .eta-big, which no longer exists — so the phone got the
   wall's 76px ETA and a card that filled its whole screen. Every class the
   card renders is re-specified here; when the card changes again, this block
   has to change with it. */
[data-layout="mobile"] .card-air .air-status { font-size: 14px; gap: 8px; margin-top: 8px; }
[data-layout="mobile"] .pulse { width: 10px; height: 10px; }
[data-layout="mobile"] .air-eta { margin-top: 8px; }
[data-layout="mobile"] .air-eta .eta-label { font-size: 10px; }
[data-layout="mobile"] .air-eta .eta-value { font-size: 30px; }
[data-layout="mobile"] .air-eta .eta-in { font-size: 13px; margin-top: 2px; }
[data-layout="mobile"] .meter { margin-top: 10px; height: 7px; border-radius: 4px; }
[data-layout="mobile"] .air-stats { margin-top: 8px; font-size: 12px; }
[data-layout="mobile"] .ac-src { font-size: 9px; gap: 8px; letter-spacing: 0.1em; }
[data-layout="mobile"] .air-off { font-size: 11px; padding-top: 8px; }
[data-layout="mobile"] .card-legno { font-size: 10px; padding: 2px 8px; }
[data-layout="mobile"] .cards-empty { font-size: 13px; padding: 16px 0; }
[data-layout="mobile"] .notif-center { margin-top: 10px; }
[data-layout="mobile"] .nc-head h2 { font-size: 11px; }
/* Same five-row cut, at the row height the phone actually renders. */
/* ---- the notification centre on a phone ----
   The wall lays a row out as one line of fixed columns, which on a 390px screen
   ran off the side: the message was clipped to an ellipsis and the time and
   source sat somewhere off-screen to the right, reachable only by scrolling the
   panel sideways (user-reported). A notification you have to drag to read is
   one nobody reads.
   So the row becomes two lines that fit the width instead of one that does not:
     line 1   POD · LABEL ............... SRC  TIME
     line 2   the message, wrapping as far as it needs
   Nothing is dropped and nothing is truncated, and the two things you scan a
   list by — when it happened and where it came from — are pinned to the right
   edge where they line up down the column. */
[data-layout="mobile"] .nc-rows {
  --nc-row: 30px;
  /* Rows are no longer a fixed height, so the five-row cut cannot be counted in
     row heights any more. Roughly five two-line rows, then it scrolls. */
  max-height: 320px;
}
[data-layout="mobile"] .nc-row {
  font-size: 12px; padding: 7px 8px;
  display: grid; height: auto;
  grid-template-columns: auto 1fr auto auto;
  grid-template-areas: "pod label src age" "msg msg msg msg";
  align-items: center;
  column-gap: 8px; row-gap: 3px;
}
[data-layout="mobile"] .nc-time { display: none; }   /* the age column says it better */
[data-layout="mobile"] .nc-pod { grid-area: pod; width: auto; }
[data-layout="mobile"] .nc-label { grid-area: label; min-width: 0; }
[data-layout="mobile"] .nc-src { grid-area: src; font-size: 11px; }
[data-layout="mobile"] .nc-age {
  grid-area: age; width: auto; font-size: 11px;
  justify-self: end; text-align: right; white-space: nowrap;
}
/* The whole point: it wraps instead of being cut off. */
[data-layout="mobile"] .nc-msg {
  grid-area: msg;
  white-space: normal; overflow: visible; text-overflow: clip;
  line-height: 1.35;
}
/* The dismiss/ack control rides on the message line's right, so it never
   squeezes the time and source out of alignment. */
[data-layout="mobile"] .nc-x,
[data-layout="mobile"] .nc-ack,
[data-layout="mobile"] .nc-by { grid-area: msg; justify-self: end; align-self: center; }
[data-layout="mobile"] .nc-msg { padding-right: 34px; }
[data-layout="mobile"] .nc-empty { font-size: 12px; padding: 4px 0 2px; }
/* Comfortably tappable on a phone — the glyph is small but the target is not. */
[data-layout="mobile"] .nc-audio { font-size: 18px; padding: 8px 10px; margin: -8px -10px; }
[data-layout="mobile"] .nc-x { width: 28px; height: 28px; font-size: 20px; }

/* ---- weather + TFR/flow ---- */
/* One column on a phone: 390px cannot hold two of these side by side, and the
   page scrolls as a whole rather than the panel scrolling inside itself. */
/* One per row here — 390px fits nothing else — so the basis goes to 100%
   rather than the wall's 520px, which flex would otherwise treat as a minimum
   and overflow the screen with. */
[data-layout="mobile"] .wx-list { overflow: visible; gap: 8px; }
[data-layout="mobile"] .wx-list > .wx-item { flex-basis: 100%; }
[data-layout="mobile"] .wx-item { padding: 8px 10px; border-radius: 10px; gap: 6px; }
[data-layout="mobile"] .wx-icao { font-size: 15px; }
[data-layout="mobile"] .wx-name { font-size: 11px; }
[data-layout="mobile"] .wx-chip { font-size: 11px; padding: 2px 7px; }
[data-layout="mobile"] .wx-chip .chip-i { width: 13px; height: 13px; }
/* The rest of a weather card's type, same scale as the chips beside it. */
[data-layout="mobile"] .wx-when { font-size: 9px; width: 34px; letter-spacing: 0.08em; }
[data-layout="mobile"] .wx-clear { font-size: 12px; }
[data-layout="mobile"] .wx-warn { font-size: 11px; margin-top: 4px; }
[data-layout="mobile"] .wx-wind { font-size: 11px; }
[data-layout="mobile"] .wx-head { gap: 8px; }
[data-layout="mobile"] .wx-tails { gap: 8px; }
[data-layout="mobile"] .wx-tails span { font-size: 11px; gap: 4px; }
[data-layout="mobile"] .wx-tails i { width: 8px; height: 8px; }
[data-layout="mobile"] .tail-dot { width: 10px; height: 10px; }
[data-layout="mobile"] .wx-empty strong { font-size: 14px; }
[data-layout="mobile"] .wx-empty span { font-size: 11px; }
[data-layout="mobile"] .cat-pill { font-size: 10px; padding: 2px 7px; }
[data-layout="mobile"] .taf-pill { font-size: 10px; padding: 2px 7px; }
[data-layout="mobile"] .cat-key { display: none; }
/* Already one column here; the phone scrolls the whole page rather than each
   panel, so this just needs its spacing. */
[data-layout="mobile"] .tfr-flow { gap: 10px; }
/* NOT sticky here. The phone scrolls the document rather than each panel, so
   the nearest scrolling ancestor is the page — the title would peel off and
   float over the whole board, straight into the fixed SYNC button. */
[data-layout="mobile"] .col-title { font-size: 10px; position: static; background: none; }
/* One box per row on a phone, and the padding has to come back — the wall's
   `padding: 6px 0` was written for a ruled line with no border to sit inside. */
[data-layout="mobile"] .tfr-grid { gap: 6px; padding-bottom: 6px; }
[data-layout="mobile"] .tfr-grid > .tfr-row { flex-basis: 100%; }
[data-layout="mobile"] .tfr-row { font-size: 12px; padding: 6px 10px 7px 8px; gap: 8px; border-radius: 9px; }
[data-layout="mobile"] .tfr-kind { font-size: 9px; padding: 2px 6px; }
[data-layout="mobile"] .tfr-note { font-size: 10px; }
[data-layout="mobile"] .tfr-time { font-size: 10px; }
[data-layout="mobile"] .tfr-title { font-size: 13px; }
/* The generic chip. Only the row-plan variant was ever given a phone size, so
   every other card's facts stayed at the wall's 19px. */
[data-layout="mobile"] .fl-chip { font-size: 10px; padding: 2px 7px; }
[data-layout="mobile"] .fl-bullets { gap: 5px; margin-top: 4px; }
[data-layout="mobile"] .fl-check { font-size: 9px; margin-left: 8px; }
[data-layout="mobile"] .tfr-more, [data-layout="mobile"] .tfr-none { font-size: 11px; }

/* ---- centre-screen alert: the viewport, not a quadrant ---- */
[data-layout="mobile"] .banner { position: fixed; inset: 0; z-index: 60; }
[data-layout="mobile"] .banner-card { width: min(92vw, 420px); padding: 16px 18px; border-radius: 16px; }
[data-layout="mobile"] .banner-pod { font-size: 20px; }
[data-layout="mobile"] .banner-type { font-size: 22px; }
[data-layout="mobile"] .banner-msg { font-size: 15px; }
[data-layout="mobile"] .banner-reg { font-size: 14px; }
[data-layout="mobile"] .banner-time { font-size: 14px; }
[data-layout="mobile"] .banner-dot { width: 12px; height: 12px; }
[data-layout="mobile"] .banner-x { width: 36px; height: 36px; font-size: 26px; margin-left: 10px; }
[data-layout="mobile"] .banner-meta { font-size: 12px; gap: 12px; margin-top: 12px; }
[data-layout="mobile"] .banner-ack { font-size: 16px; padding: 12px 0; margin-top: 14px; }

/* Touch targets: a checklist row is tappable, so give it a thumb's worth. */
@media (pointer: coarse) {
  [data-layout="mobile"] .cl-item { padding-top: 7px; padding-bottom: 7px; }
  [data-layout="mobile"] .map-btn { width: 44px; height: 44px; }
}

/* IP satisfied by a short turn rather than a crew report. Marked distinctly so
   nobody reads an inference as a report — hovering the row states the turn. */
.cl-src.src-turn { color: var(--blue); border-color: rgba(57, 135, 229, 0.45); }

/* No TAF group covers our window — a real state, distinct from a category. */
.taf-pill.taf-none { color: var(--muted); border-color: var(--line); }

/* Airframe utilisation: airborne time in the rolling 24 hours. Neutral until it
   approaches the threshold, so the colour means something when it appears. */
/* The .ab24 airframe-utilisation badge was removed from the tail cards. */

/* Leg category on the schedule. Colour AND word — colour alone is never a
   label, the same rule the flight categories follow. */
.bar-cat { font: 700 15px var(--sans); letter-spacing: 0.1em; }
.leg-key { display: flex; gap: 14px; align-items: center; }
.leg-key span { display: inline-flex; align-items: center; gap: 6px;
  font: 700 15px var(--sans); letter-spacing: 0.1em; color: var(--muted); }
.leg-key i { width: 14px; height: 14px; border-radius: 4px; display: inline-block; }
[data-layout="mobile"] .leg-key { display: none; }
/* Rail controls on a phone: the buttons and the date matter, the range readout
   and the drag hint do not survive the width. */
/* The phone keeps the header as a WRAPPING FLEX row rather than the wall's
   three-column grid: 390px cannot hold a title, a date, three buttons and a
   status chip side by side, and forcing it to try is how the title ends up two
   characters wide. Title and range on one line, the controls on the next, the
   chip on its own. */
/* The range readout gets its own line UNDER the title. Pushed right, it sat
   in the top-right corner of the viewport — the exact patch the fixed SYNC
   and fullscreen buttons float over, so the one line that says how far back
   and forward the board reaches was the one line the buttons covered. */
[data-layout="mobile"] .sched-title { width: 100%; flex-wrap: wrap; justify-content: flex-start; gap: 2px 10px; }
[data-layout="mobile"] .rail-centre { width: 100%; }
[data-layout="mobile"] .rail-btn { font-size: 12px; padding: 4px 8px; min-width: 28px; }
/* Wide enough for GO LIVE, which it now says whenever the now line is off
   screen — sized for the longer label so it does not resize as you scroll. */
[data-layout="mobile"] .rail-live { min-width: 76px; }
[data-layout="mobile"] .rail-group { gap: 6px; }
[data-layout="mobile"] .rail-centre .dateline { font-size: 12px; }
[data-layout="mobile"] .rail-range { flex-basis: 100%; font-size: 12px; min-width: 0; }
[data-layout="mobile"] .rail-hint { display: none; }

/* Leg category on the flight-prep card — same word and colour as its bar. */
.card-cat {
  font: 700 15px var(--sans); letter-spacing: 0.1em;
  border: 1.5px solid; border-radius: 999px; padding: 2px 10px;
  margin-left: 10px; vertical-align: middle; white-space: nowrap;
}
.card-route { display: flex; align-items: center; gap: 0; flex-wrap: wrap; }

/* ---- sync: the button, and the pull that does the same thing ---- */
/* Sits in the map quadrant's empty top-right corner. z-index clears the map
   header, which spans the full width of that panel to centre the clocks. */
/* Both top-right controls in one strip, so adding a second did not mean
   hand-computing the first one's width to place it. */
.top-ctl {
  position: absolute; top: 26px; right: 34px; z-index: 20;
  display: flex; align-items: center; gap: 12px;
}
.sync-btn {
  display: inline-flex; align-items: center; gap: 12px;
  font: 600 22px/1 var(--sans); letter-spacing: 0.14em;
  color: var(--ink-2);
  background: rgba(10, 15, 26, 0.48);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-edge); border-radius: 999px;
  box-shadow: inset 0 1px 0 var(--glass-glint);
  padding: 11px 22px; cursor: pointer;
}
@media (hover: hover) { .sync-btn:hover { color: var(--ink); border-color: var(--line); } }
.sync-ico { font-size: 26px; line-height: 1; display: inline-block; }
/* The glyph turns while the feeds are in flight, then the word reports what
   happened. A button that looks identical before and after is a button people
   press twice. */
.sync-btn.busy { color: var(--warn); }
.sync-btn.busy .sync-ico { animation: spin 0.9s linear infinite; }
.sync-btn.ok { color: var(--good); border-color: rgba(12, 163, 12, 0.5); }
.sync-btn.bad { color: var(--crit); border-color: rgba(208, 59, 59, 0.5); }
@keyframes spin { to { transform: rotate(360deg); } }
/* Square, because it is a glyph rather than a word. */
.fs-btn { padding: 11px; }
.fs-ico {
  width: 26px; height: 26px; display: block;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Arrows out to enter, arrows in to leave. One path is hidden at a time, so the
   button always shows what pressing it DOES rather than where you are. */
.fs-exit { display: none; }
.fs-btn.on { color: var(--good); border-color: rgba(12, 163, 12, 0.45); }
.fs-btn.on .fs-enter { display: none; }
.fs-btn.on .fs-exit { display: block; }

/* ---- account ----
   Same glass as the buttons beside it, cut round. Hidden entirely unless
   sign-in is on AND a session exists — see renderProfile. */
.profile { position: relative; }
.profile[hidden] { display: none; }
.profile-btn {
  position: relative; display: block; overflow: hidden;
  width: 50px; height: 50px; padding: 0; cursor: pointer;
  border-radius: 50%;
  border: 1px solid var(--glass-edge);
  background: rgba(10, 15, 26, 0.48);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: inset 0 1px 0 var(--glass-glint);
}
@media (hover: hover) { .profile-btn:hover { border-color: var(--line); } }
/* Open reads like an engaged control, matching .fs-btn.on next to it. */
.profile.open .profile-btn { border-color: rgba(57, 135, 229, 0.65); }
.profile-pic {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.profile-pic[hidden] { display: none; }
/* Underneath the photo, so a failed load falls back to this with no scripting. */
.profile-ini {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font: 600 19px/1 var(--mono); letter-spacing: 0.04em;
  color: var(--ink-2);
}

.profile-menu {
  position: absolute; top: calc(100% + 12px); right: 0; z-index: 30;
  width: 300px; padding: 16px 18px 18px;
  text-align: left;
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.075) 0%,
    rgba(255, 255, 255, 0.025) 46%,
    rgba(255, 255, 255, 0.05) 100%);
  backdrop-filter: blur(30px) saturate(1.5);
  -webkit-backdrop-filter: blur(30px) saturate(1.5);
  border: 1px solid var(--glass-edge); border-radius: 18px;
  box-shadow:
    inset 0 1px 0 var(--glass-glint),
    0 24px 60px rgba(0, 0, 0, 0.55);
}
.profile-menu[hidden] { display: none; }
.pm-label {
  font: 600 12px/1 var(--mono); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 13px;
}
.pm-who { display: flex; align-items: center; gap: 12px; }
.pm-avatar {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  background-size: cover; background-position: center;
  border: 1px solid var(--glass-edge);
  display: flex; align-items: center; justify-content: center;
  font: 600 17px/1 var(--mono); color: var(--ink-2);
}
.pm-id { min-width: 0; }
.pm-name {
  display: block; font-size: 18px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pm-mail {
  display: block; font: 400 13px/1.4 var(--mono); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Reads like the gate's annunciator, green here because this one is satisfied. */
.pm-strip {
  display: flex; align-items: center; gap: 9px;
  font: 500 12px/1 var(--mono); letter-spacing: 0.12em; color: var(--muted);
  border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
  padding: 10px 0; margin: 14px 0;
}
.pm-lamp {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--good); box-shadow: 0 0 7px rgba(12, 163, 12, 0.7);
}
.pm-out {
  display: block; width: 100%; padding: 11px 0; cursor: pointer;
  font: 600 13px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--serious);
  background: rgba(236, 131, 90, 0.08);
  border: 1px solid rgba(236, 131, 90, 0.4); border-radius: 10px;
}
@media (hover: hover) {
  .pm-out:hover { color: var(--crit); border-color: rgba(208, 59, 59, 0.6); background: rgba(208, 59, 59, 0.12); }
}
.pm-out:disabled { opacity: 0.55; cursor: default; }

/* Menu rows above the sign-out: same shape, neutral colour. */
.pm-item {
  display: block; width: 100%; padding: 11px 0; margin: 0 0 10px; cursor: pointer;
  font: 600 13px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-2);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-edge); border-radius: 10px;
}
@media (hover: hover) { .pm-item:hover { color: var(--ink); border-color: var(--line); } }
.pm-item[hidden] { display: none; }

/* What an account is allowed to do, as a chip. Admin borrows the manual-
   override violet, ops the working blue, viewer stays grey — the palette the
   board already speaks. */
.role-chip {
  display: inline-block; padding: 4px 8px 3px; border-radius: 6px;
  font: 600 11px/1 var(--mono); letter-spacing: 0.12em;
  color: var(--muted); border: 1px solid var(--line);
}
.role-chip.role-admin  { color: var(--violet); border-color: rgba(144, 133, 233, 0.55); }
.role-chip.role-ops    { color: var(--blue);   border-color: rgba(57, 135, 229, 0.55); }
.role-chip.role-viewer { color: var(--muted);  border-color: var(--line); }
.pm-who .role-chip { flex: none; margin-left: auto; }

/* ---- centered modals: account, permissions ----
   Same glass as the sign-in card; fixed at body level so they float over the
   scaled stage. The backdrop dims the board without hiding it — these are
   settings, not a gate. */
.modal {
  position: fixed; inset: 0; z-index: 8000;
  display: grid; place-items: center;
  background: rgba(5, 8, 14, 0.62);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.modal[hidden] { display: none; }
.modal-card {
  width: min(440px, calc(100vw - 40px));
  max-height: min(80vh, 720px);
  display: flex; flex-direction: column;
  padding: 20px 24px 24px;
  background: linear-gradient(158deg,
    rgba(30, 38, 56, 0.92) 0%, rgba(18, 24, 38, 0.92) 50%, rgba(24, 30, 46, 0.92) 100%);
  border: 1px solid var(--glass-edge); border-radius: 20px;
  box-shadow: inset 0 1px 0 var(--glass-glint), 0 30px 80px rgba(0, 0, 0, 0.6);
}
.modal-head { display: flex; align-items: center; margin-bottom: 18px; }
.modal-title { font: 600 13px/1 var(--mono); letter-spacing: 0.2em; color: var(--muted); }
.modal-x {
  margin-left: auto; padding: 2px 8px; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--muted);
  background: none; border: none;
}
@media (hover: hover) { .modal-x:hover { color: var(--ink); } }
.modal-btn {
  padding: 9px 14px; cursor: pointer;
  font: 600 12px/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-2);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-edge); border-radius: 9px;
}
@media (hover: hover) { .modal-btn:hover { color: var(--ink); border-color: var(--line); } }
.modal-btn:disabled { opacity: 0.5; cursor: default; }
.modal-btn-quiet { border-color: transparent; background: none; color: var(--muted); }
.modal-note { margin: 14px 0 0; font: 400 13px/1.5 var(--sans); color: var(--muted); }
.modal-note.err { color: var(--crit); }
.modal-note.okay { color: var(--good); }

/* Account: the photo, then the facts. */
.acct-photo { display: flex; align-items: center; gap: 18px; margin-bottom: 18px; }
.acct-avatar {
  flex: none; width: 84px; height: 84px; border-radius: 50%;
  background-size: cover; background-position: center;
  background-color: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--glass-edge);
  display: flex; align-items: center; justify-content: center;
  font: 600 30px/1 var(--mono); color: var(--ink-2);
}
.acct-photo-btns { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.acct-rows { border-top: 1px solid var(--hair); }
.acct-row {
  display: flex; align-items: center; gap: 16px;
  padding: 12px 0; border-bottom: 1px solid var(--hair);
}
.acct-k { flex: none; width: 110px; font: 600 11px/1 var(--mono); letter-spacing: 0.16em; color: var(--muted); }
.acct-v {
  min-width: 0; font-size: 16px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Permissions: one row per account, selector on the right, Save underneath. */
.perm-card { width: min(560px, calc(100vw - 40px)); position: relative; }
.perm-list { overflow-y: auto; min-height: 60px; border-top: 1px solid var(--hair); }
.perm-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--hair);
}
.perm-avatar {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  background-size: cover; background-position: center;
  background-color: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--glass-edge);
  display: flex; align-items: center; justify-content: center;
  font: 600 14px/1 var(--mono); color: var(--ink-2);
}
.perm-id { min-width: 0; flex: 1; }
.perm-name {
  display: block; font-size: 15px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.perm-mail {
  display: block; font: 400 12px/1.4 var(--mono); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.perm-sel {
  flex: none; padding: 7px 10px; cursor: pointer;
  font: 600 12px/1 var(--mono); letter-spacing: 0.1em;
  color: var(--ink-2);
  background: rgba(10, 15, 26, 0.6);
  border: 1px solid var(--glass-edge); border-radius: 8px;
}
.perm-sel:disabled { opacity: 0.5; cursor: default; }
/* A row the screen has changed but not saved yet. */
.perm-row.dirty .perm-sel { border-color: rgba(250, 178, 25, 0.6); color: var(--warn); }

/* An account waiting on somebody, and one that has been refused. The first is
   a job for whoever opened this screen, so it is the one that draws the eye;
   the second is a settled decision and reads as struck through rather than as
   an alarm. */
.perm-row.perm-new { background: rgba(250, 178, 25, 0.06); }
.perm-row.perm-blocked { opacity: 0.55; }
.perm-tag {
  margin-left: 8px; padding: 1px 6px; border-radius: 4px;
  font: 700 11px var(--sans); letter-spacing: 0.1em;
  background: rgba(250, 178, 25, 0.16); color: var(--warn);
}
.perm-tag-x { background: rgba(214, 69, 69, 0.16); color: var(--crit); }
/* Its own control, not a value in the selector: refusing somebody is a
   different kind of act from choosing what they may do. */
.perm-block {
  margin-left: 10px; padding: 4px 10px; border-radius: 6px; cursor: pointer;
  background: none; border: 1px solid var(--line); color: var(--muted);
  font: 700 12px var(--sans); letter-spacing: 0.09em;
}
@media (hover: hover) {
  .perm-block:hover { border-color: var(--crit); color: var(--crit); }
  .perm-block.is-on:hover { border-color: var(--good); color: var(--good); }
}
.perm-block.is-on { border-color: rgba(12, 163, 12, 0.5); color: var(--good); }

/* The confirmation, over the list rather than over the whole board: the
   question is "is this the right person", and the rows stay readable behind
   it so the answer is on screen. */
/* FIXED, not absolute, and above the modal's own 8000.
   
   It was absolute-inset-0 inside .perm-card, which made it depend on that card
   carrying `position: relative` — and on the browser having the stylesheet that
   says so. A viewer running a cached copy from before this shipped got an
   overlay anchored somewhere else entirely, with the card painting over the
   buttons: the dialog was on screen and Cancel could not be pressed. Anchored
   to the viewport it cannot be got at by any ancestor. #perm-modal is a child
   of <body>, outside the scaled stage, so `fixed` here means the screen. */
.perm-confirm {
  position: fixed; inset: 0; z-index: 8100;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 10, 14, 0.72); backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
/* AND THIS LINE IS LOAD-BEARING.
   `hidden` hides an element through the browser's own `[hidden] {display:none}`,
   which is the weakest rule there is: the `display: flex` above is a class
   selector and beats it outright. Without this the dialog was on screen from
   the moment PERMISSIONS opened — covering the list, swallowing every click,
   and showing Cancel and Block with no handlers on them, because those are
   attached in confirmBlock and it had never run. It looked like a popup that
   would not close; it was a popup that had never been opened. */
.perm-confirm[hidden] { display: none; }
/* Opaque, and stated rather than borrowed. This read `var(--panel)`, which is
   not a variable this stylesheet defines — so the box had no background at all
   and the rows behind it showed straight through the question. The same near
   -solid dark the modal card uses, so the two look like one thing. */
.pc-box {
  width: min(420px, 86vw); padding: 22px 24px; text-align: center;
  background: linear-gradient(158deg,
    rgba(30, 38, 56, 0.98) 0%, rgba(18, 24, 38, 0.98) 50%, rgba(24, 30, 46, 0.98) 100%);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
}
.pc-title { font: 700 19px var(--sans); color: var(--ink); }
.pc-who { margin-top: 12px; font: 600 22px var(--sans); color: var(--ink); }
.pc-mail { margin-top: 2px; font: 500 15px var(--num); color: var(--muted); }
.pc-body { margin: 14px 0 18px; font: 400 14px var(--sans); line-height: 1.5; color: var(--ink-2); }
.pc-btns { display: flex; gap: 10px; justify-content: center; }
/* The dangerous answer wears the colour and is NOT the default focus — see
   confirmBlock, where Cancel takes focus and Enter confirms nothing. */
.pc-danger { border-color: rgba(214, 69, 69, 0.6); color: var(--crit); }
@media (hover: hover) { .pc-danger:hover { background: rgba(214, 69, 69, 0.12); } }

/* Signed in and held: waiting for approval, or refused. The Google button is
   hidden in this state — they are already signed in, and offering it again
   would have them press it and land straight back here. */
.signin-held .signin-lamp { background: var(--warn); box-shadow: 0 0 12px var(--warn); }
.signin-fine { display: inline-block; margin-top: 8px; font-size: 13px; color: var(--muted); }
.perm-foot { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
.perm-foot .modal-note { margin: 0; flex: 1; }
.perm-save { border-color: rgba(57, 135, 229, 0.55); color: var(--blue); }
@media (hover: hover) { .perm-save:hover:not(:disabled) { color: var(--ink); background: rgba(57, 135, 229, 0.25); } }

/* ---- viewer: the board reads, nothing on it writes ----
   The role attribute is stamped on <html> by renderProfile. The server refuses
   a viewer's writes with a 403 regardless; this is the honest UI for it. */
[data-role="viewer"] .cl-item { cursor: default; }
[data-role="viewer"] .cl-box { opacity: 0.4; }
[data-role="viewer"] .cl-cancel { display: none; }
[data-role="viewer"] .nc-ack, [data-role="viewer"] #b-ack { display: none; }

[data-layout="mobile"] .modal-card { padding: 16px 16px 18px; border-radius: 16px; }
[data-layout="mobile"] .acct-avatar { width: 64px; height: 64px; font-size: 22px; }
[data-layout="mobile"] .acct-k { width: 92px; font-size: 10px; }
[data-layout="mobile"] .acct-v { font-size: 14px; }
[data-layout="mobile"] .perm-name { font-size: 13px; }
[data-layout="mobile"] .perm-mail { font-size: 11px; }
[data-layout="mobile"] .perm-sel { font-size: 11px; padding: 6px 8px; }
[data-layout="mobile"] .pm-item { font-size: 11px; padding: 9px 0; }

[data-layout="mobile"] .profile-btn { width: 30px; height: 30px; }
[data-layout="mobile"] .profile-ini { font-size: 12px; }
[data-layout="mobile"] .profile-menu { width: 250px; padding: 13px 14px 15px; border-radius: 15px; }
[data-layout="mobile"] .pm-label { font-size: 10px; margin-bottom: 10px; }
[data-layout="mobile"] .pm-avatar { width: 34px; height: 34px; font-size: 13px; }
[data-layout="mobile"] .pm-name { font-size: 14px; }
[data-layout="mobile"] .pm-mail { font-size: 11px; }
[data-layout="mobile"] .pm-strip { font-size: 10px; padding: 8px 0; margin: 11px 0; }
[data-layout="mobile"] .pm-out { font-size: 11px; padding: 9px 0; }

.pull {
  position: fixed; top: 0; left: 0; right: 0; height: 54px; z-index: 60;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  font: 700 13px/1 var(--sans); letter-spacing: 0.18em; color: var(--muted);
  background: linear-gradient(180deg, rgba(10, 15, 26, 0.95), rgba(10, 15, 26, 0));
  transform: translateY(-54px);
  pointer-events: none;            /* it is a readout, never a target */
}
.pull-ico { font-size: 16px; display: inline-block; }
/* Past the trip point: the gesture will fire if released here, so say so before
   the release rather than after it. */
.pull.armed { color: var(--good); }
.pull.armed .pull-ico { transform: rotate(180deg); }
.pull.busy { color: var(--warn); }
.pull.busy .pull-ico { animation: spin 0.9s linear infinite; }

/* ---- overflow: panels scroll rather than silently clipping ----
   A wall is not scrolled by hand, so these panels still prioritise and
   summarise; this is the safety valve for the day the content genuinely does
   not fit, and it is what makes the phone layout usable at all. */
.cards, .wx-list { overflow-y: auto; overscroll-behavior: contain; }
.cards, .wx-list, .nc-rows, .tfr-col, .flow-col { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.22) transparent; }
.cards::-webkit-scrollbar, .wx-list::-webkit-scrollbar,
.nc-rows::-webkit-scrollbar, .tfr-col::-webkit-scrollbar, .flow-col::-webkit-scrollbar { width: 10px; }
.cards::-webkit-scrollbar-track, .wx-list::-webkit-scrollbar-track,
.nc-rows::-webkit-scrollbar-track, .tfr-col::-webkit-scrollbar-track, .flow-col::-webkit-scrollbar-track { background: transparent; }
.cards::-webkit-scrollbar-thumb, .wx-list::-webkit-scrollbar-thumb,
.nc-rows::-webkit-scrollbar-thumb, .tfr-col::-webkit-scrollbar-thumb, .flow-col::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.22); border-radius: 999px;
}
.tfr-col, .flow-col { overflow-y: auto; min-height: 0; }
/* The phone scrolls the DOCUMENT rather than each panel, so these hand their
   content back to the page. `.cards` is deliberately NOT in this list any more:
   it is the swipe deck, and a deck has to clip and scroll on its own axis. This
   rule sits later in the file at equal specificity, so leaving it in here beat
   the deck's `overflow-x: auto` and the cards simply hung off the side of the
   panel — visible, unscrollable, unreachable. */
[data-layout="mobile"] .wx-list,
[data-layout="mobile"] .tfr-col, [data-layout="mobile"] .flow-col { overflow: visible; }

/* ---- the board is not painted until the session check has answered ----
   `visibility` rather than `display`: the layout still resolves underneath, so
   the stage is measured and scaled while hidden and appears already correct
   rather than reflowing in front of the reader.

   The gate itself is deliberately NOT covered by this rule — it has to be
   visible while the board is not. */
html[data-auth="pending"] #fit,
html[data-auth="denied"] #fit { visibility: hidden; }
/* Something to look at while the check is in flight, so a slow network reads
   as "working" rather than as a broken screen. */
html[data-auth="pending"] body::after {
  content: ""; position: fixed; left: 50%; top: 50%; z-index: 100;
  width: 34px; height: 34px; margin: -17px 0 0 -17px;
  border: 2px solid var(--hair); border-top-color: var(--blue); border-radius: 50%;
  animation: auth-spin 0.9s linear infinite;
}
@keyframes auth-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { html[data-auth="pending"] body::after { animation: none; } }

/* ---- sign-in ----
   Covers everything, because a half-visible board behind a login prompt invites
   somebody to try to read it. Only ever shown when sign-in is switched on and
   the silent renewal below has already failed — on the wall that should be
   never.

   Cut from the same glass as the four quadrants: its own light field, a faint
   route chart drifting underneath, and one slab of blurred glass on top. The
   chart matters — backdrop-filter needs something behind it to refract, and
   with the board hidden a plain background made the "glass" read as flat grey. */
.signin {
  position: fixed; inset: 0; z-index: 9000;
  display: grid; place-items: center;
  overflow: hidden;
  /* Inter across the whole gate (user-specified) — the one screen that is not
     the board, so it is allowed its own voice. Everything inside inherits,
     including the pieces that were cut in Plex Mono; `tabular-nums` below keeps
     the ticking clock from shimmying now that it is on a proportional face. */
  font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  background:
    radial-gradient(46% 58% at 11% 6%,  rgba(64, 104, 186, 0.20), transparent 62%),
    radial-gradient(40% 50% at 87% 10%, rgba(102, 76, 190, 0.15), transparent 60%),
    radial-gradient(52% 62% at 80% 94%, rgba(28, 118, 146, 0.13), transparent 64%),
    radial-gradient(44% 56% at 18% 90%, rgba(128, 82, 168, 0.10), transparent 62%),
    /* soft source directly behind the card, so its blur has light to catch */
    radial-gradient(34% 40% at 50% 46%, rgba(70, 96, 156, 0.14), transparent 70%),
    var(--page, #0b0f14);
}
.signin[hidden] { display: none; }

/* The route chart. Runs UNDER the card on purpose: the legs crossing behind
   the glass are what sells the material. */
.signin-map { position: absolute; inset: 0; width: 100%; height: 100%; }
.signin-map path {
  fill: none; stroke: rgba(139, 147, 164, 0.26);
  stroke-width: 1.5; stroke-dasharray: 5 7;
}
.signin-map path.sm-live { stroke: rgba(57, 135, 229, 0.55); animation: sm-crawl 48s linear infinite; }
@keyframes sm-crawl { to { stroke-dashoffset: -600; } }
.signin-map .sm-wpt rect { fill: none; stroke: rgba(139, 147, 164, 0.5); stroke-width: 1.5; }
.signin-map .sm-wpt text {
  font: 500 19px Inter, system-ui, sans-serif; letter-spacing: 0.08em;
  fill: rgba(139, 147, 164, 0.62);
}
.signin-map .sm-base rect { fill: rgba(57, 135, 229, 0.75); stroke: none; }
.signin-map .sm-base text { fill: rgba(178, 198, 228, 0.85); }

.signin-card {
  position: relative; z-index: 1;
  width: min(480px, calc(100vw - 48px));
  padding: 42px 44px 40px;
  text-align: center;
  background: linear-gradient(158deg,
    rgba(255, 255, 255, 0.075) 0%,
    rgba(255, 255, 255, 0.025) 46%,
    rgba(255, 255, 255, 0.05) 100%);
  backdrop-filter: blur(26px) saturate(1.45);
  -webkit-backdrop-filter: blur(26px) saturate(1.45);
  border: 1px solid var(--glass-edge);
  border-radius: 26px;
  box-shadow:
    inset 0 1px 0 var(--glass-glint),
    inset 0 -1px 0 rgba(255, 255, 255, 0.04),
    0 30px 80px rgba(0, 0, 0, 0.55);
  /* instrument-bezel second ring */
  outline: 1px solid rgba(255, 255, 255, 0.05);
  outline-offset: 7px;
  overflow: hidden;
}
/* Slow specular sweep — the liquid in the glass. Rests most of the cycle. */
.signin-card::after {
  content: ""; position: absolute; top: -40%; bottom: -40%; left: 0;
  width: 34%; pointer-events: none;
  background: linear-gradient(90deg, transparent,
    rgba(255, 255, 255, 0.045) 42%, rgba(255, 255, 255, 0.09) 50%,
    rgba(255, 255, 255, 0.045) 58%, transparent);
  transform: translateX(-140%) rotate(16deg);
  animation: signin-glint 12s ease-in-out infinite;
}
@keyframes signin-glint {
  0%, 58%   { transform: translateX(-140%) rotate(16deg); }
  88%, 100% { transform: translateX(420%)  rotate(16deg); }
}

.signin-head {
  display: flex; align-items: center; justify-content: center;
  gap: 14px; margin-bottom: 24px;
}
.signin-brand {
  font-weight: 700; letter-spacing: 0.42em; margin-right: -0.42em;
  font-size: 26px; color: var(--ink);
}
/* Taxiway location sign: yellow inscription, yellow border, black field. */
.signin-sign {
  font: 600 15px/1 inherit; letter-spacing: 0.14em;
  color: var(--warn); border: 2px solid var(--warn); border-radius: 6px;
  padding: 6px 8px 5px 10px; background: rgba(0, 0, 0, 0.35);
}
/* Same voice as the quadrant headers ("TODAY'S SCHEDULE", "FLIGHT PROGRESS"),
   so the gate reads as part of the wall rather than a page in front of it. */
.signin-card h2 {
  font-size: 21px; font-weight: 700; margin: 0 0 20px;
  letter-spacing: 0.22em; text-transform: uppercase; text-indent: 0.22em;
  color: var(--ink-2);
}

/* Status line: live Zulu, and why the gate is up. Reads like an annunciator. */
.signin-strip {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  font: 500 15px inherit; letter-spacing: 0.1em; color: var(--muted);
  border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
  padding: 11px 0; margin: 0 0 24px;
}
.signin-zulu { color: var(--ink-2); font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.signin-lamp {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--warn); box-shadow: 0 0 8px rgba(250, 178, 25, 0.7);
  animation: signin-lamp 2.4s ease-in-out infinite;
}
@keyframes signin-lamp { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

.signin-sub { font-size: 17px; color: var(--muted); margin: 0 0 26px; }
#gsi-button { display: flex; justify-content: center; min-height: 44px; }

/* Failure as a caution annunciator, not a paragraph. */
.signin-err {
  margin: 22px auto 0; max-width: 360px;
  font: 600 13px/1.5 inherit; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--crit);
  border: 1px solid rgba(208, 59, 59, 0.5); border-radius: 8px;
  background: rgba(208, 59, 59, 0.08);
  padding: 10px 14px;
}
.signin-err::before { content: "\26A0  "; }
.signin-err[hidden] { display: none; }

[data-layout="mobile"] .signin-card { padding: 30px 22px 28px; border-radius: 20px; }
[data-layout="mobile"] .signin-brand { font-size: 19px; }
[data-layout="mobile"] .signin-sign { font-size: 12px; padding: 5px 6px 4px 8px; }
[data-layout="mobile"] .signin-card h2 { font-size: 15px; margin-bottom: 16px; }
[data-layout="mobile"] .signin-strip { font-size: 12px; gap: 9px; padding: 9px 0; margin-bottom: 18px; }
[data-layout="mobile"] .signin-sub { font-size: 14px; margin-bottom: 20px; }
[data-layout="mobile"] .signin-map .sm-wpt text { font-size: 15px; }

@media (prefers-reduced-motion: reduce) {
  .signin-card::after, .signin-map path.sm-live, .signin-lamp { animation: none; }
}
