/* =====================================================================
   Skycentrix — Aviation Operations Platform — Design System
   Self-contained, no external dependencies.
   ===================================================================== */

/* The typeface lives in fonts.css, linked before this file by every document that
   renders text — the app, the airline portal, and each standalone public page. */

:root {
  /* Tell the browser which side we are on, so the NATIVE bits follow the theme too:
     scrollbars, the text caret, form controls, and the flash before first paint. Without
     this the app went dark and the scrollbars stayed light. `data-theme` is the resolved
     theme (never "system"), so these two rules always match what the CSS variables do. */
  color-scheme: dark;
  /* Brand — platform blue + green accent */
  /* THE TENANT COLOUR ARRIVES THROUGH --brand-tenant, NEVER --brand.
     `App.applyBrand` used to write --brand inline on <html>, which outranks every selector
     in this file — so `html[data-theme="light"] { --brand: … }` below never applied once a
     user was signed in, and the light theme silently ran on the dark palette's blue. An
     inline custom property cannot be beaten by a stylesheet; it can only be read by one. */
  --brand: var(--brand-tenant, #1B9FDD);       /* brand blue */
  --brand-dark: var(--brand-tenant-dark, #1480B5);
  --brand-soft: var(--brand-tenant-soft, rgba(27, 159, 221, 0.14));
  --brand-ink: var(--brand-tenant-ink-dark, #29a5df);   /* on --brand-soft: 4.31 -> 4.60 */
  --brand-fill: var(--brand-tenant-fill, #157cac); /* white on it: 2.98 -> 4.65 */
  --accent: #8CC63F;         /* accent green */
  --accent-dark: #6fa32f;
  --accent-soft: rgba(140, 198, 63, 0.16);
  --accent-ink: #8cc63f;     /* 5.65 on --accent-soft */
  --accent-fill: #5a7f28;    /* white on it: 2.05 -> 4.68 */

  /* Surfaces (dark ops console) */
  --bg: #0e1320;
  --bg-2: #141b2d;
  --surface: #1a2236;
  --surface-2: #212c44;
  --surface-3: #283450;
  --panel-2: var(--surface-3);   /* alias used by chips/tracks; follows the theme via --surface-3 */
  --border: #2c3a59;
  --border-soft: #243049;

  /* The arrivals-wall scope paints SEA and LAND, and neither is a surface token. Reusing
     --bg / --surface-2 was the obvious thing and it broke light theme outright: there they are
     #eef2f8 and #f6f9fd, so the land was a quarter-percent LIGHTER than the ocean and the map
     rendered as a blank disc. A map's figure/ground is its own decision, so it gets its own
     three values per theme, chosen against each other rather than against a panel. */
  --wall-sea: #0b1120;  --wall-land: #243052;  --wall-land-line: #3c5182;

  /* Text */

  /* ALIASES for names the views actually reached for. `var(--line)` appears bare in ~32 places
     across eight views and the LIR aircraft diagram, and `var(--surface-1)` in two more — and
     an undefined custom property makes the WHOLE declaration invalid at computed-value time.
     Measured: `border: 1px solid var(--line)` computed to width 0 / style none, so every one
     of those hairlines simply did not render, and the LIR panels had no background. Defining
     the aliases fixes all 34 sites at once and keeps them theme-aware, which a find-and-replace
     across eight files would not. */
  --line: var(--border);
  --surface-1: var(--surface);

  --text: #eef2fb;
  --text-2: #aeb9d4;
  /* --text-3 is the muted token behind every sub-line, timestamp, help string and unit label,
     so it carries a large share of the platform's text. Measured against the real surfaces it
     failed WCAG AA almost everywhere: 4.33 on --surface and 3.81 on --surface-2 (dark), and
     3.14 / 2.79 / 2.97 in light — against a 4.5 floor for body text. On screens the handheld
     contract itself describes as read "at arm's length, in sunlight, in gloves".
     Lifted by the SMALLEST step that clears 4.5 on the WORST pairing, so the hierarchy holds:
     --text-3 is still visibly dimmer than --text-2 (5.14 vs 8.06 on --surface). */
  /* 4.01 on --surface-3 was the pairing the earlier pass did not measure — chips, dots and
     share rows all sit on it. Lifted 9% further; --text-2 is still clearly brighter
     (8.06 vs 5.80 on --surface), so the three-step hierarchy holds. */
  --text-3: #909db9;   /* dark: worst pairing 4.01 -> 4.53 */

  /* Status palette.
     THREE ROLES, NOT ONE, and conflating them is what put 94 pairings under AA.

       --x          the identity colour: an icon, a rule, a chart series, a border.
                    Foreground on an ordinary surface, where it already passes.
       --x-ink      the same hue as TEXT ON ITS OWN --x-soft chip. A 15%-alpha wash of a
                    colour barely moves the background, so the hue sitting on it is very
                    nearly the hue on the page — 2.6 to 3.6 across the badge family.
       --x-fill     the same hue as a SOLID BACKGROUND under white text. Every one of these
                    was failing: white on --ok measured 2.28, which is the platform's own
                    handheld success toast.

     Each value is the SMALLEST step from --x that clears 4.5 against the surface it is
     actually used on, computed rather than eyeballed, and re-measured by
     tests/test_contrast.py on every run. */
  /* STATUS COLOURS — softened by LIGHTNESS, not by saturation.
     The old set was Tailwind red-500 / green-500 / amber-500 at full chroma AND high
     lightness, which is what made them glare on a dark ground: `--ok #22c55e` is a spring
     green, and the neon in it is its YELLOWNESS, not its intensity. Lowering chroma instead —
     the obvious move — produced a pastel that read as "disabled" rather than "calmer", which
     is the thing to know if this is ever revisited.
     So: chroma is roughly held, lightness comes down, and the hues move off the primaries —
     red toward carmine (18°), green toward emerald (163°). Validated with the palette
     validator against this surface; the old set FAILED it twice over, on the dark-mode
     lightness band and on `#f59e0b ↔ #22c55e` at ΔE 5.7 for protanopia, which is below the
     floor — amber and green were near-indistinguishable for about one man in twelve.
     `--danger` sits at 3.82 on --surface (the old one was 4.21, itself already under the 4.5
     small-text floor). Both clear the 3.0 large-text threshold, and every element that renders
     status as SMALL text uses the `-ink` tokens below. Those are tuned to ~4.8 rather than to
     4.5 exactly: stopping at the first value that clears the floor lands on 4.50, and
     tests/test_contrast computes at full precision and rightly calls that a failure. */
  --ok: #19af7c;
  /* The ink that sits ON an --ok fill. A near-black green reads best on the dark theme's
     brighter green (5.55) and fails on the light theme's darker one (3.47), so the pairing is
     a token rather than a literal — four rules hard-coded #042a12, and changing --ok for the
     colour-blind separation quietly broke all four until tests/test_contrast caught it. */
  --ok-on: #042a12;
  --ok-soft: rgba(25, 175, 124, 0.15);
  --ok-ink: #27b484;                /* 4.78 on --ok-soft */
  --ok-fill: #008858;               /* white on it: 4.50 */
  --warn: #eb9f2c;
  --warn-soft: rgba(235, 159, 44, 0.15);
  --warn-ink: #eb9f2c;              /* 5.49 on --warn-soft */
  --warn-fill: #ad6500;             /* white on it: 4.53 */
  --danger: #dc4658;
  --danger-soft: rgba(220, 70, 88, 0.15);
  --danger-ink: #e57683;            /* 4.78 on --danger-soft */
  --danger-fill: #d24253;           /* white on it: 4.52 */
  --info: #3b82f6;
  --info-soft: rgba(59, 130, 246, 0.15);
  --info-ink: #609af8;              /* 3.54 -> 4.63 */
  --info-fill: #3371d6;             /* 3.68 -> 4.69 */
  --purple: #a855f7;
  --purple-soft: rgba(168, 85, 247, 0.15);
  --purple-ink: #bb7af9;            /* 3.37 -> 4.63 */
  --purple-fill: #994de1;           /* 3.96 -> 4.66 */
  --teal: #14b8a6;
  --teal-soft: rgba(20, 184, 166, 0.15);
  --teal-ink: #14b8a6;              /* 4.94 */
  --teal-fill: #0e8376;             /* 2.49 -> 4.66 */

  --radius: 14px;
  --radius-sm: 9px;
  /* Height of every control that shares a toolbar row — buttons, segmented tabs,
     selects, the search box. Without an explicit height each one is only as tall as
     its own content's line box, so a button labelled "⬇ CSV" came out 3px taller than
     "+ New Case" (emoji have a taller line box) and a native select taller again. Set
     it once and every toolbar lines up regardless of what's written on it. */
  --control-h: 36px;
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.25);
  /* Sized so the longest department label ("Commercial & Corporate") fits without
     truncating — at 256px six of the twelve nav sections rendered with an ellipsis. */
  --sidebar-w: 296px;
  /* Let every device use the interface face it was designed around: SF Pro on macOS and
     iOS, Segoe UI Variable on Windows 11, Roboto on Android — each one hinted for the
     renderer actually drawing it. Naming "Segoe UI" first (as this did) meant Apple
     hardware silently fell through to system-ui anyway, and Windows 11 got legacy Segoe
     rather than the variable cut. The named faces after system-ui are only there for
     browsers that don't resolve the generic. */
  --font: "IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Plex Mono for the same reason as the sans, plus one this stack made worse: four
     candidate faces meant four digit widths and four ideas of what a zero looks like,
     so an AWB read off one desk did not sit the same as the same AWB on the next. The
     PDF pipeline and the printed labels have been on Plex Mono all along. */
  --mono: "IBM Plex Mono", "Cascadia Code", "SF Mono", Consolas, "Roboto Mono", monospace;
}

/* ---------------------------------------------------------------- Light theme
   An airy, high-whitespace palette. Toggled from the topbar (persisted to
   localStorage as sc_theme). Everything is variable-driven, so overriding the
   tokens re-themes the whole app; the few hardcoded dark chrome surfaces
   (sidebar, topbar) get explicit light overrides below. */
html[data-theme="light"] {
  color-scheme: light;
  --brand: var(--brand-tenant, #1487c4);
  --brand-dark: var(--brand-tenant-dark, #0f6699);
  --brand-soft: var(--brand-tenant-soft, rgba(20, 135, 196, 0.10));
  --brand-ink: var(--brand-tenant-ink-light, #1173a7); /* 3.52 -> 4.63 */
  --brand-fill: var(--brand-tenant-fill, #127bb2);  /* 3.97 -> 4.67 */
  --accent: #6fa32f; --accent-dark: #5c8a26; --accent-soft: rgba(111, 163, 47, 0.14);
  --accent-ink: #507522;   /* 2.64 -> 4.67 */
  --accent-fill: #577f25;  /* 3.02 -> 4.70 */

  --bg: #eef2f8;  --bg-2: #f5f8fc;  --surface: #ffffff;  --surface-2: #f6f9fd;  --surface-3: #eceff6;
  --border: #dce3ee;  --border-soft: #e8edf5;
  --wall-sea: #c8daf0;  --wall-land: #ffffff;  --wall-land-line: #9db6d4;
  --line: var(--border);  --surface-1: var(--surface);

  /* Light was the worse of the two — 3.14 / 2.79 / 2.97 against --surface / --bg / --surface-2.
     Same rule: smallest darkening that clears 4.5 on the worst pairing (now 4.56). */
  --text: #16233d;  --text-2: #4c5a75;  --text-3: #626d80;   /* 4.40 on --surface-3 -> 4.55 */

  /* The dark theme's hues, stepped darker for white. Same three families, so a screenshot from
     one theme still reads as the same product in the other. */
  /* ok and warn are stepped APART in luminance, not just chosen for contrast. On white the
     space is tight: for warn to separate from ok it has to be lighter, which costs contrast,
     and the first pass put them at 0.195 and 0.181 — indistinguishable for a deuteranope,
     the very failure this palette was replacing. Balanced so the WEAKER of the two gaps is as
     large as possible (0.059 danger/ok, 0.060 ok/warn) rather than one gap being generous and
     the other barely there. warn lands at 3.59 on white: over the large-text floor it needs,
     and every small use goes through --warn-ink at 4.55. */
  --ok: #008858;     --ok-on: #ffffff;   --ok-soft: rgba(0, 136, 88, 0.12);
  --ok-ink: #00754c;     --ok-fill: #008854;      /* 4.92 on --ok-soft · white on it 4.52 */
  --warn: #bf7700;   --warn-soft: rgba(191, 119, 0, 0.12);
  --warn-ink: #955d00;   --warn-fill: #ad6500;    /* 4.78 · 4.53 */
  --danger: #b82c41; --danger-soft: rgba(184, 44, 65, 0.12);
  --danger-ink: #b82c41; --danger-fill: #b82c41;  /* 5.01 · 6.04 */
  --info: #2563eb;   --info-soft: rgba(37, 99, 235, 0.10);
  --info-ink: #2461e6;   --info-fill: #2563eb;    /* 4.50 -> 4.65 · already 5.17 */
  --purple: #9333ea; --purple-soft: rgba(147, 51, 234, 0.10);
  --purple-ink: #9333ea; --purple-fill: #9333ea;  /* already 4.64 · 5.38 */
  --teal: #0d9488;   --teal-soft: rgba(13, 148, 136, 0.12);
  --teal-ink: #0b786e;   --teal-fill: #0b8278;    /* 3.25 -> 4.65 · 3.74 -> 4.68 */

  --shadow: 0 10px 34px rgba(23, 43, 84, 0.10);
  --shadow-sm: 0 2px 10px rgba(23, 43, 84, 0.07);
}
/* The wordmark swaps to its dark-text variant in light mode (default = light-text). */
html[data-theme="light"] .sc-wm-onDark { display: none !important; }
html[data-theme="light"] .sc-wm-onLight { display: block !important; }
/* …except on surfaces that are ALWAYS dark regardless of the app theme (the auth
   aside has its own hardcoded dark gradient). There the light-text wordmark must
   stay — the dark-text one is invisible on the dark panel. Higher specificity than
   the rules above, so it wins in light mode. */
html[data-theme="light"] .auth-aside .sc-wm-onDark { display: block !important; }
html[data-theme="light"] .auth-aside .sc-wm-onLight { display: none !important; }
html[data-theme="light"] .sidebar { background: #ffffff; border-right: 1px solid var(--border); }
html[data-theme="light"] .nav-item:hover { background: var(--surface-3); }
html[data-theme="light"] .nav-item.active { background: var(--brand-soft); color: var(--brand-ink); }
html[data-theme="light"] .topbar { background: rgba(255, 255, 255, 0.82); }
html[data-theme="light"] .mobile-nav { background: rgba(255, 255, 255, 0.96); }
/* smooth the flip between themes */
html, body { transition: background-color .25s ease, color .25s ease; }

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
/* A NUMBER THAT OPENS WHAT IT COUNTED. Anchors are reset to inherit/none above, so a figure
   wrapped in one is indistinguishable from the figure beside it that leads nowhere: in the
   staff-performance table "6" under Open opens six cases and "16" under Handled opens
   nothing, and until this nothing on screen said which was which. A chevron per cell — the
   affordance the KPI tiles use — is noise at table density, so: a dotted rule at rest, solid
   under the cursor, drawn in `currentColor` so an overdue figure keeps the colour that is
   the reason a supervisor is looking at it. */
.num-link { text-decoration-line: underline; text-decoration-style: dotted;
  text-decoration-color: currentColor; text-decoration-thickness: 1px;
  text-underline-offset: 3px; cursor: pointer; }
.num-link:hover { text-decoration-style: solid; }
button { font-family: inherit; cursor: pointer; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 10px; }
::-webkit-scrollbar-track { background: transparent; }

/* ------------------------------------------------- Numerals & display type
   Every figure the eye compares down a column gets tabular numerals. By
   default each digit is a different width — a "1" is narrow, a "0" wide — so
   a column of costs, minutes or counts never lines up on its decimal and has
   to be re-found row by row instead of scanned. It also stops values that
   update in place (clocks, badge counts, live KPIs) twitching sideways as
   their digits change. Set structurally rather than per-view so a new table
   or tile inherits it without anyone having to remember.

   Deliberately NOT global: running prose (notes, telex bodies, descriptions,
   training content) reads better with proportional digits inside a sentence,
   and the mono face is already fixed-width. */
.table, table.data, table.tbl, table.matrix,
.num, .right,
.kpi .value, .kpi .delta, .kpi .foot, .kpi .kpi-val, .kpi .kpi-sub,
.dash-val, .dash-bar-top, .dash-li, .dash-foot,
.badge, .badge-count, .seg-badge, .bell-badge, .suite-count {
  font-variant-numeric: tabular-nums;
}

/* Display-size type sets loose at its default tracking — the bigger the type,
   the more air every pair carries — so large numbers read as separate digits
   rather than one value. Pull it back in proportion to size. Body and label
   text is left alone: tightening small text costs legibility, and these
   screens get read at arm's length on a ramp tablet. */
.kpi .value, .kpi .kpi-val, .dash-val { letter-spacing: -.02em; }
.auth-aside-inner h1 { letter-spacing: -.015em; }
.page-head h2 { letter-spacing: -.012em; }
.topbar h1 { letter-spacing: -.006em; }

/* ---------------------------------------------------------------- Layout */
.app { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  transition: width .2s cubic-bezier(.33,1,.68,1), flex-basis .2s cubic-bezier(.33,1,.68,1);
  background: linear-gradient(180deg, #0c1322, #0a0f1c);
  border-right: 1px solid var(--border-soft);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
  padding-top: env(safe-area-inset-top, 0px);
  z-index: 30;
}

.brand {
  display: flex; align-items: center; gap: 12px;
  padding: 20px 20px 16px;
}
.brand-logo {
  width: 42px; height: 42px; border-radius: 12px;
  display: grid; place-items: center; overflow: hidden;
  box-shadow: 0 4px 14px rgba(27, 159, 221, 0.4);
}
.brand-logo svg { width: 100%; height: 100%; display: block; }
.brand-text { line-height: 1.1; }
.brand-text b { font-size: 16px; letter-spacing: .3px; }
.brand-text span { display: block; font-size: 10.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.5px; }

.nav { padding: 8px 12px; overflow-y: auto; flex: 1; }
.nav-section { margin: 14px 8px 6px; font-size: 10.5px; text-transform: uppercase; letter-spacing: 1.3px; color: var(--text-3); }
.nav-item {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border-radius: 10px; color: var(--text-2);
  font-weight: 500; margin: 2px 0; position: relative;
  transition: background .15s, color .15s;
}
.nav-item:hover { background: var(--surface); color: var(--text); }
.nav-item.active { background: var(--brand-soft); color: #fff; }
.nav-item.active::before {
  content: ""; position: absolute; left: -12px; top: 8px; bottom: 8px;
  width: 4px; border-radius: 4px; background: var(--brand);
}
/* Uniform flat-line icon rendering across the whole app (sidebar, hubs, launcher). */
.ico { stroke-linecap: round; stroke-linejoin: round; }
/* A module page title now draws the same flat SVG the sidebar does. `.ico` carries no
   intrinsic size — every other use sizes it by context — so without this the 24×24 viewBox
   renders at whatever the browser picks rather than at heading size. */
.page-h2-ico { display: flex; align-items: center; gap: 9px; }
.page-h2-ico .ico { width: 22px; height: 22px; flex: 0 0 22px; opacity: .85; }
.nav-item .ico { width: 20px; height: 20px; flex: 0 0 20px; opacity: .9; }
.nav-item .badge-count {
  margin-left: auto; background: var(--surface-3); color: var(--text-2);
  font-size: 11px; padding: 1px 8px; border-radius: 20px; font-weight: 600;
}
.nav-item.active .badge-count { background: var(--brand-fill); color: #fff; }

/* ---- Department accordion (collapsible nav groups + landing hubs) ---- */
.nav-dept { margin: 2px 0; }
.nav-dept-head { display: flex; align-items: center; border-radius: 10px; }
.nav-dept-head:hover { background: var(--surface); }
.nav-dept-head.active { background: color-mix(in srgb, var(--brand), transparent 88%); }
.nav-dept-link {
  display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
  padding: 10px 12px; color: var(--text-2); font-weight: 600;
  transition: color .15s;
}
.nav-dept-link:hover { color: var(--text); }
.nav-dept-head.active .nav-dept-link { color: var(--brand); }
.nav-dept-ico { width: 20px; flex: 0 0 20px; display: grid; place-items: center; }
.nav-dept-ico .ico { width: 19px; height: 19px; opacity: .95; }
/* Wrap, never ellipsise: a primary-nav label with letters missing reads broken, and
   font metrics differ enough per OS that a nowrap label sized to fit here clips there. */
.nav-dept-label { overflow-wrap: break-word; line-height: 1.25; min-width: 0; }
.nav-dept-toggle {
  background: none; border: 0; color: var(--text-3); cursor: pointer;
  padding: 8px 4px; display: grid; place-items: center; border-radius: 8px;
  transition: transform .2s, color .15s;
}
.nav-dept-toggle:hover { color: var(--text); background: var(--surface-2); }
.nav-dept.open > .nav-dept-head .nav-dept-toggle { transform: rotate(90deg); }
/* Animate open/closed without knowing the child height (grid 0fr → 1fr). */
.nav-dept-kids { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .28s cubic-bezier(.33,1,.68,1); }
.nav-dept.open > .nav-dept-kids { grid-template-rows: 1fr; }
.nav-dept-kids-inner {
  overflow: hidden; margin: 2px 0 4px 24px; padding-left: 8px; border-left: 1px solid var(--border-soft);
  /* Ride along with the row-track animation: content drifts down and fades in as the
     track opens, instead of standing fully-formed inside a growing window. */
  opacity: 0; transform: translateY(-4px);
  transition: opacity .22s ease .06s, transform .28s cubic-bezier(.33,1,.68,1);
}
.nav-dept.open > .nav-dept-kids > .nav-dept-kids-inner { opacity: 1; transform: none; }
.nav-item.nav-child { padding: 8px 10px; font-size: 13px; margin: 1px 0; }
.nav-item.nav-child.active::before { left: -9px; }

/* Nested sub-accordion (e.g. Cargo Reference inside Cargo) — sits in the child list, so it
   indents relative to its parent's rail rather than starting a new one. */
.nav-subdept { margin: 2px 0; }
.nav-subdept > .nav-dept-head { padding-left: 2px; }
.nav-subdept > .nav-dept-head .nav-dept-label { font-size: 12.5px; }
.nav-subdept > .nav-dept-kids .nav-dept-kids-inner { margin-left: 14px; }
/* The whole head row is the accordion control now (open AND close), so it must read clickable. */
.nav-dept-head { cursor: pointer; }

/* ---- The rail: the sidebar collapsed to its icons -------------------------------------- */
/* 296px of navigation is a fifth of a 14" laptop, and an operations table is seventeen
   columns. Which of those matters more is not a property of the screen — it is a property of
   the shift. Someone learning the product needs the labels; someone who has read this nav
   four hundred times needs the width. So it is a toggle with a remembered answer, not a
   breakpoint that decides for them.
   The width lives in ONE place (`--sidebar-w`), so the rail is a variable override rather
   than a second set of layout rules to keep in step with the first. */
body.nav-rail { --sidebar-w: 72px; }

/* Everything that is words rather than a shape. The child lists go too: there is no width to
   render them at, and the category icon navigates to the department hub instead — see
   `toggleDept` in app.js, which stops toggling an accordion nobody can see. */
body.nav-rail .sidebar .brand-text,
body.nav-rail .sidebar .nav-item > span:not(.badge-count),
body.nav-rail .sidebar .nav-dept-label,
body.nav-rail .sidebar .nav-dept-toggle,
body.nav-rail .sidebar .nav-dept-kids,
body.nav-rail .sidebar .user-hub-who,
body.nav-rail .sidebar .user-hub-chev { display: none; }

body.nav-rail .sidebar .brand { padding: 20px 0 16px; justify-content: center; }
body.nav-rail .sidebar .nav { padding: 8px 10px; }
body.nav-rail .sidebar .nav-item,
body.nav-rail .sidebar .nav-dept-link { justify-content: center; gap: 0; padding: 11px 0; }
body.nav-rail .sidebar .nav-item.active::before { left: -10px; }
body.nav-rail .sidebar .user-hub { padding-left: 0; padding-right: 0; }
body.nav-rail .sidebar .user-hub-btn { justify-content: center; padding: 10px 0; }

/* A POPOVER IS NOT NAVIGATION, so it does not shrink with the rail. `.user-menu` is pinned
   `left:10px; right:10px` — the sidebar's own width — which is a comfortable card at 296px
   and about fifty pixels at 72: "Profile & account" wrapped over three lines, the email
   ellipsed to "j...", and the three appearance buttons ran off the edge. Everything else in
   this block is hidden because there is genuinely no width to render it at; a menu that
   floats has its own width and needs to step OUTSIDE the rail rather than be squeezed into
   it. Anchored to the hub's bottom so it still reads as belonging to the avatar it came from.
   `.sidebar` sets no `overflow`, which is what lets this leave. */
body.nav-rail .sidebar .user-menu {
  left: calc(100% + 8px); right: auto; width: 272px; bottom: 8px;
}

/* A workload count still has to reach the eye, and `margin-left:auto` would push it off a
   72px row. Ride it on the icon's corner instead — the same place a phone puts one. */
body.nav-rail .sidebar .nav-item { position: relative; }
body.nav-rail .sidebar .nav-item .badge-count {
  position: absolute; top: 4px; right: 10px; margin-left: 0;
  min-width: 16px; padding: 0 4px; font-size: 10px; line-height: 16px; text-align: center;
}

/* Styled ONLY here: at full width the child row already carries `.active`, and tinting its
   parent as well would highlight two rows for one destination. */
body.nav-rail .sidebar .nav-dept-head.holds { background: color-mix(in srgb, var(--brand), transparent 88%); }
body.nav-rail .sidebar .nav-dept-head.holds .nav-dept-link { color: var(--brand); }

/* THE LABEL THE RAIL TOOK AWAY. Fixed, not absolute: `.nav` scrolls, and a scroll container
   clips both axes the moment one of them is not `visible` — an `::after` on the row is drawn
   entirely inside the 72px column and never seen. Positioned from JS off the row's rect.

   Not `pointer-events: none` alone — the tip is never under the cursor, since it sits 10px
   clear of the sidebar's right edge; the property is there so a fast diagonal mouse cannot
   land on it and flicker the row's own mouseout. */
.nav-tip {
  position: fixed; z-index: 90; left: -9999px; top: -9999px;
  padding: 7px 11px; border-radius: 9px;
  background: var(--surface-3); color: var(--text);
  border: 1px solid var(--border-soft);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .38);
  font-size: 12.5px; font-weight: 600; line-height: 1.2; white-space: nowrap;
  pointer-events: none; opacity: 0; transition: opacity .1s ease;
}
.nav-tip.open { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .nav-tip { transition: none; } }
/* No flyout on a phone: there is no rail there, and a tip anchored to a drawer row would sit
   over the content the drawer is covering. */
@media (max-width: 900px) { .nav-tip { display: none; } }

@media (prefers-reduced-motion: reduce) { .sidebar { transition: none; } }

/* ------------------------------------------------------------- Busy overlay */
/* Blocks all interaction while a slow, non-repeatable action runs (e.g. sending an
   invite via an external SMTP relay). Above the modal layer so it also covers a form. */
.busy-overlay {
  position: fixed; inset: 0; z-index: 4000; display: grid; place-items: center;
  background: rgba(6, 10, 20, .62); backdrop-filter: blur(3px);
  opacity: 0; transition: opacity .16s ease; cursor: progress;
}
.busy-overlay.open { opacity: 1; }
.busy-card {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 26px 34px; border-radius: 16px; background: var(--surface);
  border: 1px solid var(--border); box-shadow: 0 18px 50px rgba(0,0,0,.45); min-width: 240px;
}
.busy-msg { font-size: 14.5px; font-weight: 650; color: var(--text); text-align: center; }
.busy-sub { font-size: 12px; color: var(--text-3); text-align: center; max-width: 280px; line-height: 1.5; }
@media (prefers-reduced-motion: reduce) { .busy-overlay { transition: none; } }

/* ------------------------------------------------ Customisable home dashboard */
/* 12-column block grid with a FIXED row unit, and every block explicitly placed.
   --x/--y are the block's 1-based column and row; --w/--h its span.

   Explicit placement is the whole point. Under auto-placement a tile's position was decided
   by the browser from the sizes of the tiles before it, so two tiles spanning different row
   counts left holes their owner could not close: measured on a real board, 16% of the grid
   was empty cells, and neither `grid-auto-flow: dense` nor a fixed row unit moved that
   number at all. The row unit has to be fixed for --y to mean anything — an auto-sized row
   is as tall as whatever landed in it, so "row 6" would be a different place on every board.

   The unit is 132px: one row holds a stat tile's chrome + big number + label + foot without
   clipping, which is what makes h:1 a usable size rather than a broken one. */
.dash-grid {
  display: grid; grid-template-columns: repeat(12, 1fr);
  grid-auto-rows: var(--dash-row, 132px); gap: 14px; align-items: stretch;
}
.dash-block {
  grid-column: var(--x, auto) / span var(--w, 3);
  grid-row: var(--y, auto) / span var(--h, 1);
  min-width: 0;
}
.dash-card {
  height: 100%; display: flex; flex-direction: column; background: var(--surface);
  border: 1px solid var(--border); border-radius: 14px; padding: 13px 15px; min-width: 0; overflow: hidden;
}
/* min-height matches .dash-tool's 22px: the head reserves room for the edit controls even when
   it is only showing a title, so entering and leaving edit mode does not change any tile's
   content height. Without it every tile is 9px shorter in view mode, each auto-sized row
   shrinks with it, and the board you arranged is not the board you get back. */
.dash-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex: 0 0 auto; min-height: 22px; }
.dash-title { font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dash-open { margin-left: auto; font-size: 11.5px; color: var(--brand); text-decoration: none; flex: 0 0 auto; }
.dash-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.dash-na { font-size: 12.5px; display: grid; place-items: center; flex: 1; }
.dash-foot { font-size: 11px; margin-top: 6px; }

/* stat */
.dash-stat { display: flex; flex-direction: column; justify-content: center; flex: 1; }
.dash-val { font-size: 30px; font-weight: 800; line-height: 1.1; }
.dash-lab { font-size: 12.5px; color: var(--text-2); margin-top: 2px; }

/* breakdown (labelled bars) */
.dash-bars { display: flex; flex-direction: column; gap: 9px; overflow: auto; flex: 1; }
.dash-bar-row { flex: 0 0 auto; }
.dash-bar-top { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }

/* list */
.dash-list { display: flex; flex-direction: column; gap: 2px; overflow: auto; flex: 1; }
.dash-li { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 8px; text-decoration: none; color: var(--text-2); font-size: 12.5px; }
a.dash-li:hover { background: var(--surface-2); color: var(--text); }
.dash-li-main { min-width: 0; display: flex; flex-direction: column; flex: 1; }
.dash-li-main b { font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dash-li-main span { font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* links (quick actions) */
.dash-links { display: flex; flex-wrap: wrap; gap: 7px; align-content: flex-start; overflow: auto; flex: 1; }
.dash-link { display: inline-flex; align-items: center; gap: 6px; padding: 7px 11px; border-radius: 9px; background: var(--surface-2); border: 1px solid var(--border-soft); color: var(--text-2); font-size: 12.5px; text-decoration: none; }
.dash-link:hover { background: var(--surface-3); color: var(--text); border-color: var(--border); }
.dash-link-ico { display: grid; place-items: center; }

/* loading skeleton */
.dash-card.loading { justify-content: center; }
.dash-skel { height: 30px; border-radius: 8px; background: linear-gradient(90deg, var(--surface-2), var(--surface-3), var(--surface-2)); background-size: 200% 100%; animation: dashSkel 1.1s linear infinite; }
@keyframes dashSkel { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.dash-empty { grid-column: span 12; }

/* edit mode */
.dash-grid.editing .dash-card { border-style: dashed; border-color: var(--brand); cursor: grab; }
.dash-block.dragging { opacity: .45; }
/* Where the tile will land. Occupies a real grid cell, so it shows the exact footprint rather
   than a hint — the point of storing positions is that the drop is aimed, not approximated. */
.dash-ghost {
  grid-column: var(--x, auto) / span var(--w, 3); grid-row: var(--y, auto) / span var(--h, 1);
  border: 2px dashed var(--brand); border-radius: 14px; background: color-mix(in srgb, var(--brand) 9%, transparent);
  pointer-events: none;
}
.dash-tools { margin-left: auto; display: flex; gap: 2px; flex: 0 0 auto; }
.dash-tool { width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--border-soft); background: var(--surface-2); color: var(--text-3); font-size: 12px; line-height: 1; cursor: pointer; display: grid; place-items: center; }
.dash-tool:hover { background: var(--surface-3); color: var(--text); }
.dash-tool.danger:hover { background: var(--danger-soft); color: var(--danger-ink); border-color: var(--danger); }

/* add-tile picker */
.dash-pick { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; }
.dash-pick-item { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); cursor: pointer; text-align: left; }
.dash-pick-item:hover { border-color: var(--brand); background: var(--surface-3); }
.dash-pick-item b { font-size: 13px; }
.dash-pick-item span.muted { font-size: 11px; }
.dash-pick-item.on { border-color: var(--ok); }

/* Narrow screens drop back to FLOW and ignore --x/--y: a board arranged for 12 columns has
   positions that are meaningless at 6, and honouring them would leave a column of holes.
   Heights stay auto here so nothing is clipped on a phone. */
@media (max-width: 1100px) {
  .dash-grid { grid-auto-rows: minmax(var(--dash-row, 132px), auto); }
  .dash-block { grid-column: auto / span min(var(--w, 3), 6); grid-row: auto / span var(--h, 1); }
}
@media (max-width: 760px) {
  .dash-grid { grid-template-columns: repeat(6, 1fr); }
  .dash-block { grid-column: auto / span 6; grid-row: auto / span var(--h, 1); }
}

/* --------------------------------------------- User hub (bottom-left, Discord-style) */
.user-hub { position: relative; padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border-soft); }
.user-hub-btn {
  width: 100%; display: flex; align-items: center; gap: 9px; padding: 7px 8px; border-radius: 10px;
  background: none; border: 1px solid transparent; color: var(--text); cursor: pointer; text-align: left;
}
.user-hub-btn:hover { background: var(--surface-2); border-color: var(--border-soft); }
.user-hub-avatar {
  width: 32px; height: 32px; border-radius: 50%; flex: 0 0 auto; display: grid; place-items: center;
  background: var(--brand-fill); color: #fff; font-size: 12px; font-weight: 800; overflow: hidden;
}
.user-hub-avatar img { width: 100%; height: 100%; object-fit: cover; }
.user-hub-who { min-width: 0; flex: 1; display: flex; flex-direction: column; line-height: 1.2; }
.user-hub-who b { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-hub-who span { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-hub-chev { color: var(--text-3); display: grid; place-items: center; transform: rotate(-90deg); flex: 0 0 auto; }
.user-menu {
  position: absolute; bottom: calc(100% - 2px); left: 10px; right: 10px; z-index: 60;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 16px 40px rgba(0,0,0,.34); padding: 6px; animation: umIn .12s ease-out;
}
@keyframes umIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.user-menu-head { padding: 9px 10px 8px; border-bottom: 1px solid var(--border-soft); margin-bottom: 5px; display: flex; flex-direction: column; }
.user-menu-head b { font-size: 13px; }
.user-menu-head span { font-size: 11.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-menu-item {
  display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 10px; border-radius: 8px;
  background: none; border: 0; color: var(--text-2); font-size: 13px; cursor: pointer; text-decoration: none; text-align: left;
}
.user-menu-item:hover { background: var(--surface-2); color: var(--text); }
.user-menu-item.danger:hover { background: var(--danger-soft); color: var(--danger-ink); }
.user-menu-theme { padding: 8px 10px 4px; border-top: 1px solid var(--border-soft); margin-top: 5px; }
.user-menu-theme-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); }
.tsegs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 6px; }
.tseg { padding: 6px 4px; border-radius: 8px; border: 1px solid var(--border-soft); background: var(--surface-2); color: var(--text-3); font-size: 11.5px; font-weight: 600; cursor: pointer; }
.tseg:hover { color: var(--text); }
.tseg.active { background: var(--brand-fill); border-color: var(--brand); color: #fff; }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ------------------------------------------------------------- Top bar */
.topbar {
  min-height: 64px; display: flex; align-items: center; gap: 16px;
  /* top inset keeps the header clear of the notch / Dynamic Island (viewport-fit=cover) */
  padding: calc(env(safe-area-inset-top, 0px) + 6px) 24px 6px; border-bottom: 1px solid var(--border-soft);
  background: rgba(14, 19, 32, 0.85); backdrop-filter: blur(8px);
  position: sticky; top: 0; z-index: 20;
}
/* the title block shrinks + ellipsises instead of wrapping a long module blurb
   and shoving the search / user pill (which were getting squished) */
.topbar-title { min-width: 0; flex: 0 1 auto; }
.topbar h1 { font-size: 18px; margin: 0; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar .sub { color: var(--text-3); font-size: 12.5px; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar .spacer { flex: 1; min-width: 8px; }
.scan-btn, .bell-btn, .theme-btn, .agent-pill { flex-shrink: 0; }

.global-search {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; height: var(--control-h); padding: 0 12px; width: 320px; max-width: 34vw;
}
.global-search input { background: none; border: none; color: var(--text); outline: none; width: 100%; font-size: 13px; }
.global-search input::placeholder { color: var(--text-3); }


.agent-pill {
  display: flex; align-items: center; gap: 10px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 30px; padding: 5px 14px 5px 5px;
}
.avatar { width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--info), var(--purple)); display: grid; place-items: center; font-weight: 700; color: #fff; font-size: 13px; }
.agent-pill .who { line-height: 1.2; display: flex; flex-direction: column; }
.agent-pill .who b { font-size: 13px; display: block; }
.agent-pill .who span { font-size: 11px; color: var(--text-3); display: block; margin-top: 1px; }

/* --------------------------------------------------------------- Content */
.content { padding: 24px; flex: 1; }
.page-head { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
.page-head h2 { margin: 0; font-size: 22px; font-weight: 680; }
.page-head p { margin: 4px 0 0; color: var(--text-3); font-size: 13px; }

/* ---------------------------------------------------------------- Cards */
.grid { display: grid; gap: 16px; }
.cols-2 { grid-template-columns: repeat(2, 1fr); }
.cols-3 { grid-template-columns: repeat(3, 1fr); }
.cols-4 { grid-template-columns: repeat(4, 1fr); }
.cols-5 { grid-template-columns: repeat(5, 1fr); }
@media (max-width: 1200px) { .cols-4 { grid-template-columns: repeat(2, 1fr); } .cols-3 { grid-template-columns: repeat(2, 1fr); }
  .cols-5 { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 760px) { .grid[class*="cols"] { grid-template-columns: 1fr; } }

.card {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--radius); padding: 22px; box-shadow: var(--shadow-sm);
}
.card.pad-0 { padding: 0; overflow: hidden; }
.card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.card-head h3 { margin: 0; font-size: 15px; font-weight: 640; }
.card-head .spacer { flex: 1; }
.card-sub { color: var(--text-3); font-size: 12px; }
/* pad-0 cards (PRM flights/agents boards) had ZERO head padding on desktop — the
   title was jammed into the top-left corner and the buttons into the top-right,
   flush to the card edges. Give the head the same gutter the app's other card
   headers use. (The ≤640px block below keeps its own tighter mobile gutter.) */
.card.pad-0 > .card-head { padding: 14px 16px 6px; }

/* KPI */
.kpi { position: relative; overflow: hidden; }
/* A LINKED KPI SAYS SO AT REST. `a.card` further down already lifts on hover, and the comment
   there is right that a lift means "this does something" — but hover is not an affordance on a
   tablet, and in a row of six tiles three link and three cannot. A quiet chevron marks the ones
   that do, in an icon from the registry so it takes `currentColor` and follows the theme. */
.kpi.linked { color: inherit; text-decoration: none; display: block; }
.kpi-go { position: absolute; right: 14px; bottom: 12px; color: var(--text-3); opacity: .5;
  display: grid; place-items: center;
  transition: opacity .16s var(--ease), transform .16s var(--ease); }
.kpi-go .ic-inline { width: 16px; height: 16px; margin-right: 0; }
.kpi.linked:hover .kpi-go { opacity: 1; transform: translateX(2px); }
.kpi .label { color: var(--text-3); font-size: 12.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .6px; }
.kpi .value { font-size: 34px; font-weight: 760; margin: 8px 0 2px; line-height: 1; }
.kpi .value .unit { font-size: 15px; font-weight: 600; color: var(--text-3); letter-spacing: 0; }
.kpi .delta { font-size: 12.5px; font-weight: 600; }
.kpi .ico-wrap { position: absolute; right: 16px; top: 16px; width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center; }
/* The chip's icon fills the chip, not the 1.05em an inline icon gets in running text. */
.kpi .ico-wrap .ic-inline { width: 22px; height: 22px; margin-right: 0; }
.kpi .foot { color: var(--text-3); font-size: 12px; margin-top: 6px; }

/* --------------------------------------------------------------- Badges */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: 20px; font-size: 11.5px; font-weight: 650;
  white-space: nowrap; text-transform: capitalize;
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.ok { background: var(--ok-soft); color: var(--ok-ink); }
.badge.warn { background: var(--warn-soft); color: var(--warn-ink); }
.badge.danger { background: var(--danger-soft); color: var(--danger-ink); }
/* Composer reach line: a measured zero is a refusal to send, not a styling nicety. */
.reach-none { color: var(--danger-ink); font-weight: 600; }
.badge.info { background: var(--info-soft); color: var(--info-ink); }
.badge.purple { background: var(--purple-soft); color: var(--purple-ink); }
.badge.teal { background: var(--teal-soft); color: var(--teal-ink); }
.badge.grey { background: var(--surface-3); color: var(--text-2); }
.badge.brand { background: var(--brand-soft); color: var(--brand-ink); }
.badge.accent { background: var(--accent-soft); color: var(--accent-ink); }

/* Case-type tags */
.tag-type { font-family: var(--mono); font-weight: 700; font-size: 11px; padding: 2px 7px; border-radius: 6px; letter-spacing: .5px; white-space: nowrap; }
.tag-AHL { background: var(--danger-soft); color: var(--danger-ink); }
.tag-OHD { background: var(--info-soft); color: var(--info-ink); }
.tag-DPR { background: var(--warn-soft); color: var(--warn-ink); }
/* Pilferage reads as theft, not wear — it gets the danger palette rather than DPR's amber
   so the two are not mistaken for each other at a glance on a mixed queue. */
.tag-PIL { background: var(--danger-soft); color: var(--danger-ink); }
.tag-QUICK_RUSH { background: var(--purple-soft); color: var(--purple-ink); }
.tag-FP { background: var(--teal-soft); color: var(--teal-ink); }
.tag-BDO { background: var(--accent-soft); color: var(--accent-ink); }

/* --------------------------------------------------------------- Tables */
.table-wrap { overflow-x: auto; }

/* ---- RECORD CARDS — the module list on a phone ------------------------------------
   The desktop table is right at desktop density and unusable at 375px: a dozen columns
   dragged sideways to read. Same rows, same filters, same drawer — a shape you can read
   with one thumb. See Views.module.drawCards. */
.rec-cards { display: flex; flex-direction: column; gap: 10px; }
.rec-card {
  width: 100%; text-align: left; display: block; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 13px 14px; color: var(--text); -webkit-tap-highlight-color: transparent;
}
.rec-card:active { transform: scale(.995); }
.rc-top { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 9px; }
.rc-id { flex: 1; min-width: 0; }
.rc-title { display: block; font-size: 16px; font-weight: 650; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rc-ref { display: block; font-size: 13px; color: var(--text-3); margin-top: 2px; }
/* Two across: a label/value pair needs the width, and three columns truncates every value. */
.rc-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; margin-bottom: 10px; }
.rc-f { min-width: 0; }
.rc-f i { display: block; font-style: normal; font-size: 13px; color: var(--text-3); line-height: 1.2; }
.rc-f b { display: block; font-size: 14.5px; font-weight: 600; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rc-foot { display: flex; align-items: center; gap: 8px; }
.rc-when { margin-left: auto; font-size: 13px; white-space: nowrap; }
/* A long single value (a description) gets the full width rather than half of it. */
.rc-fields .rc-f:only-child { grid-column: 1 / -1; }

@media (max-width: 760px) {
  /* The record toolbar is a desktop toolbar: segments, a search box, several dropdowns,
     bulk-edit and a view toggle, all on one line. Let it stack, give the search the full
     width it needs, and drop the Table/Board toggle — on a phone the answer is cards
     either way, so offering the choice is offering a lie. */
  .filters .global-search { width: 100% !important; order: -1; }
  .filters .loc-filter { flex: 1 1 44%; min-width: 0; }
  .filters .spacer { display: none; }
  .filters .seg.view-toggle { display: none; }
}

/* Generic detail table (contract drawer, sub-lists, etc.). Previously unstyled —
   cells fell back to the browser's ~1px default, which read as cramped. */
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { text-align: left; font-weight: 640; color: var(--text-3); font-size: 11px;
  text-transform: uppercase; letter-spacing: .5px; padding: 11px 15px;
  border-bottom: 1px solid var(--border); white-space: nowrap; }
.table td { padding: 12px 15px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--surface-2); }
table.data { width: 100%; border-collapse: collapse; font-size: 13px; }
table.data th {
  text-align: left; padding: 12px 16px; color: var(--text-3);
  font-weight: 600; font-size: 11.5px; text-transform: uppercase; letter-spacing: .6px;
  border-bottom: 1px solid var(--border); white-space: nowrap; position: sticky; top: 0;
  background: var(--surface);
}
/* Sortable column headers. The engine could not sort at all — every module list was
   newest-first and nothing else was reachable — and clicking a header is the habit every
   incumbent in this market trains (Cargospot binds Ctrl+S to "sort by the focused field").
   Keyboard-reachable, because a header only a mouse can use re-creates the problem one
   step along. */
table.data th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
table.data th.sortable:hover { color: var(--text); }
table.data th.sortable:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
table.data th.sortable.sorted { color: var(--brand-ink); }
table.data th .sort-ar { font-size: 9px; margin-left: 4px; vertical-align: 1px; }
table.data td { padding: 14px 18px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }

/* ONE LINE PER CELL, BECAUSE ONE CELL WAS SETTING THE HEIGHT OF THE WHOLE ROW.
   A turnaround's IATA delay reason is a sentence — "87 Airport facilities, parking stands,
   ramp congestion, gate limitations" — and it wrapped to five lines, so seven rows filled a
   screen that holds twenty. The horizontal scroll on a seventeen-column operations table is
   the lesser problem; the vertical space was the expensive one.

   THE CLAMP IS ON A SPAN, NOT ON THE `td`. `max-width` on a table cell is ignored under
   `table-layout: auto`, which this table uses — the browser sizes columns from content and the
   declaration does nothing. An inner block-level span is the element the width can actually
   apply to.

   NOTHING IS LOST: the renderer puts the full value in the cell's `title`, so it is one hover
   away, and the record drawer shows it in full. */
table.data td > .cell {
  display: block; max-width: 26ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A cell that is already a control, a badge or a chip sizes itself — clamping those would cut
   a status pill in half. Only free text is clamped. */
table.data td > .cell.wide { max-width: 44ch; }

/* AN ANCHOR WHEN THE TABLE IS SCROLLED SIDEWAYS. Seventeen columns do not fit a 14" screen at
   a readable size, and shrinking them is the wrong trade for a screen read at a glance in a
   bright ramp office. What makes the scroll navigable rather than disorienting is keeping the
   record's identity in view: scrolled right, every row was otherwise an anonymous grid of
   times.

   The background is opaque and matches the row, or the scrolled content shows through it. The
   hover and zebra rules below re-state it for the same reason. */
table.data th.stick, table.data td.stick {
  position: sticky; left: 0; z-index: 2;
  background: var(--surface);
  /* Hairline AND a soft drop: at any scroll offset SOME column is half-covered by this one,
     and without the depth cue a clipped word ("...ESTONE") reads as corrupt data rather than
     as content passing underneath. */
  box-shadow: 1px 0 0 var(--border-soft), 6px 0 8px -6px rgba(0, 0, 0, .35);
}
table.data tbody tr:hover td.stick { background: var(--surface-2); }
/* THE ACTION IS PINNED TO THE OTHER EDGE, for the same reason and the opposite direction. The
   Team register runs eight columns; measured at 1280px the "Create account" button — the whole
   remedy for the gap the screen exists to flag — sat at x=1476, so the reader had to scroll
   right past every row to reach it and lost the name they were acting on. Its shadow points
   the other way because the content passes underneath from the left. */
table.data th.stick-r, table.data td.stick-r {
  position: sticky; right: 0; z-index: 2;
  background: var(--surface);
  box-shadow: -1px 0 0 var(--border-soft), -6px 0 8px -6px rgba(0, 0, 0, .35);
}
table.data tbody tr:hover td.stick-r { background: var(--surface-2); }
/* A PERSON WHO CANNOT SIGN IN IS A GAP, NOT A ROW. Staff sign in to the handheld to work their
   duties, so an account is what makes a roster entry operable — the register marks the ones
   that are not, quietly enough to scan past twenty of them and clearly enough to act on one.
   `--warn-fill` rather than `--warn`: every small use goes through the ink/fill pair, because
   the bare hue does not carry on this surface. */
table.data tbody tr.gap-row td:first-child { box-shadow: inset 3px 0 0 var(--warn-fill); }
/* The row hover rule below is unconditional and says "clickable". A login with no person behind
   it has nothing to open, so it must not claim otherwise. */
table.data tbody tr.not-clickable { cursor: default; }
table.data tbody tr { transition: background .12s; }
table.data tbody tr:hover { background: var(--surface-2); cursor: pointer; }
table.data tbody tr:last-child td { border-bottom: none; }
/* Compact in-card table used by module custom widgets (catalogue, ledgers, boards).
   Fills its card on desktop; wrap it in .table-wrap (overflow-x:auto) so wide
   tables scroll inside the card on narrow/mobile viewports instead of pushing
   the whole page sideways. */
table.tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
table.tbl th {
  text-align: left; padding: 8px 12px; color: var(--text-3);
  font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  border-bottom: 1px solid var(--border); white-space: nowrap;
}
table.tbl td { padding: 9px 12px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
table.tbl tbody tr { transition: background .12s; }
table.tbl tbody tr:hover { background: var(--surface-2); }
table.tbl tbody tr:last-child td { border-bottom: none; }
.mono { font-family: var(--mono); font-size: 12.5px; }
/* A reference in a table cell ("QRH-260808-61140") breaks at its hyphens when the
   browser squeezes the column, splitting an ID a person needs to read as one token.
   The wrap belongs to .table-wrap's horizontal scroll, not inside the reference. */
td.mono, th.mono { white-space: nowrap; }
.muted { color: var(--text-3); }
.nowrap { white-space: nowrap; }

/* --------------------------------------------------------------- Buttons */
.btn {
  display: inline-flex; align-items: center; gap: 8px; justify-content: center;
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  height: var(--control-h); padding: 0 16px;
  border-radius: 10px; font-weight: 600; font-size: 13px;
  transition: background .15s, border-color .15s, transform .05s;
}
.btn:hover { background: var(--surface-3); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--brand-fill); border-color: var(--brand); color: #fff; }
/* A COUNT INSIDE A FILLED BUTTON CANNOT KEEP ITS OWN INK. `.badge.warn` is amber on a
   translucent amber wash — right on a card, and on a filled primary the wash goes blue and
   leaves amber text on blue. The gap chips carry their count that way, so clicking "cannot
   sign in" made the number it exists to state the least readable thing on it. Any tone badge
   inside a primary button borrows the button's own colours instead, and keeps its pill shape
   from a border rather than a fill.

   THE FILL IS `--brand-fill` AND NOT A TRANSLUCENT WASH, which is what this first shipped as.
   White on `rgba(255,255,255,.22)` cannot be measured — the answer depends on what is behind
   it — and `tests/test_contrast.py` scored it 3.20, correctly. White on `--brand-fill` is the
   button's own pairing, already tuned to clear AA in both themes. */
.btn.primary .badge { background: var(--brand-fill); color: #fff;
  border: 1px solid rgba(255, 255, 255, .5); }
.btn.primary:hover { background: var(--brand-dark); }
.btn.ok { background: var(--ok); border-color: var(--ok); color: var(--ok-on); }
.btn.ghost { background: transparent; }
/* The compact and large variants set their own height rather than inheriting the
   toolbar one — they exist precisely to break out of that rhythm (inline row actions,
   primary calls to action). */
.btn.sm { height: 28px; padding: 0 11px; font-size: 12px; }
/* One step below `sm`, for a button that lives in a table cell. Written in commercial and PRM
   fourteen times and defined nowhere, so every one of them rendered at the full 34px default —
   a row of chunky buttons down the right-hand edge of a data table. */
.btn.xs { height: 24px; padding: 0 9px; font-size: 11.5px; }
.btn.lg { height: 44px; padding: 0 22px; font-size: 15px; }
.btn:disabled { opacity: .5; pointer-events: none; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; }

/* --------------------------------------------------------------- Forms */
/* THE BASE RULE IS ON THE ELEMENT, NOT A CLASS.
   Only `.field input` was styled, so a control was themed if — and only if — someone
   remembered to wrap it in a .field. 634 inputs, selects and textareas across the views
   carry no class at all, and every one of them was rendering as a raw browser control:
   native border, native height, square corners, ignoring the theme entirely. Two of the
   six that DID carry `class="input"` were reaching for a class that has never existed.
   Styling the element fixes all of them at once and makes the next one correct by
   default. Everything that already styles inputs (.field, .input-affix, .sla-row,
   body.hh-mode) is a class or descendant selector, so it still outranks this. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]),
select, textarea, .input {
  font: inherit; font-size: 13.5px; font-family: inherit;
  color: var(--text); background: var(--bg-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0 12px; height: var(--control-h);
  outline: none; min-width: 0; max-width: 100%;
  transition: border-color .15s, box-shadow .15s, background .15s;
}
/* A textarea is the one control that must NOT be locked to the toolbar height. */
textarea { height: auto; min-height: 76px; padding: 9px 12px; line-height: 1.5; resize: vertical; }
input:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled) {
  border-color: var(--text-3);
}
/* Focus reads as a soft ring in the brand colour rather than the UA's hard outline —
   the same language the buttons use. */
input:focus, select:focus, textarea:focus, .input:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}
input:disabled, select:disabled, textarea:disabled {
  opacity: .55; cursor: not-allowed; background: var(--surface-3);
}
input::placeholder, textarea::placeholder { color: var(--text-3); }

/* A value a MACHINE read, which nobody has confirmed yet.
   The document scanner used to invent a passenger name and passport number and prefill this
   form with them at 99% confidence — indistinguishable, on screen, from something the agent
   had typed. A scanned value now looks unconfirmed until the agent touches the field, and
   the tint is on the CONTROL rather than on a class the markup has to remember, because
   600+ inputs across the views carry no class at all. */
input.from-scan, select.from-scan, textarea.from-scan {
  border-color: var(--warn);
  background: var(--warn-soft);
}
input.from-scan:focus, select.from-scan:focus, textarea.from-scan:focus {
  border-color: var(--warn);
  box-shadow: 0 0 0 3px var(--warn-soft);
}
/* Native chevron differs per OS and ignores the theme; draw our own, as .field select
   already did, and reserve the room for it. */
select {
  appearance: none; -webkit-appearance: none; cursor: pointer;
  /* line-height 1 clipped descenders inside the fixed-height control */
  padding: 0 32px 0 12px; line-height: normal;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 1px), calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select option { background: var(--surface); color: var(--text); }
/* THE TOGGLES ARE DRAWN HERE, NOT BY THE OS.
   `accent-color` paints the CHECKED fill and nothing else, so an unchecked box kept the
   platform's own chrome: a white square with a grey hairline on a #141b2d surface, at
   whatever size and corner radius the OS felt like, and a different shape on macOS,
   Windows and Android. On the Access dialog that is a column of forty of them, none of
   them ours. Drawing the box is the only way to theme the UNchecked state, which is the
   state most of them are in.
   ON THE ELEMENT, not a class: 73 of these across twenty views, in twenty-two different
   label shapes, and fifteen of them are a bare <label>. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  /* flex:none or a flex label squashes the box to nothing when its text wraps —
     which is what the Locations/ports list does at every width. */
  flex: none; display: inline-block; box-sizing: border-box;
  width: 16px; height: 16px; margin: 0; padding: 0;
  border: 1px solid var(--border); border-radius: 5px;
  background: var(--bg-2) no-repeat center center;
  /* Sits on the text baseline. Without this the box rides high against its own label
     in every non-flex <label>, which is most of them. */
  vertical-align: -3px;
  cursor: pointer;
  transition: background-color .14s, border-color .14s, box-shadow .14s;
}
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:hover:not(:disabled), input[type="radio"]:hover:not(:disabled) {
  border-color: var(--text-3);
}
/* --brand-fill rather than --brand: this is a solid fill under a white mark, which is the
   exact pairing that token is tuned for (white on it 4.65 dark / 4.67 light). White on
   --brand measures 2.84 and would miss even the 3:1 non-text floor. Same pairing as
   .btn.primary, so a ticked box and the primary button read as the same affordance. */
input[type="checkbox"]:checked, input[type="radio"]:checked,
input[type="checkbox"]:indeterminate {
  background-color: var(--brand-fill); border-color: var(--brand);
}
/* The tick is a drawn path, not a ✓ — a glyph renders as a different shape per OS and
   carries the OS's own colour. Inline SVG here rather than UI.ic(): CSS is the only layer
   that can reach the pseudo-content of a form control, and this one mark is never themed
   (it only ever sits on --brand-fill). */
input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.6 8.4l2.9 2.9 5.9-6.4'/%3E%3C/svg%3E");
  background-size: 15px 15px;
}
/* A PARTLY-ticked parent is not the same as an unticked one. `access.js` sets
   .indeterminate on every permission-group head, and with the native control gone an
   unstyled indeterminate box reads as OFF — telling the admin nothing under it is granted
   when half of it is. */
input[type="checkbox"]:indeterminate {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.3' stroke-linecap='round'%3E%3Cpath d='M4.2 8h7.6'/%3E%3C/svg%3E");
  background-size: 15px 15px;
}
input[type="radio"]:checked {
  background-image: radial-gradient(circle at center, #fff 0 3px, transparent 3.5px);
}
/* The generic input:disabled rule below repaints the background, which would wipe the
   fill off a ticked-but-locked box — a read-only settings form would show every one of
   its enabled options as OFF. Dim it instead, and keep the mark. */
input[type="checkbox"]:disabled, input[type="radio"]:disabled { opacity: .5; cursor: not-allowed; }
input[type="checkbox"]:disabled:not(:checked):not(:indeterminate),
input[type="radio"]:disabled:not(:checked) { background-color: var(--surface-3); }
/* Keyboard focus keeps the ring the rest of the form uses; a mouse click does not, because
   a clicked box already shows its own state change. */
input[type="checkbox"]:focus:not(:focus-visible), input[type="radio"]:focus:not(:focus-visible) {
  box-shadow: none;
}
/* A label wrapping a toggle lines itself up — every one of the twenty-two shapes at once,
   including the fifteen bare <label>s that had the box jammed against its own text with no
   gap. A label that already sets its own flex gap (.perm-leaf, .req-toggle, .choice, …)
   is a class rule and still outranks the gap here. */
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  /* The global `label` rule sets 600, which is right for a FIELD label naming the control
     under it and wrong for the option text beside a box — forty bold role names in the
     Access dialog with nothing to be bold against. */
  font-weight: 500;
}
input[type="range"] { accent-color: var(--brand); }
input[type="file"] { font-size: 12.5px; color: var(--text-2); }
/* Compact variant, for controls sharing a row with .btn.sm */
.input.sm, input.sm, select.sm { height: 28px; font-size: 12.5px; padding-left: 10px; }
select.sm { padding-right: 28px; }
.input.lg, input.lg, select.lg { height: 44px; font-size: 15px; }

.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 6px; font-weight: 600; }
/* NOT the toggles. `width:100%` on a checkbox is what forced six class rules and thirty-two
   inline `style="width:auto"` attributes across twelve views to claw the size back — and each
   of those then fought the toggle sizing above. Excluding them here is the root of it, so
   the escapes could all be deleted and the next .field checkbox is correct by default. */
.field input:not([type="checkbox"]):not([type="radio"]), .field select, .field textarea {
  width: 100%; background: var(--bg-2); border: 1px solid var(--border);
  color: var(--text); border-radius: 10px; padding: 10px 12px; font-size: 13.5px; outline: none;
  font-family: inherit;
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--brand); }
.field textarea { resize: vertical; min-height: 70px; }
/* A native select adds its own internal metrics on top of the padding above, so in a
   stacked form it sat 2px taller than the inputs either side of it. Same treatment as the
   toolbar dropdowns: fix the height, draw our own chevron, and it lines up. */
.field select {
  appearance: none; -webkit-appearance: none;
  height: 38px; padding: 0 32px 0 12px; line-height: normal; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 1px), calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.field select option { background: var(--surface); color: var(--text); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* iOS Safari auto-zooms the whole page in when a focused field has a font-size
   below 16px, and it never zooms back out — that's what left the app "slightly
   zoomed" after the (13.5px) login form on first sign-in, on every iPhone. Pin
   every text field to 16px on phones so the browser has no reason to zoom.
   Desktop keeps the denser design; checkboxes/radios/ranges are exempt. */
@media (max-width: 760px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select, textarea { font-size: 16px !important; }
}
/* NOT `.field .req` — the required asterisk is written the same way inside a `.field`
   wrapper and outside one (module.js's location and title fields, the person picker in
   components.js, the add-a-person modal), and scoping it to the wrapper rendered those
   as a plain black star while every other required field showed a bold red one. The
   class means the same thing wherever it appears, so it is styled wherever it appears. */
.req { color: var(--danger); font-weight: 700; }
.field-help { font-size: 11.5px; color: var(--text-3); margin-top: 5px; }
/* Type-ahead suggestion menu (e.g. the ITAM check-out holder search) */
.typeahead-menu { position: absolute; left: 0; right: 0; top: 100%; z-index: 40; margin-top: 4px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow); max-height: 240px; overflow-y: auto; }
.typeahead-item { padding: 8px 12px; cursor: pointer; font-size: 13.5px; line-height: 1.3;
  border-bottom: 1px solid var(--border-soft); }
.typeahead-item:last-child { border-bottom: 0; }
.typeahead-item:hover { background: var(--surface-3); }
.field.invalid input, .field.invalid select, .field.invalid textarea { border-color: var(--danger); }
.input-affix { display: flex; align-items: stretch; background: var(--bg-2); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.input-affix:focus-within { border-color: var(--brand); }
.input-affix input { border: none; background: transparent; border-radius: 0; }
.input-affix .affix { display: flex; align-items: center; padding: 0 11px; font-size: 13px; color: var(--text-3); background: var(--surface-2); }
.input-affix .affix.suffix { border-left: 1px solid var(--border); }

/* Inline icon (UI.ic) — sits in a line of text where an emoji used to. Sized in `em` so
   it tracks whatever it is set in, and optically nudged up a hair because a 24-box icon
   centred on the baseline reads low next to capitals. Takes currentColor, so it inherits
   the state of the thing it labels — an emoji never could. */
.ic-inline { display: inline-flex; align-items: center; justify-content: center;
  width: 1.05em; height: 1.05em; vertical-align: -.14em; margin-right: .42em; flex: 0 0 auto; }
/* An icon standing ALONE — in a stepper bead, a badge with no following text — where the
   trailing margin that separates an icon from its label would instead push it off-centre. */
.ic-inline.ic-bare { margin-right: 0; }
.ic-inline > svg { width: 100%; height: 100%; display: block; }
/* The nav icons carry .ico, which the sidebar sizes in px — reset that inside .ic-inline
   so an inline icon scales with its text rather than staying nav-sized. */
.ic-inline > svg.ico { width: 100%; height: 100%; }
.ic-inline:last-child { margin-right: 0; }

/* --------------------------------------------------------------- Filters */
.filters { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }
.seg { display: inline-flex; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; max-width: 100%; height: var(--control-h); }
/* On narrow screens a long tab bar (e.g. PRM's 5 tabs) scrolls instead of clipping. */
@media (max-width: 760px) {
  .seg { display: flex; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  .seg::-webkit-scrollbar { display: none; }
  .seg button { flex: 0 0 auto; white-space: nowrap; }
}
.seg button { background: none; border: none; color: var(--text-2); padding: 0 14px; font-size: 12.5px; font-weight: 600; }
.seg button.active { background: var(--brand-fill); color: #fff; }
.seg button:not(:last-child) { border-right: 1px solid var(--border); }

/* --------------------------------------------------------------- Drawer */
.drawer-backdrop {
  position: fixed; inset: 0; background: rgba(4, 7, 14, 0.6);
  backdrop-filter: blur(2px); z-index: 50; opacity: 0; pointer-events: none;
  transition: opacity .2s;
}
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
/* Width scales with the viewport instead of being fixed: 560px forced a two-column detail
   grid into ~250px columns, so values wrapped under their own labels and any table inside
   overflowed. clamp() gives a phone the full width, a laptop a comfortable read, and a wide
   monitor more without ever becoming an unreadable line length. */
.drawer {
  position: fixed; top: 0; right: 0; height: 100vh;
  width: clamp(560px, 46vw, 880px); max-width: 100vw;
  background: var(--bg-2); border-left: 1px solid var(--border); z-index: 51;
  transform: translateX(100%); transition: transform .25s ease; overflow-y: auto;
  box-shadow: -20px 0 50px rgba(0,0,0,.4);
}
.drawer.open { transform: translateX(0); }
/* Full-height drawer sits under the iOS status bar on mobile — inset the header top
   so the record title/close button clear the clock & battery (needs viewport-fit=cover;
   env() is 0 on desktop, so no change there). */
.drawer-head { position: sticky; top: 0; background: var(--bg-2); padding: calc(20px + env(safe-area-inset-top, 0px)) 24px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; z-index: 2; }
.drawer-head .close { margin-left: auto; width: 34px; height: 34px; border-radius: 9px; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-2); display: grid; place-items: center; font-size: 18px; }
.drawer-body { padding: 28px 32px 40px; }
/* Any table inside a drawer scrolls ITSELF rather than pushing the drawer wide. The SLA
   terms table ran its PENALTY column off the edge with no way to reach it. The negative
   margin lets the scroll area use the drawer's full width while the text keeps its inset. */
/* DIRECT CHILDREN ONLY. The negative margin here is a BLEED: it cancels `.drawer-body`'s own
   32px padding so a wide table can run to the drawer's edges instead of being squeezed into
   the prose column. That is only true of a wrap the drawer itself contains.
   Nested in a `.card.pad-0` — which is where every sub-entity table on the platform lives —
   it instead pushed the wrap 32px past the card on each side, and `.card` has
   `overflow: hidden` for its rounded corners, so the overhang was CUT OFF. Measured in the
   Load Control drawer: a 715px wrap inside a 651px card, clipping the last column of the
   table (`Status`) off the right-hand edge on every module that declares sub-entities.
   The child combinator keeps the bleed where it was designed and stops it where it wasn't. */
.drawer-body > .table-wrap, .drawer-body > .scroll-x {
  overflow-x: auto; margin: 0 -32px; padding: 0 32px;
  -webkit-overflow-scrolling: touch;
}
.drawer-body table { min-width: 100%; }
/* Sections inside a drawer need separation, or Overview/Commercial/Term run together. */
.drawer-body h3, .drawer-body .sec-title { margin: 32px 0 14px; font-size: 13px;
  font-weight: 700; letter-spacing: .02em; }
.drawer-body > :first-child { margin-top: 0; }

/* Modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(4,7,14,.65); backdrop-filter: blur(3px); z-index: 60; display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; opacity: 0; pointer-events: none; transition: opacity .2s; padding: 20px; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
/* min-width:0 + the constrained grid column above keep max-width:100% honest, so
   the modal never exceeds the viewport (was overflowing to its 560px on phones). */
.modal { background: var(--bg-2); border: 1px solid var(--border); border-radius: 16px; width: 560px; max-width: 100%; min-width: 0; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow); }
.modal-head { padding: 20px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
.modal-head .close { margin-left: auto; width: 32px; height: 32px; border-radius: 8px; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-2); display: grid; place-items: center; font-size: 17px; }
.modal-body { padding: 24px; }
.modal-foot { padding: 16px 24px; border-top: 1px solid var(--border); display: flex; gap: 10px; justify-content: flex-end; }

/* Bag-tag print preview.
   The tag is 51 x 533 mm — 1:10.45 — and nothing renders that shape whole at a readable
   width inside a 560px modal, so the preview is a WINDOW onto the strip: about actual size
   across, scrolling down the length.
   The wrap HUGS that window. It used to span the full modal, so a 200px preview sat in a
   510px box with 155px of dead grey either side — which is what read as the pdf window not
   being scaled to the tag.
   `align-items: flex-start` is a guard, not decoration: a flex container's default `stretch`
   sets the item's CROSS size, and would silently discard a height or an `aspect-ratio` set
   on the preview below. */
.tag-preview-wrap { display: flex; justify-content: center; align-items: flex-start;
  width: max-content; max-width: 100%; margin: 0 auto;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
/* Height is bounded so the MODAL never scrolls: two nested scrollbars — the modal's and the
   PDF viewer's own — is what makes a long tag feel broken rather than long. 46vh keeps the
   whole dialog inside its 90vh at a 800px laptop viewport, where 52vh did not. */
.tag-preview { width: 200px; height: min(46vh, 520px); border: 0; border-radius: 4px; background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,.35); }

/* ----------------------------------------------------------- Timeline */
.timeline { position: relative; padding-left: 28px; }
.timeline::before { content: ""; position: absolute; left: 9px; top: 4px; bottom: 4px; width: 2px; background: var(--border); }
.tl-item { position: relative; padding-bottom: 18px; }
.tl-item::before { content: ""; position: absolute; left: -23px; top: 3px; width: 12px; height: 12px; border-radius: 50%; background: var(--info); border: 3px solid var(--bg-2); }
.tl-item.ok::before { background: var(--ok); }
.tl-item.warn::before { background: var(--warn); }
.tl-item.brand::before { background: var(--brand); }
.tl-item .tl-type { font-weight: 650; font-size: 13px; }
.tl-item .tl-meta { color: var(--text-3); font-size: 11.5px; margin-top: 2px; }
.tl-item .tl-note { color: var(--text-2); font-size: 12.5px; margin-top: 4px; }

/* ------------------------------------------------------------ Misc */
/* `minmax` lets a long label take the room it needs instead of wrapping the VALUE, and the
   floor keeps short labels aligned. Row gap opened up: at 10px the rows read as one block. */
.kv { display: grid; grid-template-columns: minmax(140px, max-content) 1fr;
      gap: 14px 20px; font-size: 13px; align-items: start; }
.kv .k { color: var(--text-3); }
.kv .v { color: var(--text); font-weight: 500; }
/* A grouping sub-heading inside the record drawer / a form (e.g. "Device details"). */
.kv .detail-section { grid-column: 1 / -1; margin-top: 6px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text-3);
  border-top: 1px solid var(--border, rgba(148,163,184,.2)); padding-top: 10px; }

/* THE RECORD DRAWER'S DETAIL BLOCK.
   `.kv` above is a two-column list, which is right for the six-row summaries in prm and the
   handheld and wrong for what the generic module drawer does with it: turnaround declares 40
   fields, load control 41, pax disruption 41. Populated, that is one undifferentiated column
   832px tall in a 653px-wide drawer — a label, a 20px gutter the eye has to jump, a value,
   twenty-nine times, with nothing marking where one reading ends and the next begins. It is
   the screen this platform is most often read from and it was the least readable thing in it.

   So each field becomes a SELF-CONTAINED cell, label over value, and the cells flow into
   however many columns the drawer is actually wide enough for. Three things follow:
     * the label/value pairing is proximity, not alignment across a gutter, so there is
       nothing to track and no gutter to mis-jump;
     * 40 fields become ~14 rows instead of 40, which is the difference between a screen and
       a scroll;
     * `auto-fit` means the drawer at its 560px floor gets two columns and at 880px gets
       three, with no breakpoint to keep in sync with the drawer's own clamp().
   `min(100%, …)` rather than a bare floor: without it a cell wider than its container
   overflows instead of wrapping, which is what a long value in a narrow drawer does. */
.detail-grid { display: grid; gap: 15px 26px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }
.detail-grid .kv-pair { min-width: 0; }
.detail-grid .k { font-size: 11.5px; line-height: 1.35; color: var(--text-3); margin-bottom: 2px; }
/* tabular-nums so a column of times, minutes and dollar amounts lines up on its digits —
   most of what a turnaround or a cargo record holds is a number. */
.detail-grid .v { font-size: 13.5px; line-height: 1.4; color: var(--text); font-weight: 500;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.detail-grid .detail-section { grid-column: 1 / -1; margin-top: 8px; font-size: 10.5px;
  font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-3);
  border-top: 1px solid var(--border); padding-top: 12px; }
.detail-grid > .detail-section:first-child { margin-top: 0; border-top: 0; padding-top: 2px; }
/* Coordinator notes, a mishandling narrative, a customer's own words. A paragraph in a 200px
   column is a ladder; it takes the full width and reads as prose. */
.detail-grid .kv-pair.wide { grid-column: 1 / -1; }
.detail-grid .kv-pair.wide .v { font-weight: 400; color: var(--text-2); line-height: 1.55;
  white-space: pre-wrap; }
/* THE ONE DEFINITION. There were two, at equal specificity, 662 lines apart — an 11px muted
   uppercase rule with a top border, and a 14px --text rule with a BOTTOM border — so source
   order merged them into a heading nobody designed: 14px, uppercase, letter-spaced, in
   --text, with a rule above AND below it and padding on both. The same shape of collision
   `.btn.ok.ghost` had, and `tests/test_style_hygiene.py` now gates it.
   The surviving design is the drawer's: `.detail-section` renders the same spec sections in
   the record drawer, and a heading should not change identity between reading a record and
   editing it. */
.form-section-head { margin: 18px 0 6px; font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-3); border-top: 1px solid var(--border);
  padding-top: 12px; }
.form-section-head:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
.section-title { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-3); margin: 22px 0 10px; font-weight: 700; }
.divider { height: 1px; background: var(--border-soft); margin: 18px 0; }
.empty { text-align: center; padding: 50px 20px; color: var(--text-3); }
.empty .big { font-size: 40px; margin-bottom: 8px; opacity: .5; }
/* The empty state now draws a flat icon instead of a 40px emoji. font-size sizes a glyph, not
   an SVG, so the icon needs its own dimensions — and centring, because the emoji was inline
   text and the svg is a block. Kept at 40px so the state is the same visual weight it was. */
.empty .big svg { width: 40px; height: 40px; display: block; margin: 0 auto; stroke-width: 1.4; }
.flex { display: flex; align-items: center; gap: 10px; }
.flex.between { justify-content: space-between; }
.flex.wrap { flex-wrap: wrap; }

/* THE PADDED BODY OF A `card pad-0`. `.pad` was written in three views and defined in none of
   them, so every one of those blocks sat flush against the card border — the same shape of bug
   as `.row`, and invisible to a suite that reads markup. Named for what it is so the next
   person finds it: a card whose padding was zeroed to let a table reach the edges still needs
   somewhere to put prose. (`.ps-card-body` is the tighter variant tuned for a tab body and is
   deliberately not merged into this — different padding, different job.) */
.card-body { padding: 22px; }
.card-body.tight { padding: 14px 22px; }
.stack { display: flex; flex-direction: column; gap: 4px; }
.right { text-align: right; }
.pill-bar { height: 8px; border-radius: 6px; background: var(--surface-3); overflow: hidden; }
.pill-bar > span { display: block; height: 100%; border-radius: 6px; background: var(--brand); }

/* Toast */
.toasts { position: fixed; bottom: 24px; right: 24px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
.toast { background: var(--surface-2); border: 1px solid var(--border); border-left: 4px solid var(--info); border-radius: 10px; padding: 12px 16px; min-width: 280px; box-shadow: var(--shadow); animation: slidein .25s ease; }
.toast.ok { border-left-color: var(--ok); }
.toast.warn { border-left-color: var(--warn); }
.toast.danger { border-left-color: var(--danger); }
.toast b { display: block; font-size: 13px; }
.toast span { font-size: 12px; color: var(--text-2); }
@keyframes slidein { from { transform: translateX(40px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

/* Funnel */
.funnel { display: flex; flex-direction: column; gap: 8px; }
.funnel-step { display: flex; align-items: center; gap: 12px; }
.funnel-bar { height: 38px; border-radius: 9px; background: linear-gradient(90deg, var(--brand), var(--brand-dark)); display: flex; align-items: center; padding: 0 14px; color: #fff; font-weight: 700; min-width: 60px; transition: width .4s; }
.funnel-label { width: 160px; flex: 0 0 160px; font-size: 12.5px; color: var(--text-2); }

/* Phone mockup (mobile agent app) */
.phone-stage { display: flex; gap: 30px; flex-wrap: wrap; align-items: flex-start; }
.phone {
  width: 380px; flex: 0 0 380px; background: #0a0f1c; border: 10px solid #05080f;
  border-radius: 40px; padding: 0; box-shadow: var(--shadow); overflow: hidden;
  position: relative; max-width: 100%;
}
.phone-notch { height: 28px; background: #05080f; display: grid; place-items: center; }
.phone-notch::after { content: ""; width: 120px; height: 18px; background: #000; border-radius: 0 0 14px 14px; }
.phone-screen { height: 720px; overflow-y: auto; background: var(--bg); }
.phone-statusbar { display: flex; justify-content: space-between; padding: 8px 18px; font-size: 11px; color: var(--text-2); }
.phone-app-head { padding: 12px 18px; background: linear-gradient(135deg, var(--brand), var(--brand-dark)); color: #fff;
  display: flex; flex-direction: column; gap: 2px; }
.phone-app-head b { font-size: 16px; line-height: 1.2; }
.phone-app-head span { font-size: 11px; opacity: .85; }
.phone-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 14px; }
.phone-tile { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 14px; padding: 14px 8px; text-align: center; transition: transform .1s, border-color .15s; }
.phone-tile:hover { border-color: var(--brand); transform: translateY(-2px); }
.phone-tile .ti { font-size: 22px; }
.phone-tile .tl { font-size: 11px; font-weight: 700; margin-top: 6px; }
.phone-body { padding: 14px; }

/* loading */
.skeleton { background: linear-gradient(90deg, var(--surface) 25%, var(--surface-2) 37%, var(--surface) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 8px; }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
.spinner { width: 18px; height: 18px; border: 2px solid var(--border); border-top-color: var(--brand); border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.center-load { display: grid; place-items: center; padding: 60px; }

/* Bars — a fixed-height PLOT (value + bar) sits above the label so every bar shares
   one baseline regardless of how many lines the label wraps to. align-items:flex-start
   keeps the plots top-aligned; the label area grows downward, never pushing the bar. */
.barchart { display: flex; align-items: flex-start; gap: 10px; padding: 12px 4px 4px; }
/* min-width:0 lets bars shrink below their label's intrinsic width; without it a
   long category label ("Personal injury / WHS") forces each bar to its text width
   and the whole chart overflows narrow / mobile viewports. */
.barchart .bar { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; }
.barchart .bar .plot { height: 150px; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 6px; }
.barchart .bar .col { width: 70%; max-width: 42px; border-radius: 7px 7px 0 0; background: linear-gradient(180deg, var(--info), var(--brand-dark)); min-height: 4px; transition: height .4s; }
/* Label: a centred (vertically + horizontally) box below the baseline, with room for
   ~2 lines and padding so wrapped labels breathe. overflow-wrap:anywhere (not
   break-word) also shrinks the label's min-content width, which is what lets the
   min-width:0 flex bars shrink below the longest word instead of overflowing. */
.barchart .bar .lbl { font-size: 10.5px; color: var(--text-3); width: 100%; text-align: center;
  line-height: 1.25; overflow-wrap: anywhere; min-height: 2.7em; padding-top: 8px;
  display: flex; align-items: center; justify-content: center; }
.barchart .bar .val { font-size: 11px; font-weight: 700; }

/* Horizontal bars — the default: label on the left at full width (no wrapping games),
   a track that fills to the right, value at the end. Reads cleanly for long category
   names and any number of categories. */
.barchart.h { display: flex; flex-direction: column; gap: 9px; padding: 8px 2px 4px; height: auto; align-items: stretch; }
.barchart.h .hbar { display: flex; align-items: center; gap: 10px; min-width: 0; }
.barchart.h .hlbl { flex: 0 0 38%; max-width: 165px; font-size: 11.5px; color: var(--text-2);
  text-align: right; line-height: 1.25; overflow-wrap: break-word; }
.barchart.h .htrack { flex: 1 1 auto; min-width: 0; height: 16px; border-radius: 5px; overflow: hidden;
  background: var(--panel-2, rgba(148, 163, 184, .15)); }
.barchart.h .hfill { height: 100%; border-radius: 5px; min-width: 3px; transition: width .35s; }
.barchart.h .hval { flex: 0 0 auto; min-width: 30px; font-size: 11.5px; font-weight: 700; color: var(--text); }

.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--text-2); }
.legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: 5px; vertical-align: middle; }

.btn.danger { background: var(--danger-fill); border-color: var(--danger); color: #fff; }
.btn.danger:hover { background: #dc2626; }
/* GHOST AND DANGER TOGETHER MEANT NEITHER. `.btn.ghost` sets a transparent background at line
   705 and `.btn.danger` sets a filled red one here, same specificity and later in the file —
   so it won, and all thirty-six `btn ghost danger` buttons across the platform rendered as
   solid red blocks. On the passenger-services roster that is one per row: a wall of red down
   the right edge of a read-only table, shouting louder than the records it belongs to.
   `.btn.danger-soft` already exists and says exactly this in its own comment; the markup
   simply never reached it, because the class everybody actually writes is `ghost danger`.
   Given as its own specificity-3 rule rather than by reordering: `.btn.danger` alone must
   keep the fill, which is right for a confirmed delete in a modal footer. */
.btn.ghost.danger {
  background: transparent; border-color: var(--danger, #dc4646); color: var(--danger, #dc4646);
}
.btn.ghost.danger:hover { background: var(--danger-soft, rgba(220, 70, 70, .12)); }

/* THE SAME COLLISION, ON THE OTHER SIDE. `.btn.ok` (704) and `.btn.ghost` (705) are equal
   specificity, so `class="btn ok ghost"` took ghost's transparent background and KEPT ok's
   `color:#042a12` — a near-black green chosen to sit on a bright green fill, now sitting on
   nothing. The Pass button on the session gradebook reads as black text in a green outline.
   State the combination rather than leave it to source order. */
.btn.ghost.ok {
  background: transparent; border-color: var(--ok); color: var(--ok);
}
.btn.ghost.ok:hover { background: var(--ok-soft, rgba(34, 160, 90, .12)); }

/* Kanban board */
.kanban { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(220px, 1fr); gap: 14px; overflow-x: auto; padding-bottom: 8px; align-items: start; }
.kan-col { background: var(--bg-2); border: 1px solid var(--border-soft); border-radius: 12px; padding: 10px; }
.kan-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px 10px; font-size: 13px; font-weight: 650; }
.kan-cards { display: flex; flex-direction: column; gap: 8px; min-height: 24px; }
.kan-card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 10px; padding: 10px; cursor: pointer; transition: border-color .12s, transform .08s; }
.kan-card:hover { border-color: var(--brand); transform: translateY(-2px); }

/* Departure board (turnaround live) */
.depboard { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.dep-card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 12px; padding: 14px; cursor: pointer; transition: border-color .12s, transform .08s; }
.dep-card:hover { border-color: var(--brand); transform: translateY(-2px); }
.dep-card.delayed { border-color: var(--danger); }
.dep-flight { font-size: 20px; font-weight: 800; font-family: var(--mono); letter-spacing: 1px; }
.dep-time { font-size: 18px; font-weight: 700; font-family: var(--mono); }
.dep-steps { display: flex; gap: 4px; margin-top: 12px; }
.dep-step { flex: 1; height: 6px; border-radius: 4px; background: var(--surface-3); }
/* IROPs recovery cockpit */
.iro-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 14px; }
.iro-card { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--warn); border-radius: 12px; padding: 14px; cursor: pointer; transition: border-color .12s, transform .08s; }
.iro-card:hover { transform: translateY(-2px); }
.iro-card.urgent { border-left-color: var(--danger); }
.iro-flight { font-size: 16px; font-weight: 800; font-family: var(--mono); letter-spacing: 1px; }
.cockpit-alert { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger-soft); }
.dep-step.done { background: var(--brand); }
/* There is no `.dep-step.active` any more. It painted the one step matching the record's
   "Current milestone" select, and the bar is now proportional segments counted from the
   milestone sub-records — where "in progress" has two writers that disagree about what it
   means, so it is counted as still-to-do. See backend/turnaround_progress. */
/* Not applicable — a step somebody cancelled with a reason, not one still to do. Hatched
   rather than a fourth colour: the difference has to survive a colour-blind reader and a
   greyscale print of the board, and on a 6px bar a hue shift does neither. */
.dep-step.na { background: repeating-linear-gradient(45deg, var(--surface-3) 0 3px, var(--border) 3px 6px); }

/* Milestone tracker (turnaround detail) */
.milestone-track { display: flex; flex-wrap: wrap; gap: 6px; }
.ms-node { flex: 1; min-width: 64px; text-align: center; cursor: pointer; padding: 6px 2px; border-radius: 8px; transition: background .12s; }
.ms-node:hover { background: var(--surface-3); }
.ms-dot { width: 26px; height: 26px; border-radius: 50%; margin: 0 auto 5px; display: grid; place-items: center; font-size: 12px; font-weight: 700; background: var(--surface-3); color: var(--text-3); }
.ms-node.done .ms-dot { background: var(--ok); color: var(--ok-on); }
.ms-node.active .ms-dot { background: var(--info-fill); color: #fff; box-shadow: 0 0 0 4px var(--info-soft); }
.ms-label { font-size: 10.5px; font-weight: 600; }
.ms-time { font-size: 10px; }

/* Risk meter (miles fraud) */
.risk-meter { height: 8px; border-radius: 6px; background: var(--surface-3); overflow: hidden; display: inline-block; width: 90px; vertical-align: middle; margin-right: 6px; }
.risk-meter > div { height: 100%; border-radius: 6px; }

/* Approval chain (procurement) */
.approval-chain { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.ac-step { display: flex; align-items: center; gap: 8px; }
.ac-dot { width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; background: var(--surface-3); color: var(--text-3); }
.ac-step.done .ac-dot { background: var(--ok); color: var(--ok-on); }
.ac-line { flex: 1; height: 2px; background: var(--border); min-width: 16px; }

/* Composer device preview (communications) */
.cmp-device { border: 8px solid #05080f; border-radius: 24px; overflow: hidden; background: var(--bg); }
.cmp-bar { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: var(--surface-2); font-size: 12px; }
.cmp-screen { padding: 16px; min-height: 220px; }
.cmp-subj { font-size: 16px; font-weight: 700; margin-bottom: 10px; }
.cmp-body { font-size: 13px; color: var(--text-2); line-height: 1.5; white-space: pre-wrap; }
.cmp-foot { margin-top: 18px; padding-top: 12px; border-top: 1px solid var(--border-soft); font-size: 11px; color: var(--text-3); }

/* Suite launcher */
.suite-group { margin-bottom: 22px; }
.suite-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.suite-card { display: flex; flex-direction: column; gap: 8px; background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 18px; transition: transform .12s, border-color .15s, box-shadow .15s; }
.suite-card:hover { transform: translateY(-3px); border-color: var(--brand); box-shadow: var(--shadow); }
.suite-ico { font-size: 30px; color: var(--text-2); }
.suite-ico .ico { width: 27px; height: 27px; }
.page-head h2 .ico { width: 22px; height: 22px; vertical-align: -4px; margin-right: 4px; color: var(--text-2); }
.suite-name { font-weight: 680; font-size: 15px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.suite-blurb { color: var(--text-3); font-size: 12.5px; line-height: 1.45; flex: 1; }
.suite-foot { color: var(--brand); font-size: 12px; font-weight: 600; margin-top: 4px; }
.suite-count { background: var(--surface-3); color: var(--text); padding: 1px 8px; border-radius: 20px; font-weight: 700; }

/* Reports */
.report-card { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; transition: border-color .15s, transform .1s; }
.report-card:hover { border-color: var(--brand); transform: translateY(-2px); }
.report-ico { font-size: 28px; }
.report-name { font-weight: 680; font-size: 15px; }
.report-desc { color: var(--text-3); font-size: 12.5px; line-height: 1.45; flex: 1; }
.report-card .btn { margin-top: 4px; }

/* Subscription / à-la-carte */
.sub-cat { margin-bottom: 18px; }
.prod-card { display: flex; flex-direction: column; gap: 7px; transition: border-color .15s, transform .1s; }
.prod-card.on { border-color: var(--brand-soft); }
.prod-ico { font-size: 26px; }
.prod-name { font-weight: 680; font-size: 14px; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.prod-desc { color: var(--text-3); font-size: 12px; line-height: 1.45; flex: 1; }
.prod-price { font-size: 13px; color: var(--text-2); margin-top: 2px; }
.prod-price b { font-size: 17px; color: var(--text); }
.suite-card.upsell { border-style: dashed; opacity: .92; }
.suite-card.upsell:hover { opacity: 1; }

/* Toggle switch */
.toggle { position: relative; display: inline-block; width: 42px; height: 24px; cursor: pointer; flex: 0 0 42px; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle .track { position: absolute; inset: 0; background: var(--surface-3); border-radius: 24px; transition: background .15s; }
.toggle .track::before { content: ""; position: absolute; left: 3px; top: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform .15s; }
.toggle input:checked + .track { background: var(--brand); }
.toggle input:checked + .track::before { transform: translateX(18px); }
.toggle.disabled { opacity: .5; cursor: not-allowed; }

/* Federation */
.fed-card { transition: transform .12s, border-color .15s; }
.fed-card:hover { border-color: var(--brand); transform: translateY(-2px); }
.fed-avatar { width: 40px; height: 40px; border-radius: 11px; background: linear-gradient(135deg, var(--brand), var(--purple)); display: grid; place-items: center; font-weight: 700; color: #fff; font-size: 14px; }
.fed-foot { color: var(--brand); font-size: 12.5px; font-weight: 600; margin-top: 12px; }
.share-head, .share-row { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 10px; align-items: center; }
.share-head { padding: 0 4px 8px; font-size: 12px; }
.share-table { border: 1px solid var(--border-soft); border-radius: 12px; overflow: hidden; }
.share-row { padding: 11px 12px; border-bottom: 1px solid var(--border-soft); }
.share-row:last-child { border-bottom: none; }
.share-row:nth-child(even) { background: var(--surface); }
.share-dom { font-size: 13px; font-weight: 600; }
.share-side { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; }
.share-side.them { opacity: .92; }
.share-chip { font-size: 10.5px; font-weight: 700; padding: 2px 7px; border-radius: 6px; background: var(--surface-3); color: var(--text-3); border: 1px solid transparent; }
.share-chip.on { background: var(--ok-soft); color: var(--ok-ink); border-color: rgba(34,197,94,.3); }
.share-edit { padding: 2px 8px !important; font-size: 11px !important; }
.share-cat { border-bottom: 1px solid var(--border); }
.share-cat:last-child { border-bottom: none; }
.share-cat-head { display: flex; align-items: center; justify-content: space-between; padding: 9px 12px; background: var(--surface-2); font-size: 12px; }
.cat-bulk { display: flex; gap: 4px; }
.cat-bulk button { background: var(--surface-3); border: 1px solid var(--border); color: var(--text-2); font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 6px; }
.cat-bulk button:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.cat-bulk button.clear:hover { background: var(--danger-soft); color: var(--danger-ink); border-color: var(--danger); }
.preset-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.fld-note { font-size: 10px; color: var(--accent); font-weight: 700; cursor: help; }
.fld-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; max-height: 230px; overflow-y: auto; padding: 4px 2px; }
.fld-check { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-2); padding: 4px 0; cursor: pointer; }
@media (max-width: 560px) { .fld-grid { grid-template-columns: 1fr; } .cat-bulk { flex-wrap: wrap; } }
.switch-row { display: flex; gap: 10px; align-items: flex-start; padding: 9px 0; font-size: 13px; color: var(--text-2); cursor: pointer; }
.switch-row input { margin-top: 2px; }
.switch-row b { color: var(--text); }
.shared-item { margin-bottom: 12px; }
.shared-ico { width: 40px; height: 40px; flex: 0 0 40px; border-radius: 11px; background: var(--surface-3); display: grid; place-items: center; font-size: 19px; }
.pl-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.pl-chip { font-size: 11px; background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 7px; padding: 3px 8px; color: var(--text-2); }
.pl-chip b { color: var(--text-3); font-weight: 600; text-transform: capitalize; }

@media (max-width: 560px) { .share-head, .share-row { grid-template-columns: 1fr; gap: 4px; } .share-side.them { margin-top: 4px; } }

/* Notification bell */
.bell-btn, .theme-btn { position: relative; background: var(--surface); border: 1px solid var(--border); color: var(--text-2); width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; }
.bell-btn:hover, .theme-btn:hover { background: var(--surface-2); color: var(--text); }

/* Zulu clock — the reference every schedule, telex and milestone on this platform is
   stated in. Deliberately quiet: it is a constant, not an alert, so it reads as
   instrument panel rather than notification.
   tabular-nums is load-bearing — proportional digits reflow the whole field every
   second and the clock visibly jitters. */
.zulu { flex-shrink: 0; display: flex; flex-direction: column; justify-content: center;
  align-items: flex-start; gap: 2px; padding: 0 11px; height: 40px; border-radius: 10px;
  background: var(--surface); border: 1px solid var(--border);
  user-select: none; cursor: default; }
/* Time and date share a left edge and the same mono face, so the columns line up and the
   date reads as a caption of the time rather than a second, competing field. */
.zulu .z-time { display: flex; align-items: baseline; gap: 2px; line-height: 1; }
.zulu .z-hms { font-family: var(--mono); font-size: 14.5px; font-weight: 600;
  letter-spacing: .3px; color: var(--text); font-variant-numeric: tabular-nums; }
.zulu .z-z { font-family: var(--mono); font-size: 10px; font-weight: 700; color: var(--brand); }
.zulu .z-date { font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .3px;
  color: var(--text-3); white-space: nowrap; line-height: 1; font-variant-numeric: tabular-nums; }
.bell-badge { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 10px; background: var(--brand-fill); color: #fff; font-size: 11px; font-weight: 700; display: grid; place-items: center; box-shadow: 0 0 0 2px var(--bg); }
.bell-dd { position: fixed; top: calc(60px + env(safe-area-inset-top, 0px)); right: 24px; width: 360px; max-width: 92vw; max-height: 70vh; overflow-y: auto; background: var(--bg-2); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); z-index: 70; opacity: 0; transform: translateY(-6px); transition: opacity .15s, transform .15s; }
.bell-dd.open { opacity: 1; transform: translateY(0); }
.bell-dd-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--border-soft); position: sticky; top: 0; background: var(--bg-2); }
.bell-dd-head a { color: var(--brand); font-size: 12.5px; font-weight: 600; }
.bell-dd-list { padding: 6px; }
.bell-item { display: flex; gap: 11px; padding: 10px 22px 10px 10px; border-radius: 10px; position: relative; }
.bell-item:hover { background: var(--surface); }
/* Read vs unread — an unread alert must be obvious at a glance. Unread keeps full-strength
   text + a subtle tint + a brand dot; a read alert recedes (dimmed icon, lighter text, no dot)
   so the eye lands on what still needs attention. Class is applied in App.toggleBell(). */
.bell-item.unread { background: color-mix(in srgb, var(--brand) 8%, transparent); }
.bell-item.unread:hover { background: color-mix(in srgb, var(--brand) 14%, transparent); }
.bell-item.unread::after {
  content: ""; position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
  width: 8px; height: 8px; border-radius: 50%; background: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
.bell-item:not(.unread) .bell-ico { opacity: .45; }
.bell-item:not(.unread) .bell-line b { font-weight: 500; color: var(--text-2); }
.bell-item:not(.unread) .bell-msg { color: var(--text-3); }
.bell-item:not(.unread) { opacity: .78; }
.bell-ico { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 9px; display: grid; place-items: center; font-size: 16px; background: var(--surface-3); }
.bell-ico.ok { background: var(--ok-soft); } .bell-ico.warn { background: var(--warn-soft); } .bell-ico.info { background: var(--info-soft); }
.bell-ico.brand { background: var(--brand-soft); } .bell-ico.purple { background: var(--purple-soft); } .bell-ico.teal { background: var(--teal-soft); }
.bell-body { min-width: 0; flex: 1; }
.bell-line { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; }
.bell-line .muted { font-size: 11px; white-space: nowrap; }
.bell-msg { font-size: 12px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Logout button in agent pill */
.agent-pill .logout-btn { background: none; border: none; color: var(--text-3); padding: 6px; border-radius: 8px; display: grid; place-items: center; margin-left: 4px; }
.agent-pill .logout-btn:hover { background: var(--surface-3); color: var(--text); }

/* ---------------------------------------------------------- Auth screen */
/* The team search that every "add to roster" door now opens with. Capped and scrollable for
   the same reason the training transcript search is: at five thousand people an uncapped list
   is a page, not a picker. */
.person-pick { position: relative; }
.pick-hits {
  margin-top: 6px; max-height: 232px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.pick-hit {
  display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: baseline;
  width: 100%; text-align: left; padding: 9px 12px; background: none; border: 0;
  border-bottom: 1px solid var(--border-soft); color: var(--text); cursor: pointer;
  font: inherit;
}
.pick-hit:last-child { border-bottom: 0; }
.pick-hit:hover, .pick-hit:focus-visible { background: var(--surface-3); }
.pick-name { font-weight: 600; }
.pick-meta { color: var(--text-3); font-size: 12px; }
.pick-none { padding: 12px; color: var(--text-3); font-size: 12.5px; }
.pick-chosen {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.pick-chosen .btn { margin-left: auto; }

body.auth-mode { overflow: hidden; }
.auth-screen { position: fixed; inset: 0; z-index: 100; display: grid; grid-template-columns: 1fr 1fr; background: var(--bg); }
/* Horizontal padding is capped (was a raw 8vw): the card is max-width:620, so on
   an ultrawide monitor 8vw would eat the whole card and squish the form to a sliver. */
.auth-card { display: flex; flex-direction: column; justify-content: center; padding: 48px clamp(24px, 8vw, 56px); max-width: 620px; width: 100%; margin: 0 auto; }
.auth-brand { display: flex; flex-direction: column; gap: 8px; margin-bottom: 28px; }
.auth-tagline { font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-3); }
/* NO SIGNAGE CONFIGURED -> ONE CENTRED CARD, not a form stretched across the display.
   The panel beside the form used to pitch the product to the people who already work here;
   with it gone the grid collapses to a single column, and the card takes a surface, a border
   and a real maximum width so it reads as a deliberate object rather than as a two-column
   layout whose right half failed to load. On an ultrawide monitor a full-width sign-in form
   is the thing that looks broken. */
.auth-screen.auth-solo { grid-template-columns: 1fr; place-items: center; padding: 24px; }
.auth-solo .auth-card {
  max-width: 424px; padding: 40px; margin: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: var(--shadow);
}
/* The card is already a bounded surface, so the screen behind it carries the depth. */
.auth-screen.auth-solo { background: radial-gradient(1200px 600px at 50% -10%, var(--bg-2), var(--bg) 70%); }
@media (max-width: 520px) {
  /* On a phone the card IS the screen — a border and a shadow around something that fills
     the viewport is just a line down each edge. */
  .auth-screen.auth-solo { padding: 0; }
  .auth-solo .auth-card { border: 0; border-radius: 0; box-shadow: none; background: none;
                          max-width: none; padding: 32px 24px; }
}
.auth-sub { color: var(--text-3); font-size: 13px; margin: -4px 0 18px; }
.auth-note { background: var(--warn-soft); color: var(--warn-ink); padding: 10px 14px; border-radius: 10px; font-size: 12.5px; margin-bottom: 18px; }
.auth-error { display: none; background: var(--danger-soft); color: var(--danger-ink); border: 1px solid var(--danger);
  padding: 10px 13px; border-radius: 10px; font-size: 13px; margin: 0 0 14px; }
/* An error box is visible when it HAS SOMETHING TO SAY — not when someone also remembered
   the `.show` class. Five boxes across four files set .textContent and never added it, so
   every failure path behind them was silent: a wrong current password on the account screen,
   a failed OCR extraction, a dashboard-template save, and two settings dialogs all wrote
   their message into a display:none panel and told the user nothing at all.
   `.show` is kept because ~29 call sites correctly set it and there is no reason to churn
   them; the :not(:empty) rule simply makes forgetting it harmless. Safe against every
   existing hide path — all three of them clear textContent BEFORE removing the class, so a
   box that should be hidden is empty and stays hidden. */
.auth-error.show,
.auth-error:not(:empty) { display: block; }
@keyframes auth-shake { 0%,100%{transform:translateX(0)} 15%,55%{transform:translateX(-7px)} 35%,75%{transform:translateX(7px)} }
.auth-card.shake { animation: auth-shake .4s ease; }
.auth-hint { margin-top: 16px; font-size: 12.5px; color: var(--text-3); text-align: center; }
.auth-hint b { color: var(--text-2); font-family: var(--mono); font-size: 12px; }
.auth-or { display: flex; align-items: center; gap: 10px; margin: 16px 0; color: var(--text-3); font-size: 11.5px; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }
/* A WHITE BUTTON NEEDS A BORDER THAT IS NOT ALSO WHITE. This was `border-color: #fff`, which
   read as a button only because the surface behind it was always dark; on the light theme's
   white card it vanished into the background and left an icon and a line of text floating in
   the middle of the form. #8C8C8C is Microsoft's own specified border for the light variant of
   the sign-in button, so the fix is the brand guidance rather than a guess. */
.sso-btn { background: #fff !important; color: #1b1b1b !important; border-color: #8c8c8c !important; font-weight: 600; }
.sso-btn:hover { background: #eee !important; border-color: #6e6e6e !important; }

.auth-aside { background: linear-gradient(150deg, #0c1322, #122036 60%, #0e1a2c); display: flex; align-items: center; padding: 48px 5vw; border-left: 1px solid var(--border-soft); position: relative; overflow: hidden; }
.auth-aside::after { content: ""; position: absolute; width: 460px; height: 460px; border-radius: 50%; background: radial-gradient(circle, var(--brand-soft), transparent 70%); right: -120px; top: -80px; }
.auth-aside-inner { position: relative; z-index: 1; max-width: 460px; }
.auth-aside-inner h1 { font-size: 30px; line-height: 1.25; margin: 0 0 24px; }
.auth-aside-inner .grad { background: linear-gradient(90deg, var(--brand), var(--accent)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.auth-aside-inner ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 14px; }
.auth-aside-inner li { color: var(--text-2); font-size: 13.5px; padding-left: 26px; position: relative; }
.auth-aside-inner li::before { content: "✓"; position: absolute; left: 0; top: 0; color: var(--accent); font-weight: 800; }
.auth-aside-inner li b { color: var(--text); }
.auth-aside-logo { margin: 0 0 30px; }
.auth-aside-sub { color: #aeb9d4; font-size: 14px; line-height: 1.65; margin: 0; max-width: 420px; }
.auth-aside-foot { margin-top: 30px; font-size: 11.5px; color: var(--text-3); letter-spacing: .5px; }

@media (max-width: 880px) {
  .auth-screen { grid-template-columns: 1fr; }
  .auth-aside { display: none; }
}

/* Topbar extras */
.hamburger { display: grid; place-items: center; background: none; border: none; color: var(--text-2); padding: 6px; border-radius: 8px; }
.hamburger:hover { background: var(--surface-2); }
/* Scan is hidden on the desktop shell — a mouse-and-keyboard user has no camera workflow, so
   it was dead top-bar space. Re-shown at the mobile breakpoint (see the 900px block below).
   Only the TOPBAR instance is gated: the mobile home's own big Scan tile and the bottom-nav
   Scan are separate elements and stay as they are. */
.topbar .scan-btn { display: none; }
/* On a phone the search, clock and bell leave ~120px for the greeting, which ellipsised
   to a meaningless "Good …" on every screen. The page body says where you are and the
   account footer says who — drop the pleasantry rather than truncate it. */
@media (max-width: 560px) { .topbar-title { display: none; } }
.scan-btn { gap: 7px; }
.nav-backdrop { position: fixed; inset: 0; background: rgba(4,7,14,.55); z-index: 25; opacity: 0; pointer-events: none; transition: opacity .2s; }
.nav-backdrop.open { opacity: 1; pointer-events: auto; }

/* Mobile bottom nav */
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; min-height: 64px; z-index: 40;
  background: rgba(14,19,32,.96); backdrop-filter: blur(10px); border-top: 1px solid var(--border-soft);
  grid-template-columns: repeat(5, 1fr); align-items: center;
  /* dark buffer above the icons + lift the row above the iOS home indicator */
  padding-top: 10px;
  padding-bottom: env(safe-area-inset-bottom, 0px); }
.mn-item { display: flex; flex-direction: column; align-items: center; gap: 3px; color: var(--text-3); font-size: 10.5px; font-weight: 600; }
.mn-item .ico { width: 21px; height: 21px; }
.mn-item:active { color: var(--brand); }
.mn-scan { background: none; border: none; }
.mn-scan .mn-scan-ico { width: 44px; height: 44px; margin-top: -22px; border-radius: 50%; background: linear-gradient(135deg, var(--brand), var(--brand-dark));
  color: #fff; display: grid; place-items: center; font-size: 20px; box-shadow: 0 6px 16px rgba(27,159,221,.5); }

/* ----------------------------------------------------- Field scanner */
.scanner { position: fixed; inset: 0; z-index: 90; background: #05080f; display: none; flex-direction: column; }
.scanner.open { display: flex; }
.scan-head { display: flex; align-items: center; justify-content: space-between; padding: calc(16px + env(safe-area-inset-top, 0px)) 18px 16px; color: #fff; }
.scan-x { background: rgba(255,255,255,.1); border: none; color: #fff; width: 36px; height: 36px; border-radius: 10px; font-size: 16px; }
.scan-modes { display: flex; gap: 8px; padding: 0 16px 12px; overflow-x: auto; }
.scan-modes button { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 3px; background: rgba(255,255,255,.07);
  border: 1px solid transparent; color: #cbd5e1; border-radius: 12px; padding: 10px 16px; font-size: 11.5px; font-weight: 600; }
.scan-modes button span { font-size: 11px; }
.scan-modes button.active { background: var(--brand-soft); border-color: var(--brand); color: #fff; }
.scan-stage { position: relative; flex: 1; margin: 0 16px; border-radius: 18px; overflow: hidden; background: #0a0f1c; display: grid; place-items: center; min-height: 200px; }
/* Absolute-fill (not grid-stretched): inside a grid/place-items stage a video with
   height:100% resolves against an auto row and collapses to its intrinsic 16:9,
   letterboxing with big black bars. inset:0 makes it fill the stage and cover-crop. */
.scan-stage video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .3s; }
.scan-stage.live video { opacity: 1; }
/* The one emoji the sweep could not reach, because it lived in CSS `content:` rather than
   in a template. Same objection as everywhere else: it renders as a different glyph per OS,
   carries Apple's or Google's colour instead of ours, and cannot take currentColor — on a
   dark scan stage it was a bright white-and-grey box. An inline SVG mask uses the icon set's
   own camera shape and inherits the colour it is given. */
.scan-stage.nocam::before {
  content: ""; position: absolute; width: 48px; height: 48px; opacity: .25;
  background: currentColor;
  -webkit-mask: var(--ic-camera) center / contain no-repeat;
          mask: var(--ic-camera) center / contain no-repeat;
}
:root {
  --ic-camera: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5A1.5 1.5 0 0 1 4.5 7h2.7l1.2-2h7.2l1.2 2h2.7A1.5 1.5 0 0 1 21 8.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z"/><circle cx="12" cy="13" r="3.4"/></svg>');
}
.scan-reticle { position: absolute; width: 70%; max-width: 320px; aspect-ratio: 1.6; border: 2px solid rgba(255,255,255,.5); border-radius: 16px;
  box-shadow: 0 0 0 2000px rgba(0,0,0,.35); }
.scan-line { position: absolute; left: 6%; right: 6%; height: 2px; background: var(--accent); box-shadow: 0 0 12px var(--accent); top: 0; animation: scanmove 2s ease-in-out infinite; }
@keyframes scanmove { 0%,100% { top: 8%; } 50% { top: 92%; } }
.scan-hint { position: absolute; bottom: 14px; left: 0; right: 0; text-align: center; color: #cbd5e1; font-size: 12.5px; padding: 0 16px; }
.scan-manual { display: flex; gap: 8px; padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px)); }
.scan-manual input { flex: 1; background: rgba(255,255,255,.08); border: 1px solid var(--border); color: #fff; border-radius: 12px; padding: 13px 14px; font-size: 15px; outline: none; }
.scan-result { padding: 0 16px; }
.scan-card { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--ok); border-radius: 12px; padding: 14px; }
.scan-card.warn { border-left-color: var(--warn); }
.scan-card.err { border-left-color: var(--danger); }
.scan-recent { padding: 14px 16px 20px; }
.scan-recent-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--border-soft); font-size: 12px; color: var(--text-2); }

/* ------------------------------------------------------- Responsive */
@media (max-width: 1100px) { .scan-btn-label { display: none; } }

@media (max-width: 900px) {
  .sidebar { position: fixed; transform: translateX(-100%); transition: transform .25s; z-index: 60; box-shadow: 20px 0 50px rgba(0,0,0,.4); }
  .sidebar.open { transform: translateX(0); }
  /* No rail on a phone — the sidebar is a drawer here, so the width override would leave a
     72px strip pinned over the content. */
  body.nav-rail { --sidebar-w: 296px; }
  /* Only the TOPBAR search hides on mobile — the in-content record/case search
     reuses .global-search and must stay visible. */
  .topbar .global-search { display: none; }
  /* keep the safe-area top inset on mobile — without it the header renders UNDER
     the iOS status bar / notch (viewport-fit=cover + black-translucent). */
  .topbar { padding: calc(env(safe-area-inset-top, 0px) + 8px) 14px 8px; gap: 10px; }
  .topbar h1 { font-size: 16px; }
  .topbar .sub { display: none; }
  /* Scan is a MOBILE action (camera in hand on the ramp/warehouse) — it earns its top-bar
     slot only on the mobile shell. Hidden by default, restored here at the mobile breakpoint. */
  .topbar .scan-btn { display: inline-flex; }
  .content { padding: 16px 14px calc(80px + env(safe-area-inset-bottom, 0px)); }
  .mobile-nav { display: grid; }
  .drawer { width: 100vw; }
  .page-head { gap: 10px; }
  .page-head h2 { font-size: 19px; }
  .phone { width: 100%; flex-basis: 100%; border-radius: 24px; }
  .phone-screen { height: auto; min-height: 60vh; }
  .kanban { grid-auto-columns: 78vw; }
  .grid.cols-4, .grid.cols-3, .grid.cols-2 { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 560px) {
  .grid.cols-4, .grid.cols-3, .grid.cols-2,
  .grid[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
  .kpi .value { font-size: 28px; }
  .modal { border-radius: 16px 16px 0 0; align-self: flex-end; }
  .modal-backdrop { align-items: flex-end; padding: 0; }
}

/* -------------------------------------------------------------------------
   Mobile QA sweep — consolidated responsive fixes (phones ≤ 640/560px).
   Each rule below traces to a screen tested at 375px; grouped by breakpoint.
   ------------------------------------------------------------------------- */
/* Let paired native controls shrink instead of forcing horizontal overflow. */
.field input, .field select, .field textarea { min-width: 0; }
/* Modal footers may hold two full-label buttons — allow them to wrap. */
.modal-foot { flex-wrap: wrap; }

@media (max-width: 900px) {
  /* Toasts sat on top of the fixed bottom-nav; lift + full-bleed them. */
  .toasts { bottom: calc(80px + env(safe-area-inset-bottom, 0px)); right: 12px; left: 12px; }
  .toast { min-width: 0; width: 100%; box-sizing: border-box; }
  /* In-content search (records/cases) is capped by max-width:34vw off-screen —
     let it use its own inline width now that it's no longer hidden. */
  .global-search { max-width: none; }
}

@media (max-width: 640px) {
  /* pad-0 cards (PRM flights/agents) crushed their heads + first column
     against the edge. Give heads a gutter and the flight table end-columns air. */
  .card.pad-0 > .card-head { padding-left: 14px; padding-right: 14px; }
  .card.prm-flights .card-head { padding: 14px 14px 4px; }
  .prm-flight-table td:first-child, .prm-flight-table th:first-child { padding-left: 14px; }
  .prm-flight-table td:last-child, .prm-flight-table th:last-child { padding-right: 14px; }
  /* Agent app renders a fake-phone mockup; strip the device chrome on a real phone. */
  .phone { border: none; border-radius: 0; box-shadow: none; }
  .phone-notch { display: none; }
  .phone-screen { min-height: 40vh; }
  /* Funnel labels were a hard 160px, squeezing the bars off-screen. */
  .funnel-label { flex: 0 0 42%; width: auto; max-width: 150px; font-size: 11.5px; }
  .funnel-bar { min-width: 44px; }
  /* Grow icon-button tap targets (reorder/remove) to a usable size on touch. */
  .icon-btn { width: 40px; height: 40px; font-size: 14px; }
  .field-card-head { gap: 8px; }
}

@media (max-width: 560px) {
  /* .field-row was a fixed 2-col grid with no mobile override — every modal
     (create/edit, check-out, compensation, integrations, forms…) collapses now. */
  .field-row { grid-template-columns: 1fr; }
  .field-row .req-toggle { margin-bottom: 14px; }
  /* Cards carrying inline grid-column:span 2 left a phantom empty column once
     the grid became single-column. Force them back to the one real column. */
  .grid.cols-3 > [style*="grid-column"],
  .grid.cols-2 > [style*="grid-column"] { grid-column: auto !important; }
  /* Fixed 130px key column left long values ~160px; stack key over value. */
  .kv { grid-template-columns: 1fr; gap: 2px 0; }
  .kv .k { margin-top: 8px; }
  /* One column on a phone. The auto-fit floor already collapses to 1fr at this width via
     min(100%, 200px), so this only tightens the rhythm. */
  .detail-grid { gap: 13px 0; }
  /* Federation bulk-toggle group gets its own line instead of crowding the title. */
  .cat-bulk { flex: 1 0 100%; margin-top: 8px; justify-content: flex-start; }
  /* Stretch modal-footer buttons full width when stacked. */
  .modal-foot { justify-content: stretch; }
  .modal-foot .btn { flex: 1; }
}

/* -------------------------------------------------------------------------
   Asset-sticker OCR — capture modal + prefill banner
   ------------------------------------------------------------------------- */
.ocr-drop { position: relative; border: 2px dashed var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); min-height: 150px; display: grid; place-items: center;
  padding: 14px; cursor: pointer; text-align: center; overflow: hidden; transition: border-color .15s; }
.ocr-drop:hover { border-color: var(--accent); }
.ocr-drop.has-img { border-style: solid; padding: 0; background: #000; }
.ocr-drop-inner { color: var(--text-3); font-size: 13.5px; }
#ocr_preview { display: none; max-width: 100%; max-height: 300px; object-fit: contain; }
.ocr-banner { background: var(--info-soft); border: 1px solid var(--border);
  border-left: 3px solid var(--info); border-radius: var(--radius-sm);
  padding: 10px 12px; margin-bottom: 14px; font-size: 13px; line-height: 1.45; }

/* -------------------------------------------------------------------------
   RBAC — permission tickbox tree, roles & groups, per-user access editor
   ------------------------------------------------------------------------- */
.perm-tree { display: flex; flex-direction: column; gap: 10px; max-height: 52vh;
  overflow-y: auto; padding: 4px 2px; }
.perm-group { border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); overflow: hidden; flex: 0 0 auto; }
.perm-group.not-entitled { opacity: .55; }
.perm-group-head { display: flex; align-items: center; gap: 8px; padding: 9px 12px;
  background: var(--surface-3); cursor: pointer; font-size: 13px; }
.perm-group-head.static { cursor: default; }
.perm-group-head .perm-cat { font-size: 11px; margin-left: 4px; }
.perm-group-body { padding: 4px 12px 10px; }
.perm-res { padding: 8px 0; border-top: 1px solid var(--border-soft); }
.perm-res:first-child { border-top: 0; }
.perm-res-head { display: flex; align-items: center; gap: 8px; font-weight: 600;
  font-size: 12.5px; cursor: pointer; color: var(--text-2); margin-bottom: 6px; }
.perm-acts { display: flex; flex-wrap: wrap; gap: 6px 16px; padding-left: 22px; }
.perm-leaf { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px;
  color: var(--text-2); cursor: pointer; }
.perm-leaf input, .perm-res-head input, .perm-group-head input { margin: 0; cursor: pointer; }
.perm-tree.override .perm-group-body { padding: 0; }
.ovr-leaf { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 7px 12px; border-top: 1px solid var(--border-soft); font-size: 12.5px; }
.ovr-name code { font-size: 11px; }
.ovr-sel { width: auto; padding: 4px 8px; font-size: 12px; }
.ovr-sel.grant { border-color: var(--ok); color: var(--ok-ink); background: var(--ok-soft); }
.ovr-sel.revoke { border-color: var(--danger); color: var(--danger-ink); background: var(--danger-soft); }
.ovr-details { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; }
.ovr-details > summary { cursor: pointer; font-weight: 600; font-size: 13px; color: var(--text-2); }

/* -------------------------------------------------------------------------
   Compensation, payment rails, attachments & document scan
   ------------------------------------------------------------------------- */
.kpi .kpi-label { color: var(--text-3); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
.kpi .kpi-val { font-size: 30px; font-weight: 760; margin: 6px 0 2px; line-height: 1; }
.kpi .kpi-sub { color: var(--text-3); font-size: 12px; }

.provider-strip { display: flex; gap: 10px; flex-wrap: wrap; padding: 6px 16px 16px; }
.provider-chip { flex: 1 1 180px; min-width: 170px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-2); padding: 11px 13px; }
.provider-chip.off { opacity: .5; }

.scan-row { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
.scan-row .btn { flex: 1; justify-content: center; font-size: 11.5px; }

.attach-host { }
.attach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 10px; }
.attach-cell { cursor: pointer; border: 1px solid var(--border); border-radius: 10px;
  overflow: hidden; background: var(--surface-2); transition: border-color .15s; }
.attach-cell:hover { border-color: var(--brand); }
.attach-thumb { aspect-ratio: 1; background: var(--surface-3); display: grid; place-items: center;
  font-size: 28px; overflow: hidden; }
.attach-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.attach-thumb.doc { color: var(--text-3); }
.attach-cap { font-size: 11.5px; font-weight: 600; padding: 6px 8px 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.attach-cell .muted { padding: 0 8px 7px; }

/* -------------------------------------------------------------------------
   Forms & inspections — designer, palette, fill inputs
   ------------------------------------------------------------------------- */
.form-card { cursor: pointer; transition: border-color .15s, transform .1s; }
.form-card:hover { border-color: var(--brand); transform: translateY(-2px); }
.form-card-icon { font-size: 30px; }

/* Forms — assignment inbox + seg badge */
.seg-badge { display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 9px; background: var(--brand); color: #04121c; font-size: 11px; font-weight: 800; margin-left: 5px; vertical-align: middle; }
.inbox-list { display: flex; flex-direction: column; gap: 8px; }
.inbox-row { display: flex; align-items: center; gap: 13px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; padding: 13px 15px; transition: border-color .12s, transform .08s; }
.inbox-row:not(.done) { cursor: pointer; }
.inbox-row:not(.done):hover { border-color: var(--brand); transform: translateY(-1px); }
.inbox-row.done { opacity: .62; }
.inbox-ico { width: 34px; height: 34px; border-radius: 9px; background: var(--surface-2); display: grid; place-items: center; font-size: 16px; flex-shrink: 0; }
/* Agent app quick-fill form tiles */
.qf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.qf-tile { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; background: var(--surface); border: 1px solid var(--border);
  border-radius: 13px; padding: 15px; cursor: pointer; transition: border-color .12s, transform .08s; text-align: left; }
.qf-tile:hover { border-color: var(--brand); transform: translateY(-2px); }
.qf-ico { font-size: 26px; }
.qf-name { font-weight: 650; font-size: 13px; line-height: 1.25; }

.palette { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.palette-btn { display: flex; align-items: center; gap: 8px; padding: 9px 11px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-2); font-size: 12.5px; text-align: left; }
.palette-btn:hover { border-color: var(--brand); color: var(--text); }
.palette-btn span { width: 20px; text-align: center; opacity: .8; }

.field-card { border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
  padding: 10px 12px; margin-bottom: 10px; }
.field-card-head { display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
.field-card-head .spacer { flex: 1; }
.icon-btn { width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--border);
  background: var(--surface-3); color: var(--text-2); font-size: 11px; line-height: 1; }
.icon-btn:hover { color: var(--text); border-color: var(--brand); }
.icon-btn.danger:hover { color: var(--danger); border-color: var(--danger); }
.req-toggle { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-2); white-space: nowrap; }

.choice-group { display: flex; flex-direction: column; gap: 6px; }
.choice { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); cursor: pointer; }
.sig-pad { border: 1px dashed var(--border); border-radius: 8px; background: var(--surface);
  touch-action: none; width: 100%; max-width: 340px; cursor: crosshair; }

/* -------------------------------------------------------------------------
   Branding & media tiles (Settings → Workspace)
   ------------------------------------------------------------------------- */
.brand-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; align-items: stretch; }
.brand-tile { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); overflow: hidden; }
.brand-tile-prev { height: 132px; flex: none; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 12px; background:
  repeating-conic-gradient(var(--surface-3) 0% 25%, var(--surface) 0% 50%) 50% / 18px 18px; }
.brand-tile-prev img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; display: block; }
.brand-tile-empty { color: var(--text-3); font-size: 12px; text-align: center; padding: 0 10px; }
.brand-tile-body { display: flex; flex-direction: column; gap: 3px; padding: 12px 13px 13px; flex: 1 1 auto; }
.brand-tile-body b { font-size: 13px; }
.brand-tile-fname { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brand-tile .btn-row { margin-top: auto; padding-top: 10px; }

/* -------------------------------------------------------------------------
   Camera capture + integrations
   ------------------------------------------------------------------------- */
.cam-wrap { background: #000; border-radius: 10px; overflow: hidden; aspect-ratio: 4 / 3;
  display: grid; place-items: center; }
.cam-wrap video { width: 100%; height: 100%; object-fit: cover; }

.integ-card { display: flex; flex-direction: column; transition: border-color .15s; }
.integ-card.on { border-color: var(--ok-soft); }
.integ-card:hover { border-color: var(--brand); }
.integ-ico { font-size: 28px; }
.integ-name { font-size: 15px; font-weight: 680; margin-top: 6px; }
.integ-sum { font-size: 12.5px; color: var(--text-2); margin: 4px 0 8px; min-height: 34px; }
.integ-caps { display: flex; flex-wrap: wrap; gap: 5px; }
.integ-caps .chip { font-size: 10.5px; padding: 2px 8px; border-radius: 20px; background: var(--surface-3);
  color: var(--text-2); }

/* -------------------------------------------------------------------------
   Flight data review — two providers, one flight, one disputed field.
   The whole screen is a comparison, so the candidates are the loudest thing
   on it: side by side, equal weight, neither pre-selected. Nudging the
   reviewer toward one vendor would defeat the point of asking them.
   ------------------------------------------------------------------------- */
/* nowrap: a multi-word chip is one token — "Baggage Operations" wrapping to two lines read
   as two separate chips. Taller padding and a larger type size make them legible rather than
   cramped; line-height keeps a wrapped ROW of chips evenly spaced. */
.chip { display: inline-flex; align-items: center; white-space: nowrap;
  font-size: 11.5px; padding: 4px 10px; border-radius: 20px; line-height: 1.5;
  background: var(--surface-3); color: var(--text-2); font-weight: 600; }
/* A set of chips needs its own rhythm — inline-flex children collapse whitespace, so the
   gap has to come from the container. */
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip.warn { background: var(--warn-soft, rgba(230, 160, 30, .16)); color: var(--warn-ink); }
.chip.danger { background: var(--danger-soft, rgba(220, 70, 70, .16)); color: var(--danger-ink); }

.fd-cands { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.fd-cand { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px;
  padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.fd-cand .fd-val { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums;
  line-height: 1.2; word-break: break-word; }
.fd-cand .btn { margin-top: auto; }
.fd-cand .fd-how { font-size: 10.5px; color: var(--text-3); line-height: 1.3; }
.fd-row:hover { border-color: var(--brand); }

/* The override field is inline, so it can't use `.field input` (that forces width:100%
   and a block margin). Same tokens, sized to sit in the row. */
.fd-own { background: var(--bg-2); border: 1px solid var(--border); color: var(--text);
  border-radius: 10px; padding: 8px 11px; font-size: 13px; outline: none;
  font-family: inherit; flex: 0 0 210px; max-width: 100%; }
.fd-own:focus { border-color: var(--brand); }
.fd-own::placeholder { color: var(--text-3); }

@media (max-width: 640px) {
  .fd-cands { grid-template-columns: 1fr; }
}

/* -------------------------------------------------------------------------
   PRM Assist — special-assistance dispatch (controller dashboard + agent app)
   ------------------------------------------------------------------------- */
.prm-alert-strip { display: flex; align-items: center; gap: 10px; padding: 11px 15px; margin-bottom: 16px;
  background: var(--warn-soft); border: 1px solid var(--warn); border-radius: var(--radius-sm);
  color: var(--warn-ink); font-size: 13px; }
.prm-alert-strip .prm-alert-ico { font-size: 16px; }
.prm-alert-strip b { color: var(--warn); font-weight: 700; }

/* Tab bar + live refresh dot */
.prm-tabs button { padding: 8px 15px; }
.prm-refresh-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--ok);
  box-shadow: 0 0 0 0 var(--ok-soft); align-self: center; margin: 0 4px; flex: none; }
.prm-refresh-dot.pulse { animation: prmPulse .8s ease-out; }
@keyframes prmPulse { 0% { box-shadow: 0 0 0 0 rgba(34,197,94,.55); } 100% { box-shadow: 0 0 0 8px rgba(34,197,94,0); } }

/* Shared "Live" indicator (Live.dot) for auto-refreshing boards */
.live-ind { display: inline-flex; align-items: center; gap: 6px; align-self: center; margin-right: 4px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .3px; color: var(--text-3); text-transform: uppercase; }
.live-ind .live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none; }
.live-ind.live-pulse .live-dot { animation: prmPulse .8s ease-out; }

/* Compact KPI strip (live board) */
.prm-kpi-strip { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 14px; }
.prm-kpi-cell { background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
  padding: 12px 14px; box-shadow: var(--shadow-sm);
  /* `position: relative` is for the linked variant's chevron below; harmless on a plain cell. */
  position: relative; }
/* A LINKED CELL SAYS SO AT REST, the same way `.kpi.linked` does on every other board: three of
   the six numbers open the rows they counted and three cannot, so hover is not the affordance —
   in a strip of six the reader has to see which three without touching them. Same `.kpi-go`
   chevron, so the two boards do not grow separate vocabularies for one idea. */
.prm-kpi-cell.linked { color: inherit; text-decoration: none; display: block; }
.prm-kpi-cell.linked:hover { border-color: var(--brand); }
.prm-kpi-cell.linked:hover .kpi-go { opacity: 1; transform: translateX(2px); }
.prm-kpi-val { font-size: 26px; font-weight: 780; line-height: 1; }
.prm-kpi-lbl { color: var(--text-3); font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .5px; margin-top: 5px; }
@media (max-width: 900px) { .prm-kpi-strip { grid-template-columns: repeat(3, 1fr); } }

/* ---- EC 1107 / ECAC Doc 30 compliance report ----
   Each band is a bar with a MARKER at the required percentage, because the question is
   never "how big is the bar" but "did it reach the line" — a bar alone makes 79% and 81%
   look the same, and they are the difference between passing and failing an audit. */
.ec-cohort .card-head { margin-bottom: 4px; }
.ec-clock { font-size: 11.5px; margin-bottom: 10px; }
.ec-band { display: grid; grid-template-columns: 150px 1fr 52px 82px; gap: 12px;
  align-items: center; padding: 6px 0; }
.ec-band-req { font-size: 12px; color: var(--text-2); }
.ec-band-bar { position: relative; height: 8px; border-radius: 6px; background: var(--surface-3); overflow: visible; }
.ec-band-bar > span { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 6px;
  background: var(--ok); }
.ec-band.bad .ec-band-bar > span { background: var(--danger); }
/* The regulated threshold, drawn ON the track so the eye lands on the line, not the fill. */
.ec-band-bar > i { position: absolute; top: -3px; bottom: -3px; width: 2px;
  background: var(--text); opacity: .55; border-radius: 2px; }
.ec-band-val { font-size: 12.5px; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }
.ec-band-flag { display: flex; justify-content: flex-end; align-items: center; }
.ec-band.ok .ec-band-flag { color: var(--ok); }
.ec-caveat { border-left: 3px solid var(--warn); margin-bottom: 14px; }
@media (max-width: 700px) {
  .ec-band { grid-template-columns: 1fr 46px; }
  .ec-band-bar, .ec-band-flag { grid-column: 1 / -1; }
  .ec-band-flag { justify-content: flex-start; }
}
@media (max-width: 520px) { .prm-kpi-strip { grid-template-columns: repeat(2, 1fr); } }

/* SLA countdown pill — amber ≤5m, RED once overdue (beats incumbent: act before breach) */
.prm-sla { display: inline-block; padding: 3px 9px; border-radius: 20px; font-size: 11.5px; font-weight: 700;
  white-space: nowrap; }
.prm-sla-ok { background: var(--surface-3); color: var(--text-2); }
.prm-sla-amber { background: var(--warn-soft); color: var(--warn-ink); box-shadow: inset 0 0 0 1px var(--warn); }
.prm-sla-red { background: var(--danger-soft); color: var(--danger-ink); box-shadow: inset 0 0 0 1px var(--danger);
  animation: prmBlink 1.4s ease-in-out infinite; }
.prm-sla-none { color: var(--text-3); }
@keyframes prmBlink { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.prm-drawer-sla { display: inline-block; padding: 7px 13px; border-radius: 10px; font-weight: 700;
  font-size: 13px; margin-bottom: 12px; }
.prm-drawer-sla.prm-sla-ok { background: var(--surface-3); color: var(--text-2); }
.prm-drawer-sla.prm-sla-amber { background: var(--warn-soft); color: var(--warn-ink); }
.prm-drawer-sla.prm-sla-red { background: var(--danger-soft); color: var(--danger-ink); }

/* Two-column controller layout: board + agent pool */
.prm-grid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 16px; align-items: start; }
.prm-board-card, .prm-pool-card { min-width: 0; }
@media (max-width: 1100px) { .prm-grid { grid-template-columns: 1fr; } }

/* Live task list (rows, not a wide table — scannable + SLA-led) */
/* The list is INSET from the card, so a state tint reads as a highlighted row rather
   than a coloured band bleeding into the card's own edge. The rows keep their rhythm;
   they just stop touching the sides. */
.prm-live-list { display: flex; flex-direction: column; gap: 2px; padding: 6px 8px 8px; }
.prm-live-row { display: grid; align-items: center; gap: 12px;
  /* The SLA track has a FLOOR, not a fixed width. It was 92px, but the pill is
     white-space:nowrap and the longest label fmtSpan can produce — "12h 45m overdue" —
     needs ~113px, so it painted straight out of the row and got clipped by the card.
     minmax() keeps short labels ("5m left") aligned on the floor while letting a long
     one take the room it needs; the pill is right-aligned either way, so the right
     edges still line up down the column. */
  grid-template-columns: auto 22px minmax(0,1fr) 130px 120px minmax(96px, max-content);
  padding: 11px 12px; border-radius: 10px; cursor: pointer;
  transition: background .12s; }
/* No rules between rows: the 2px gap on the list does the separating, which leaves the
   box-shadow free for the severity bar alone. A hairline separator and an inset bar
   were fighting over the same property, and `:first-child { box-shadow: none }` — there
   to drop the leading rule — was silently stripping the red bar off the top row, i.e.
   off the MOST overdue task on the board. */
.prm-live-row:hover { background: var(--surface-2); }
.prm-live-pending { background: var(--warn-soft); box-shadow: inset 3px 0 0 var(--warn); }
.prm-live-overdue { background: var(--danger-soft); box-shadow: inset 3px 0 0 var(--danger); }
.prm-live-overdue:hover { background: color-mix(in srgb, var(--danger) 20%, transparent); }
.prm-live-pending:hover { background: color-mix(in srgb, var(--warn) 20%, transparent); }
.prm-live-dir { font-size: 16px; text-align: center; }
.prm-live-main { min-width: 0; }
.prm-live-pax { font-size: 13.5px; }
.prm-live-sub { font-size: 11.5px; display: flex; gap: 5px; flex-wrap: wrap; margin-top: 2px; }
.prm-live-agentname { font-size: 12.5px; color: var(--text-2); }
.prm-live-sla, .prm-live-status { text-align: right; }
@media (max-width: 1100px) {
  .prm-live-row { grid-template-columns: auto 20px minmax(0,1fr) auto; }
  .prm-live-agent, .prm-live-status { display: none; }
}
@media (max-width: 560px) {
  /* drop the direction icon, tighten gaps so the passenger column isn't squeezed */
  .prm-live-row { grid-template-columns: auto minmax(0,1fr) auto; gap: 8px; padding: 11px 12px; }
  .prm-live-dir { display: none; }
  .prm-live-pax { font-size: 13px; }
  .prm-live-sla { font-size: 11px; }
  /* let long flight/route text wrap instead of bleeding past the SLA pill */
  .prm-live-sub { display: block; }
}
.prm-live-main { overflow: hidden; }
.prm-live-sub { overflow-wrap: anywhere; }

/* Allocatable green dot */
.prm-alloc-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--surface-3);
  display: inline-block; flex: none; box-shadow: inset 0 0 0 1px var(--border); }
.prm-alloc-dot.on { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.prm-agent-id { display: flex; align-items: center; gap: 9px; min-width: 0; }
.prm-leg { display: inline-flex; align-items: center; gap: 5px; }
.prm-leg .prm-alloc-dot { margin: 0; }

/* Per-flight expandable table (Arrivals / Departures) */
.prm-flights { padding: 16px; }
.prm-flight-table td.num, .prm-flight-table th.num { text-align: center; }
.prm-flight-row td { cursor: pointer; }
.prm-flight-row:hover { background: var(--surface-2); }
.prm-flight-row.open { background: var(--surface-2); }
.prm-flight-row.prm-flight-flag td:nth-child(2) { box-shadow: inset 3px 0 0 var(--warn); }
.prm-flight-row .prm-exp { color: var(--text-3); font-size: 13px; }
.prm-flight-flags { white-space: nowrap; }
.prm-flight-detail > td { padding: 0 16px 12px !important; background: var(--bg-2); }
.prm-pax-table { width: 100%; border-collapse: collapse; }
.prm-pax-table td { padding: 9px 10px; border-bottom: 1px solid var(--border-soft); font-size: 12.5px; vertical-align: middle; }
.prm-pax-table tr:last-child td { border-bottom: none; }
.prm-pax-overdue td { background: var(--danger-soft); }
.prm-pax-act { text-align: right; white-space: nowrap; }
.prm-pax-act .btn { margin-left: 6px; }

/* Workforce / employee-availability grid (Agents tab) */
.prm-agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px;
  padding: 0 16px 16px; }
.prm-wf-card { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2);
  padding: 14px; display: flex; flex-direction: column; }
.prm-wf-card.prm-agent-inactive { opacity: .55; }
.prm-wf-task { margin: 11px 0; padding: 9px 11px; background: var(--surface); border-radius: var(--radius-sm);
  font-size: 12px; color: var(--text-2); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-height: 18px; }

/* New-SSR scan note */
.prm-scan-note { background: var(--brand-soft); border: 1px solid var(--brand); border-radius: var(--radius-sm);
  padding: 9px 12px; margin-bottom: 14px; font-size: 12.5px; color: var(--brand-ink); }

/* Demand board table */
.prm-board-table td, .prm-board-table th { white-space: nowrap; }
.prm-board-table tbody tr { cursor: pointer; }
.prm-row-pending { background: var(--warn-soft); }
.prm-row-overdue { background: var(--danger-soft); box-shadow: inset 3px 0 0 var(--danger); }
.prm-row-overdue:hover, .prm-row-pending:hover { filter: brightness(1.15); }
.prm-overdue-time { color: var(--danger); font-weight: 700; }
.prm-flag { font-size: 14px; }

/* Agent pool cards */
.prm-pool-list { display: flex; flex-direction: column; gap: 10px; padding: 6px 14px 16px; }
.prm-agent-card { border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); padding: 12px 13px; }
.prm-agent-card.prm-agent-inactive { opacity: .5; }
.prm-agent-name { font-weight: 650; font-size: 13.5px; }
.prm-agent-task { margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--border-soft);
  font-size: 12px; color: var(--text-2); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.prm-agent-note { margin-top: 8px; font-size: 11.5px; color: var(--text-3); font-style: italic; }
.prm-agent-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin-top: 11px; }
/* Keep the capacity stepper + (De)activate together so they wrap as one unit and
   right-align, instead of a single control spilling past the card edge. */
.prm-agent-actions-r { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-wrap: wrap; }
.prm-cap-val { min-width: 44px; text-align: center; font-size: 12px; }
.prm-shift-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-2);
  cursor: pointer; margin-right: auto; }
.prm-shift-toggle.disabled { cursor: not-allowed; opacity: .6; }
.prm-shift-toggle input { width: auto; cursor: pointer; }

/* Assign modal options */
.prm-assign-list { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; }
.prm-assign-opt { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left; padding: 11px 13px; border: 1px solid var(--border);
  border-radius: 10px; background: var(--surface-2); color: var(--text); cursor: pointer;
  transition: border-color .12s, transform .08s; }
.prm-assign-opt:hover { border-color: var(--brand); transform: translateY(-1px); }

/* -------- Agent app (mobile-first, big touch targets) -------- */
.prm-agent-app { max-width: 540px; margin: 0 auto; }

.prm-avail { margin-bottom: 6px; }
.prm-avail-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.prm-avail-title { font-size: 17px; font-weight: 700; }
.prm-avail-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.prm-avail-note { margin-top: 11px; font-size: 12px; color: var(--text-3); line-height: 1.4; }

/* Big buttons shared by availability + task actions */
.prm-big-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; padding: 16px 14px; border-radius: 14px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font-size: 15px; font-weight: 650;
  cursor: pointer; transition: transform .08s, border-color .12s, background .12s; }
.prm-big-btn:hover { border-color: var(--brand); }
.prm-big-btn:active { transform: translateY(1px); }
.prm-big-btn:disabled { opacity: .45; pointer-events: none; }
.prm-big-btn.brand { background: var(--brand-fill); border-color: var(--brand); color: #fff; }
.prm-big-btn.ok { background: var(--ok); border-color: var(--ok); color: var(--ok-on); }
.prm-big-btn.warn { background: var(--warn); border-color: var(--warn); color: #2a1a02; }
.prm-big-btn.purple { background: var(--purple-fill); border-color: var(--purple); color: #fff; }
.prm-big-btn.danger { background: var(--danger-fill); border-color: var(--danger); color: #fff; }
.prm-big-btn.danger.ghost { background: transparent; border-color: var(--danger); color: var(--danger); }
.prm-big-btn.active { box-shadow: 0 0 0 3px var(--brand-soft); }

/* Task cards */
.prm-task-list { display: flex; flex-direction: column; gap: 12px; }
.prm-task-card { border: 1px solid var(--border); border-radius: 16px; background: var(--surface);
  padding: 15px 16px; }
.prm-task-card.prm-task-overdue { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger-soft); }
.prm-task-dir { font-size: 12.5px; color: var(--text-2); }
.prm-task-pax { font-size: 17px; font-weight: 700; margin: 10px 0 8px; }
.prm-task-meta { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-2); }
/* The card sets the passenger's NAME at 17px/700 and this at 12.5px. That was fine while the
   field was almost always empty; it now carries the carrier's own words about what the
   passenger needs, off the telex. The name identifies somebody. This changes what the agent
   does, so it does not also get the quietest ink on the screen. */
/* AND IT MUST NOT LOOK LIKE A BUTTON. On the handset this sat directly above "Passenger
   presented" as the same pale rounded rectangle at the same width, so the thing the agent has
   to READ and the thing they have to PRESS were the same shape — found by opening the screen,
   not by any assertion. The left rule is what separates a callout from a control. */
.prm-task-notes { margin-top: 10px; font-size: 12.5px; color: var(--text-2);
  background: var(--surface-2); border-radius: 4px 9px 9px 4px; padding: 8px 10px;
  border-left: 3px solid var(--brand); }
.prm-task-stamps { margin-top: 10px; font-size: 11.5px; }
.prm-task-actions { display: grid; gap: 10px; margin-top: 14px; }
@media (max-width: 560px) {
  .prm-grid { grid-template-columns: 1fr; }
}
.integ-caps .chip.mono { font-family: var(--mono); }

/* ===== Commercial & Contracts ===== */
.kpi-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
.kpi-row .kpi { background: var(--surface-2); border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px; }
/* THE TYPOGRAPHY IS NOT SCOPED TO `.kpi-row`, AND THAT IS THE FIX RATHER THAN AN OVERSIGHT.
   It was scoped, because these classes were written for this screen. Training Analytics read
   the markup, copied `kpi-n`/`kpi-l` into a plain `.grid`, and matched NOTHING — no size, no
   weight, no uppercase label — so four headline figures rendered at body size beside their
   own captions, looking like a styling choice rather than a miss.

   AND TWO OTHER SCREENS HAD ALREADY HIT THIS AND PAPERED OVER IT. `qualifications.js` and
   `sessions.js` use `kpi-n` as an emphasis span inside a sentence — "2 upcoming", "48 people
   audited" — and every one of them carries an inline `font-size`, supplying by hand the size
   the missing rule would have given. Those screens were never visibly wrong, which is why
   nobody chased it; the inline overrides ARE the bug report, written in the markup and never
   read. They still win over this rule, so those spans keep their intended smaller size and
   gain only the weight.

   A class that only works inside one parent is a trap for whoever copies it next. The layout
   stays on `.kpi-row`, which is the part that really is this screen's. */
.kpi-n { font-size: 24px; font-weight: 780; line-height: 1.05; }
.kpi-l { color: var(--text-3); font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; margin-top: 5px; }
.kpi-n.ok { color: var(--ok, #34d399); }
.kpi-n.warn { color: var(--warn, #f59e0b); }
.kpi-n.danger { color: var(--danger, #ef4444); }
@media (max-width: 1100px) { .kpi-row { grid-template-columns: repeat(3, 1fr); } }

/* Service-desk filter tiles: buttons dressed as KPI cards (.kpi-v / .kpi-l mirror the
   .kpi-row number/label pair). These had no rules at all and rendered as native
   browser buttons. */
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.kpis button.kpi { background: var(--surface-2); border: 1px solid var(--border); border-radius: 14px;
  padding: 14px 16px; color: var(--text); font-family: inherit; transition: border-color .15s, background .15s; }
.kpis button.kpi:hover { border-color: var(--brand); }
.kpis button.kpi.active { border-color: var(--brand); background: var(--brand-soft); }
.kpis .kpi-v { font-size: 24px; font-weight: 780; line-height: 1.05; font-variant-numeric: tabular-nums; }
.kpis .kpi-l { color: var(--text-3); font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; margin-top: 5px; }
.kpis button.kpi.info .kpi-v { color: var(--info); }
.kpis button.kpi.warn .kpi-v { color: var(--warn); }
.kpis button.kpi.danger .kpi-v { color: var(--danger); }
@media (max-width: 760px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .kpi-row { grid-template-columns: repeat(2, 1fr); } }

.kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 18px 32px; }
@media (max-width: 620px) { .kv-grid { grid-template-columns: 1fr; } }

/* Form matrix / grid field — a check-per-row across columns (e.g. one column per hold) */
table.matrix { border-collapse: collapse; width: 100%; font-size: 12.5px; }
table.matrix th, table.matrix td { border: 1px solid var(--border); padding: 6px 8px; text-align: center; }
table.matrix thead th { background: var(--surface-2); font-weight: 640; white-space: nowrap; }
table.matrix th.mx-rowlbl { text-align: left; font-weight: 500; background: var(--surface-2); min-width: 160px; }
table.matrix td .mx-cell { width: 100%; min-width: 64px; box-sizing: border-box; }
table.matrix td select.mx-cell { padding: 4px 6px; }
table.matrix.read td.mx-ok { color: var(--ok); font-weight: 640; }
table.matrix.read td.mx-danger { color: var(--danger); font-weight: 640; }

/* Inline port filter: sits in the tab row, sized to match the nav (seg) buttons, with a
   divider so it reads as a distinct control. Shared by the generic modules and PRM. */
.filters.tab-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.filter-divider { width: 1px; align-self: center; height: 22px; background: var(--border); margin: 0 6px; }
/* Toolbar dropdowns — the port filter, the status filter, the analytics filters.
   These sat at four different heights (34 / 35 / 35 / 32.5px) beside buttons of a fifth,
   because each was styled on its own with vertical padding and a native select adds its
   own internal metrics on top. They share one rule and the toolbar height now.
   appearance:none is what makes that height reliable: with the native control left in
   place the browser reserves its own row height and platform arrow, which is also why the
   macOS double-chevron looked foreign here. The arrow below is drawn to match. */
.port-select, .loc-filter, .prm-an-filter, .chip-select {
  appearance: none; -webkit-appearance: none;
  height: var(--control-h); padding: 0 32px 0 12px;
  /* `line-height: 1` leaves the line box exactly as tall as the em, so anything below the
     baseline is clipped — the tail of the p in "All ports". The bare `select` rule above
     was corrected for this; these four were not, and being CLASS selectors they outrank it,
     so the toolbar filters kept the bug after the generic control was fixed. Any future
     control that pins a height must leave the line-height alone rather than set it to 1. */
  font-family: inherit; font-size: 12.5px; font-weight: 600; line-height: normal;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  border-radius: 10px; cursor: pointer; max-width: 100%;
  /* Chevron drawn with the text colour so it follows light/dark like everything else. */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 1px), calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.port-select:hover, .loc-filter:hover, .prm-an-filter:hover, .chip-select:hover { border-color: var(--brand); }
.port-select:focus, .loc-filter:focus, .prm-an-filter:focus, .chip-select:focus { border-color: var(--brand); outline: none; }
/* The menu itself is drawn by the OS, so its options must be given real colours or a
   dark theme leaves white-on-white text on Windows/Linux. */
.port-select option, .loc-filter option, .prm-an-filter option, .chip-select option {
  background: var(--surface); color: var(--text);
}

/* Master Scheduler feed cards + two-column panels */
.sched-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }

/* Live-SLA "driving the platform" panel */
.driving .drive-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
.drive-block { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; }
.drive-head { font-weight: 680; font-size: 13px; margin-bottom: 6px; }
.drive-row { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: baseline; padding: 3px 0; border-top: 1px solid var(--border); }
.drive-row:first-of-type { border-top: 0; }
.drive-code { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--brand); }
.drive-metric { font-size: 12.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drive-target { font-weight: 700; font-size: 12.5px; }
.driving .drive-row .muted { grid-column: 2 / 4; }

/* Contract form */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-grid label { display: flex; flex-direction: column; font-size: 12px; color: var(--text-2); font-weight: 600; gap: 6px; }
.form-grid label.full { grid-column: 1 / -1; }
.form-grid input, .form-grid select, .form-grid textarea {
  font: inherit; padding: 9px 11px; border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface); color: var(--text); font-weight: 500;
}
.form-grid input:focus, .form-grid select:focus, .form-grid textarea:focus { border-color: var(--brand); outline: none; }
.scope-pick { display: flex; flex-wrap: wrap; gap: 8px 14px; }
.scope-opt { flex-direction: row !important; align-items: center; gap: 6px !important; font-weight: 500 !important; }
@media (max-width: 620px) { .form-grid { grid-template-columns: 1fr; } }

/* SLA term editor rows */
/* scope · code · metric · direction · target · unit · penalty · remove */
.sla-row { display: grid; grid-template-columns: 110px 110px 1fr 92px 78px 80px 1fr auto; gap: 8px; margin-bottom: 8px; align-items: center; }
.sla-row input, .sla-row select { font: inherit; padding: 7px 9px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); min-width: 0; }
.sla-row select:focus, .sla-row input:focus { border-color: var(--brand); outline: none; }
@media (max-width: 820px) { .sla-row { grid-template-columns: 1fr 1fr; } }

/* Commercial — form section headers + generic sub-list editor rows */
.form-section { font-weight:700; font-size:13px; color:var(--text-2); text-transform:uppercase; letter-spacing:.5px; margin:18px 0 8px; padding-bottom:5px; border-bottom:1px solid var(--border); }
.form-section:first-child { margin-top:0; }
.sub-row { display:grid; gap:8px; margin-bottom:8px; align-items:center; }
.sub-row input, .sub-row select { font:inherit; padding:7px 9px; border:1px solid var(--border); border-radius:8px; background:var(--surface); color:var(--text); min-width:0; }
.sub-row input:focus, .sub-row select:focus { border-color:var(--brand); outline:none; }
.attn-row { display:flex; gap:10px; align-items:center; justify-content:space-between; padding:7px 0; border-top:1px solid var(--border); cursor:pointer; flex-wrap:wrap; }
.attn-row:first-of-type { border-top:0; }
.attn-name { font-weight:650; font-size:13px; }
@media (max-width:820px){ .sub-row { grid-template-columns:1fr 1fr !important; } }

/* Module location/port filter dropdown */

/* Idle / inactivity sign-out warning */
.idle-warn { position: fixed; inset: 0; z-index: 9000; display: grid; place-items: center; background: rgba(5,8,12,.55); backdrop-filter: blur(2px); }
.idle-card { background: var(--surface-1, var(--surface)); border: 1px solid var(--border); border-radius: 16px; padding: 26px 28px; text-align: center; max-width: 340px; box-shadow: 0 24px 60px rgba(0,0,0,.45); }
.idle-card .idle-ico { font-size: 34px; }
.idle-card h3 { margin: 8px 0 4px; }
.idle-card p { color: var(--text-2); font-size: 13.5px; margin: 0 0 16px; }
.idle-card #idleSecs { color: var(--danger, #ef4444); font-variant-numeric: tabular-nums; }
.idle-card-held #idleSecs { color: var(--text-1, var(--text)); }
.idle-card .btn { width: 100%; }

/* The lock. OPAQUE, unlike .idle-warn above: the thing being covered is an assist naming a
   passenger and their SSR code, and a blurred screen is still a readable one at arm's length.
   z-index above .idle-warn so a warning that raced the lock cannot sit on top of it. */
.idle-lock { position: fixed; inset: 0; z-index: 9500; display: grid; place-items: center;
             background: var(--bg); }
.idle-lock-card { background: var(--surface); border: 1px solid var(--border);
                  border-radius: 16px; padding: 40px; text-align: center; width: 424px;
                  max-width: calc(100vw - 32px); box-shadow: var(--shadow); }
.idle-lock-card .idle-ico { color: var(--text-3); }
.idle-lock-card .idle-ico svg { width: 30px; height: 30px; }
.idle-lock-card h3 { margin: 10px 0 2px; }
.idle-lock-who { font-weight: 600; color: var(--text-1, var(--text)); margin: 0 0 2px; }
.idle-lock-card p { color: var(--text-2); font-size: 13.5px; margin: 0 0 14px; }
.idle-lock-card input { width: 100%; margin-bottom: 10px; }
.idle-lock-card .btn { width: 100%; }
.idle-lock-card .btn.ghost { margin-top: 8px; }
.idle-lock-err { color: var(--danger); font-size: 12.5px; margin-bottom: 10px; text-align: left; }

/* PRM agent capacity stepper (buggy / wheelchair train) */
.prm-cap-step { display: inline-flex; align-items: center; gap: 2px; }
.prm-cap-step .btn { padding: 4px 9px; line-height: 1; }

/* ===== PRM analytics dashboard ===== */
.prm-an-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.prm-an-head .muted { font-size: 12.5px; }
.prm-an-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.prm-an-kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-bottom: 16px; }
.prm-an-kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius, 12px); padding: 13px 14px; }
.prm-an-kpi-lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--text-3); font-weight: 700; }
.prm-an-kpi-val { font-size: 25px; font-weight: 800; margin: 4px 0 2px; font-variant-numeric: tabular-nums; line-height: 1.05; }
.prm-an-kpi-foot { font-size: 11px; color: var(--text-3); }
@media (max-width: 1100px) { .prm-an-kpis { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 620px) { .prm-an-kpis { grid-template-columns: repeat(2, 1fr); } }

.prm-an-roster { margin-bottom: 16px; }

/* hour-of-day demand + staffing column chart */
.prm-hourchart { display: flex; align-items: flex-end; gap: 6px; height: 188px; padding: 22px 4px 0; overflow-x: auto; }
.prm-hc-col { flex: 1 1 0; min-width: 26px; display: flex; flex-direction: column; align-items: center; height: 100%; }
.prm-hc-need { font-size: 11px; font-weight: 800; color: var(--text-3); height: 15px; font-variant-numeric: tabular-nums; }
.prm-hc-track { flex: 1; width: 100%; display: flex; align-items: flex-end; }
.prm-hc-bar { width: 100%; border-radius: 5px 5px 2px 2px; background: linear-gradient(180deg, var(--info), var(--brand-dark)); transition: height .3s ease; }
.prm-hc-hr { font-size: 10.5px; color: var(--text-3); margin-top: 6px; font-variant-numeric: tabular-nums; }
.prm-hc-col.peak .prm-hc-bar { background: linear-gradient(180deg, var(--brand), #0f5e87); }
.prm-hc-col.peak .prm-hc-need { color: var(--brand); }
.prm-hc-col.quiet .prm-hc-bar { background: linear-gradient(180deg, var(--surface-3), var(--surface-2)); }
.prm-hc-legend { display: flex; gap: 18px; flex-wrap: wrap; padding: 12px 4px 2px; font-size: 11.5px; color: var(--text-3); }
.prm-hc-legend i { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
.prm-hc-legend i.pk { background: var(--brand); }
.prm-hc-legend i.qt { background: var(--surface-3); }
.prm-hc-legend i.nd { background: transparent; border: 1px solid var(--text-3); border-radius: 50%; }

/* rostering insight callout */
.prm-an-insight { display: flex; gap: 10px; align-items: flex-start; margin-top: 14px; padding: 12px 14px;
  background: var(--brand-soft); border: 1px solid var(--border); border-left: 3px solid var(--brand); border-radius: 8px; font-size: 13px; color: var(--text-2); }
.prm-an-insight b { color: var(--text); }
.prm-an-insight-ico { font-size: 16px; line-height: 1.2; }

/* agent efficiency table */
.prm-an-agents { margin-bottom: 16px; }
.prm-at-wrap { overflow-x: auto; }
.prm-at { width: 100%; border-collapse: collapse; font-size: 13px; }
.prm-at thead th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--text-3);
  font-weight: 700; padding: 8px 16px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.prm-at th.num, .prm-at td.num { text-align: right; font-variant-numeric: tabular-nums; }
.prm-at tbody td { padding: 12px 16px; border-bottom: 1px solid var(--border-soft, var(--border)); vertical-align: middle; }
.prm-at tbody tr:last-child td { border-bottom: none; }
.prm-at tbody tr:hover { background: var(--surface-2); }
.prm-at-name { font-weight: 600; }
.prm-at-sub { font-size: 11px; color: var(--text-3); }
.prm-at-util { display: flex; align-items: center; gap: 8px; min-width: 130px; }
.prm-at-util-track { flex: 1; height: 7px; background: var(--surface-3); border-radius: 4px; overflow: hidden; }
.prm-at-util-bar { height: 100%; border-radius: 4px; background: var(--info); }
.prm-at-util-bar.hi { background: var(--warn); }
.prm-at-util-bar.mid { background: var(--accent); }
.prm-at-util-bar.lo { background: var(--text-3); }
.prm-at-util span { font-size: 11.5px; color: var(--text-2); font-variant-numeric: tabular-nums; min-width: 30px; text-align: right; }

/* A passenger's reply on the assist timeline. Indented under the message it answers so the
   two read as a exchange, and visually quieter than the event itself — it is context for the
   milestone, not a milestone of its own. */
.prm-reply-list { margin: 6px 0 2px; display: flex; flex-direction: column; gap: 6px; }
.prm-reply { display: flex; gap: 8px; align-items: flex-start; padding: 7px 9px;
  background: var(--surface-2); border-left: 2px solid var(--info); border-radius: 0 8px 8px 0;
  font-size: 12.5px; }
.prm-reply-mark { flex: none; color: var(--info); display: flex; }

.prm-an-breaks { margin-bottom: 16px; }
@media (max-width: 860px) { .prm-an-breaks { grid-template-columns: 1fr; } }

/* The mishandled-equipment card. Its own column count because .prm-an-kpis is a 6-up strip
   sized for the headline row, and three tiles laid out on that grid render a sixth of the
   width each with the numbers wrapped to nothing. */
.prm-an-equip { margin-bottom: 16px; }
.prm-an-equip-kpis { grid-template-columns: repeat(3, 1fr); margin-bottom: 0; }
@media (max-width: 620px) { .prm-an-equip-kpis { grid-template-columns: 1fr; } }

/* ---- Cargo Handheld: fullscreen, no-scroll device interface (Zebra guns) ---- */
/* The cargo handheld's shell. Two bugs lived here and both were about the physical device
   rather than the design:
     · it hid the sidebar and topbar but NOT the mobile bottom nav, which then sat on top of
       the last tile — on an 11-task grid that made "Locate" permanently unreachable;
     · it had no safe-area insets at all, so on a notched iPhone the operator's name and the
       Switch button rendered underneath the status bar.
   Both are exactly what the hhk-* set was built to prevent; this brings the older screen up
   to the same contract until it migrates onto that engine properly. */
body.hh-mode .sidebar,
body.hh-mode .topbar,
body.hh-mode .mobile-nav { display: none !important; }
body.hh-mode .app { height: 100dvh; min-height: 0; }
body.hh-mode .main { height: 100dvh; }
body.hh-mode .content { padding: 0 !important; height: 100dvh; overflow: hidden; }
/* The cargo handheld is hand-written rather than assembled from the hhk component set, so it
   inherits none of that contract — and a field nobody styled falls back to the UA default
   (~13px). Below 16px iOS silently zooms the page on focus and never zooms back: the bug fixed
   platform-wide in 1.13.2, still live here in 17 fields because they carried no font-size at
   all and so nothing flagged them. One scoped rule sets the floor for every field in the view,
   including the ones nobody thought to style. */
body.hh-mode input,
body.hh-mode select,
body.hh-mode textarea { font-size: 16px; }
body.hh-mode input:not([type="checkbox"]):not([type="radio"]),
body.hh-mode select { min-height: 44px; }
/* The toggles are excluded above because a 44px-tall checkbox is a 44px SQUARE, not a
   taller box — but 16px is still a poor target in gloves, so they grow instead of stretch.
   The label around them is the real hit area; this is so the box can be SEEN across a
   ramp at arm's length. */
body.hh-mode input[type="checkbox"], body.hh-mode input[type="radio"] {
  width: 22px; height: 22px; vertical-align: -5px; background-size: 21px 21px;
}
/* BUTTONS too. 149 of this view's 166 buttons declare no height at all — they express it as
   padding, which is why a check that reads `height:` reported the view clean while ~40 targets
   sat under the floor at 8-11px vertical padding. A min-height floors every one of them
   without touching 706 inline styles. */
body.hh-mode button { min-height: 44px; }
/* A blocked button set `color:#fff` unconditionally while switching its background to
   --border-soft or --surface, which in light theme is #e8edf5 on #ffffff: white text on a
   near-white button, on the label that explains WHY the step is blocked. These are bare
   <button> elements with no class, so neither .btn:disabled nor .hhk-btn:disabled reaches
   them, and an inline author style outranks the UA disabled colour regardless.
   !important is the only thing that beats an inline style, and this is what it is for. */
body.hh-mode button[disabled],
body.hh-mode button:disabled { color: var(--text-3) !important; opacity: 1; }
.hh-wrap {
  height: 100dvh; display: flex; flex-direction: column; box-sizing: border-box;
  background: var(--bg); color: var(--text); max-width: 760px; margin: 0 auto;
  padding: calc(16px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
           calc(16px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
}
/* Eleven tasks do not fit one phone screen. Shrinking them to fit (grid-auto-rows: 1fr on a
   clipped container) is what made the bottom row unreachable, so the grid scrolls and each
   tile keeps a size a gloved thumb can actually hit. */
.hh-tiles {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  grid-auto-rows: minmax(112px, auto); align-content: start; padding-bottom: 4px;
}
.hh-tiles .hh-tile {
  display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px;
  padding: 12px 8px; border-radius: 18px; border: 1px solid var(--border-soft, var(--border));
  background: var(--surface); color: var(--text); cursor: pointer; text-align: center;
}
.hh-tiles .hh-tile .ti { color: var(--brand); }
.hh-tiles .hh-tile .ti svg { width: 34px; height: 34px; }
.hh-tiles .hh-tile .tl { font-size: 16px; font-weight: 800; line-height: 1.15; }
.hh-tiles .hh-tile .ts { color: var(--text-3); font-size: 13px; }
/* The icon that heads a task screen. Sized in px, not vh: the old min(12vh,56px) was
   sizing an emoji glyph, and on a short landscape viewport it collapsed to nothing. */
.hh-bigicon { display: flex; justify-content: center; color: var(--brand); margin-bottom: 4px; }
.hh-bigicon svg { width: 44px; height: 44px; }
/* Big, glove-friendly tap targets inside the handheld */
.hh-tile:active, .hh-btn:active { transform: scale(.97); }
.hh-tile, .hh-btn { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

/* ============================================================================
   HANDHELD COMPONENT SET  (hhk-*)
   ----------------------------------------------------------------------------
   The design system every handheld screen is assembled from. It exists so screens
   are usable BY CONSTRUCTION rather than by review: the minimum sizes below are the
   contract, and tests/test_mobile_ux.py asserts them, so a screen built only from
   these components cannot ship an unreadable label or a target too small for a
   gloved thumb.

   The numbers are not arbitrary:
     · 44px is the accessibility floor for a touch target; on a ramp, in gloves,
       against vibration, we use 56px for rows and 64px+ for primary actions.
     · 16px is the floor for anything focusable — below it iOS silently zooms the
       page on focus and never zooms back (the bug fixed platform-wide in 1.13.2).
     · 13px is the floor for any text, because these screens are read at arm's
       length in sunlight, not at a desk.

   The older cargo handheld (hh-*) predates this and styles itself with ~630 inline
   style attributes; it keeps working untouched and migrates onto these components
   separately.
   ========================================================================== */
body.hhk .sidebar, body.hhk .topbar, body.hhk .mobile-nav { display: none !important; }
body.hhk .app, body.hhk .main { height: 100dvh; min-height: 0; }
body.hhk .content { padding: 0 !important; height: 100dvh; overflow: hidden; }

.hhk-screen {
  display: flex; flex-direction: column; height: 100dvh; background: var(--bg);
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
/* Header: identity + context, never competing with the task itself */
.hhk-hdr {
  display: flex; align-items: center; gap: 12px; padding: 14px 16px; min-height: 60px;
  background: var(--surface); border-bottom: 1px solid var(--border); flex: 0 0 auto;
}
.hhk-hdr h2 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.2px; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-hdr .hhk-sub { font-size: 13px; color: var(--text-3); }
/* Back is a real target, not a chevron a glove can't hit */
.hhk-back {
  min-width: 52px; min-height: 52px; display: grid; place-items: center; font-size: 22px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px;
  color: var(--text); flex: 0 0 auto; cursor: pointer;
}
.hhk-body { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 16px; }
.hhk-foot { flex: 0 0 auto; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid var(--border); background: var(--surface); display: flex; gap: 12px; }

/* Task grid — the handheld's home. Two columns so each tile stays thumb-sized. */
.hhk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.hhk-tile {
  min-height: 116px; display: flex; flex-direction: column; justify-content: center; gap: 8px;
  padding: 16px; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: 16px; color: var(--text);
}
.hhk-tile .ic { font-size: 30px; line-height: 1; }
.hhk-tile .lb { font-size: 15px; font-weight: 650; line-height: 1.25; }
.hhk-tile:active { transform: scale(.97); }

/* List rows — picking a flight, an asset, a bag */
.hhk-row {
  width: 100%; min-height: 68px; display: flex; align-items: center; gap: 12px; text-align: left;
  padding: 12px 14px; margin-bottom: 10px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; color: var(--text);
}
.hhk-row:active { transform: scale(.99); }
.hhk-row .rmain { flex: 1; min-width: 0; }
.hhk-row .rtitle { font-size: 17px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-row .rsub { font-size: 13.5px; color: var(--text-3); margin-top: 3px; }

/* Choice buttons — one decision per screen, full width, unmissable */
.hhk-choice {
  width: 100%; min-height: 64px; display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; margin-bottom: 10px; font-size: 17px; font-weight: 600; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; color: var(--text); cursor: pointer;
}
.hhk-choice.sel { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 12%, var(--surface)); }
.hhk-choice:active { transform: scale(.99); }

/* Buttons */
.hhk-btn {
  flex: 1; min-height: 60px; padding: 14px 18px; font-size: 17px; font-weight: 700;
  border-radius: 14px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); cursor: pointer;
}
.hhk-btn.primary { background: var(--brand-fill); border-color: var(--brand); color: #fff; }
.hhk-btn.danger { background: var(--danger-fill); border-color: var(--danger); color: #fff; }
.hhk-btn:disabled { opacity: .5; }
.hhk-btn:active { transform: scale(.98); }
/* A rare, reversible action ("this step doesn't apply") shouldn't be the loudest thing on the
   screen — red reads as danger, and marking a step N/A is neither dangerous nor destructive. */
.hhk-btn.quiet { background: transparent; border-color: transparent; color: var(--text-3);
  font-size: 15px; font-weight: 600; min-height: 52px; }
.hhk-btn.quiet:hover { color: var(--text-2); background: var(--surface-2); }
.hhk-quiet-row { display: flex; margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--border); }

/* Fields — 16px minimum or iOS zooms and never comes back */
.hhk-field { margin-bottom: 16px; }
.hhk-field label { display: block; font-size: 14px; font-weight: 600; color: var(--text-2); margin-bottom: 8px; }
.hhk-input, .hhk-field input, .hhk-field select, .hhk-field textarea {
  width: 100%; min-height: 56px; padding: 14px 16px; font-size: 17px; font-family: inherit;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; color: var(--text);
}
.hhk-input:focus, .hhk-field input:focus, .hhk-field select:focus, .hhk-field textarea:focus {
  border-color: var(--brand); outline: none;
}
/* Scan prompt — the primary affordance on a scan-driven screen */
.hhk-scan { text-align: center; padding: 24px 8px; }
.hhk-scan .big { font-size: 21px; font-weight: 700; margin-bottom: 10px; line-height: 1.3; }
.hhk-scan .hint { font-size: 14.5px; color: var(--text-3); margin-bottom: 20px; }

/* Read-back panel — what you are about to commit, stated plainly before you commit it */
.hhk-confirm { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 16px; margin-bottom: 16px; }
.hhk-confirm .kv { display: flex; justify-content: space-between; gap: 14px; padding: 11px 0; border-top: 1px solid var(--border-soft, var(--border)); }
.hhk-confirm .kv:first-of-type { border-top: 0; }
.hhk-confirm .k { font-size: 14px; color: var(--text-3); }
.hhk-confirm .v { font-size: 16px; font-weight: 650; text-align: right; }

.hhk-note { font-size: 14px; color: var(--text-3); line-height: 1.5; }
.hhk-empty { text-align: center; padding: 40px 16px; font-size: 16px; color: var(--text-3); }
.hhk-toast {
  position: fixed; left: 16px; right: 16px; bottom: calc(20px + env(safe-area-inset-bottom));
  padding: 16px; border-radius: 14px; font-size: 16px; font-weight: 600; text-align: center; z-index: 60;
}
.hhk-toast.ok { background: var(--ok-fill); color: #fff; }
.hhk-toast.err { background: var(--danger-fill); color: #fff; }
.hhk-tile, .hhk-btn, .hhk-row, .hhk-choice, .hhk-back { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

/* ============================================================================
   HANDHELD BOARD  (hhk-sw / hhk-step / hhk-fcard / hhk-info …)
   ----------------------------------------------------------------------------
   The record-first half of the handheld: a list of live records, a summary card
   and menu for one of them, and the BOARD — the ordered run of steps a job is
   made of, stamped as they happen.

   WHY SWIPE. Stamping a milestone is the single most repeated action on a ramp:
   dozens per turn, in gloves, one-handed, while watching an aircraft. A tap-tap-
   tap-confirm costs four deliberate touches and takes the eyes off the aircraft.
   A swipe costs one gesture that cannot be triggered by a knuckle brushing the
   screen, states its consequence under the thumb before release, and is undone
   by the same gesture. So: swipe RIGHT to stamp, swipe LEFT for the detail
   (time / fields / notes / photos). Every swipe has an equivalent tap route —
   the gesture is the fast path, never the only path.

   The row must survive being dragged while the list is being scrolled, so the
   face carries `touch-action: pan-y`: the browser keeps vertical scrolling and
   hands us horizontal movement, rather than the two fighting over the gesture.
   ========================================================================== */

/* ---- swipeable row ---- */
.hhk-sw { position: relative; overflow: hidden; border-radius: 14px; margin-bottom: 10px; background: var(--surface-2); }
.hhk-sw-pane {
  position: absolute; inset: 0; display: flex; align-items: center; gap: 10px; padding: 0 22px;
  font-size: 15px; font-weight: 750; letter-spacing: .02em; color: #fff; opacity: 0;
  border-radius: 14px; pointer-events: none;
}
/* Swiping RIGHT uncovers the left edge, so its label lives on the left (and vice versa). */
.hhk-sw-pane.go   { justify-content: flex-start; background: var(--ok); }
.hhk-sw-pane.undo { justify-content: flex-start; background: var(--warn); }
.hhk-sw-pane.info { justify-content: flex-end;   background: var(--info); }
.hhk-sw-pane.none { justify-content: flex-start; background: var(--surface-3); color: var(--text-2); }
.hhk-sw-face {
  position: relative; display: flex; width: 100%; align-items: stretch; gap: 0; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; color: var(--text);
  cursor: pointer; will-change: transform; touch-action: pan-y;
  transition: transform .22s cubic-bezier(.22,1,.36,1);
}
.hhk-sw.dragging .hhk-sw-face { transition: none; }
.hhk-sw.dragging { z-index: 2; }

/* ---- a step on the board ---- */
.hhk-step { min-height: 64px; align-items: center; padding: 0; overflow: hidden; }
/* Stacked rows (device sets, coverage warnings). A class rather than an inline margin because
   the handheld engine is held to a near-zero inline-style budget — see tests/test_mobile_ux.py,
   which is what stopped the old cargo handheld reaching ~630 inline styles. */
.hhk-step.stack { margin-bottom: 8px; }
.hhk-step .st-bar { flex: 0 0 6px; align-self: stretch; background: var(--text-3); }
.hhk-step .st-main { flex: 1 1 auto; min-width: 0; padding: 11px 12px; }
.hhk-step .st-label { display: block; font-size: 16px; font-weight: 650; line-height: 1.25; }
.hhk-step .st-owner { display: block; font-size: 13px; color: var(--text-3); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-step .st-times { flex: 0 0 auto; display: flex; align-items: center; gap: 7px; padding: 0 12px 0 4px;
  font-variant-numeric: tabular-nums; }
.hhk-step .st-t { min-width: 54px; text-align: center; padding: 5px 7px; border-radius: 8px; font-size: 14px; font-weight: 700; }
.hhk-step .st-target { background: var(--surface-2); color: var(--text-3); font-weight: 600; }
.hhk-step .st-actual { background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); }
.hhk-step .st-actual.empty { color: var(--text-3); font-weight: 600; }
/* Status boards carry a word, not a clock — it needs room to breathe and must not wrap. */
.hhk-step .st-state { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2);
  font-size: 13px; font-weight: 700; min-width: 0; white-space: nowrap; max-width: 108px;
  overflow: hidden; text-overflow: ellipsis; }
.hhk-step.is-done .st-state { background: color-mix(in srgb, var(--ok) 26%, transparent); color: var(--text); }
.hhk-step.is-late .st-state, .hhk-step.is-overdue .st-state { background: color-mix(in srgb, var(--danger) 24%, transparent); }
.hhk-step.is-due  .st-state { background: color-mix(in srgb, var(--warn) 26%, transparent); }
.hhk-step .st-flags { flex: 0 0 auto; display: flex; gap: 6px; padding-right: 12px; font-size: 13px; color: var(--text-3); }

/* State is carried by BOTH a fill and a bar — colour alone is not a signal you can
   rely on in sunlight, through a screen protector, or with colour-blindness. */
.hhk-step.is-done    { background: color-mix(in srgb, var(--ok) 16%, var(--surface)); }
.hhk-step.is-done    .st-bar { background: var(--ok); }
.hhk-step.is-late    { background: color-mix(in srgb, var(--danger) 15%, var(--surface)); }
.hhk-step.is-late    .st-bar { background: var(--danger); }
.hhk-step.is-late    .st-actual { background: color-mix(in srgb, var(--danger) 26%, transparent); }
.hhk-step.is-overdue { background: color-mix(in srgb, var(--danger) 9%, var(--surface)); }
.hhk-step.is-overdue .st-bar { background: var(--danger); }
.hhk-step.is-due     { background: color-mix(in srgb, var(--warn) 15%, var(--surface)); }
.hhk-step.is-due     .st-bar { background: var(--warn); }
.hhk-step.is-na      { opacity: .55; }
.hhk-step.is-na      .st-bar { background: var(--text-3); }
.hhk-step.is-na      .st-label { text-decoration: line-through; }

/* ---- phase heading inside the board ---- */
.hhk-phase {
  display: flex; align-items: center; gap: 10px; margin: 18px 0 10px; font-size: 13px;
  font-weight: 750; letter-spacing: .06em; text-transform: uppercase; color: var(--text-3);
}
.hhk-phase:first-child { margin-top: 2px; }
.hhk-phase::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.hhk-phase .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--brand); flex: 0 0 auto; }

/* ---- progress through the board ---- */
.hhk-prog { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.hhk-prog .bar { flex: 1; height: 8px; border-radius: 99px; background: var(--surface-3); overflow: hidden; }
.hhk-prog .bar > i { display: block; height: 100%; background: var(--ok); border-radius: 99px; transition: width .3s; }
.hhk-prog .lbl { font-size: 13px; font-weight: 700; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* ---- record summary card (the identity strip above a record's screens) ---- */
.hhk-fcard { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 14px; margin-bottom: 14px; }
.hhk-fcard-top { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.hhk-fcard-top .fc-title { font-size: 22px; font-weight: 800; letter-spacing: -.3px; }
.hhk-fcard-top .fc-sub { font-size: 14px; color: var(--text-3); margin-left: auto; }
.hhk-fcard-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border-radius: 12px; overflow: hidden; }
.hhk-fcard-grid > div { background: var(--surface); padding: 10px 6px; text-align: center; min-height: 64px;
  display: flex; flex-direction: column; justify-content: center; gap: 3px; }
.hhk-fcard-grid b { font-size: 17px; font-weight: 750; font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-fcard-grid i { font-size: 13px; font-style: normal; letter-spacing: .03em; text-transform: uppercase; color: var(--text-3); line-height: 1.2; }

/* Direction badge — arrival vs departure. The SHAPE carries it: `arrDown` and `depUp` are
   two different aircraft silhouettes, and the row's subtitle says "from DXB" / "to DXB"
   beside them, so colour is the third cue rather than the only one.

   ARRIVAL WAS `--danger`. A red filled disc on every inbound row, on the screen a ramp agent
   holds all shift — and an arrival is not a fault. Status colours are reserved for status;
   spending the strongest one in the palette on a routine direction is how a board ends up
   with a wall of red that nobody reads any more.

   Blue and teal instead — neither is a status colour, and they hold apart for a colour-blind
   reader: OKLab dE 21.3 normal / 20.3 deuteranopia / 20.9 protanopia in the light theme and
   17.3 / 14.3 / 16.0 in the dark (Vienot simulation), well past the 8 that pairs are meant to
   clear. Soft tint behind the glyph rather than a filled disc, so the mark reads at arm's
   length in daylight and the row stays calm.

   `-ink` ON `-soft`, WHICH IS THE PAIRING THOSE TOKENS EXIST FOR — the same one `.badge.info`
   and `.badge.teal` use. The first cut of this used `--info` and `--teal-fill`, which are the
   FILL colours, and `tests/test_contrast.py` measured the result at 2.65:1 in the dark theme:
   a 20px stroked glyph nobody could see on a lit apron. The ink tokens carry their measured
   ratio in a comment beside them and clear AA in both themes. */
.hhk-dir { flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; }
.hhk-dir svg { width: 20px; height: 20px; }
.hhk-dir.arr { background: var(--info-soft); color: var(--info-ink); }
.hhk-dir.dep { background: var(--teal-soft); color: var(--teal-ink); }

/* ---- menu of a record's sections ---- */
.hhk-menu-item {
  width: 100%; min-height: 60px; display: flex; align-items: center; gap: 12px; text-align: left;
  padding: 14px 16px; margin-bottom: 8px; font-size: 17px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; color: var(--text); cursor: pointer;
}
.hhk-menu-item .mi-ic { flex: 0 0 24px; display: grid; place-items: center; color: var(--text-2); }
.hhk-menu-item .mi-lb { flex: 1; min-width: 0; }
.hhk-menu-item .mi-ct { font-size: 14px; font-weight: 700; color: var(--text-3); font-variant-numeric: tabular-nums; }
.hhk-menu-item .mi-ch { color: var(--text-3); font-size: 20px; }
.hhk-menu-item.on .mi-ic { color: var(--brand); }
.hhk-menu-item.on .mi-ic svg { fill: color-mix(in srgb, var(--brand) 28%, transparent); }

/* Flat line icons — sized once here so no screen has to set width/height inline. */
.hhk-ic { width: 22px; height: 22px; display: block; flex: 0 0 auto; }
.hhk-ic-lit { font-size: 20px; line-height: 1; }
.hhk-btn .hhk-ic { display: inline-block; vertical-align: -4px; margin-right: 8px; }
.hhk-tile .ic .hhk-ic { width: 28px; height: 28px; color: var(--brand); }
.hhk-lrow .lr-star .hhk-ic { width: 18px; height: 18px; color: var(--brand); fill: color-mix(in srgb, var(--brand) 30%, transparent); }
.hhk-menu-item:disabled { opacity: .45; cursor: default; }
.hhk-menu-item:not(:disabled):active { transform: scale(.99); }

/* ---- tab strip ---- */
.hhk-tabs { display: flex; gap: 6px; margin-bottom: 14px; background: var(--surface-2); padding: 5px; border-radius: 13px; }
.hhk-tabs button {
  flex: 1; min-height: 46px; padding: 9px 6px; font-size: 14px; font-weight: 700; cursor: pointer;
  background: transparent; border: 0; border-radius: 9px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hhk-tabs button.sel { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.12); }
/* The role filter can carry eight teams. Equal flex would give each 40px and crush the labels,
   so this variant sizes to content and scrolls sideways inside its own strip — the one place
   horizontal scrolling is right, because the alternative is unreadable team names. */
.hhk-tabs.hhk-roles { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.hhk-tabs.hhk-roles::-webkit-scrollbar { display: none; }
.hhk-tabs.hhk-roles button { flex: 0 0 auto; padding: 9px 15px; }

/* ---- read-only detail grid ---- */
.hhk-info { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
.hhk-info > div { background: var(--surface); padding: 12px 6px; text-align: center; min-height: 72px;
  display: flex; flex-direction: column; justify-content: center; gap: 4px; }
.hhk-info b { font-size: 16px; font-weight: 700; word-break: break-word; }
.hhk-info i { font-size: 13px; font-style: normal; letter-spacing: .03em; text-transform: uppercase; color: var(--text-3); line-height: 1.2; }
.hhk-info .wide { grid-column: 1 / -1; }

/* ---- section bar: the handheld's peer screens ----------------------------------
   Tasks / Pickups / Flights are not a hierarchy, so they are not a back-stack. Bottom of
   the screen because that is where the thumb already is, and only at root — inside a
   record it would compete with Back. */
.hhk-secs {
  flex: 0 0 auto; display: flex; border-top: 1px solid var(--border); background: var(--surface);
  padding-bottom: env(safe-area-inset-bottom);
}
.hhk-secs button {
  flex: 1; min-height: 58px; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 3px; padding: 8px 4px; border: 0; background: transparent;
  color: var(--text-3); font-size: 13px; font-weight: 650; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.hhk-secs button.sel { color: var(--brand); }
.hhk-secs button .hhk-ic { width: 22px; height: 22px; }

/* ---- primary action in the header (safety's "Log incident") ---- */
.hhk-hdr-act {
  flex: 0 0 auto; display: flex; align-items: center; gap: 7px; min-height: 46px;
  padding: 10px 14px; border-radius: 12px; border: 0; cursor: pointer;
  background: var(--brand-fill); color: #fff; font-size: 14.5px; font-weight: 700;
  -webkit-tap-highlight-color: transparent;
}
.hhk-hdr-act:active { transform: scale(.97); }
.hhk-hdr-act .hhk-ic { width: 19px; height: 19px; }
/* On a phone the label goes and the icon carries it — the header must never wrap.
   360px WAS TOO NARROW A THRESHOLD. `.hhk-hdr h2` is the flex item that gives way (flex:1,
   min-width:0, ellipsis) while the button is flex:0 0 auto, so above the breakpoint a long
   label is paid for by the TITLE: at 390px — an iPhone 12 through 16, the single most common
   width there is — "Practical Assessment" rendered as "Practical A…" beside a full-width
   "New assessment". 430px covers every mainstream phone (390 iPhone, 393 Pixel, 430 Pro Max)
   and leaves the label on tablets, where there is room for both. */
@media (max-width: 430px) { .hhk-hdr-act span { display: none; } .hhk-hdr-act { padding: 10px 12px; } }

/* ---- station chip in the header ---- */
.hhk-port { flex: 0 0 auto; padding: 6px 11px; border-radius: 99px; font-size: 13px; font-weight: 750;
  background: var(--brand-soft); color: var(--brand-ink); white-space: nowrap; }

/* ---- date separator in a record list ---- */
.hhk-day { position: sticky; top: -16px; z-index: 1; margin: 14px -16px 10px; padding: 9px 16px;
  background: var(--surface-2); font-size: 13px; font-weight: 750; letter-spacing: .06em; color: var(--text-2); }
.hhk-day:first-child { margin-top: -6px; }

/* ---- a record row in the list (identity + a few live columns) ---- */
.hhk-lrow { min-height: 66px; align-items: center; padding: 10px 12px; gap: 11px; }
.hhk-lrow .lr-main { flex: 1 1 auto; min-width: 0; }
/* display:block, not the span default — an inline title lets a short flight number ("JQ394")
   pull its subtitle up onto the same line while a long one wraps, so the list reads as two
   different layouts depending on the data. */
.hhk-lrow .lr-title { display: block; font-size: 17px; font-weight: 750; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-lrow .lr-sub { display: block; font-size: 13.5px; color: var(--text-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-lrow .lr-time, .hhk-lrow .lr-meta { display: block; }
.hhk-lrow .lr-side { flex: 0 0 auto; text-align: right; font-variant-numeric: tabular-nums; }
.hhk-lrow .lr-time { font-size: 16px; font-weight: 700; }
.hhk-lrow .lr-meta { font-size: 13px; color: var(--text-3); margin-top: 2px; }
.hhk-lrow .lr-star { flex: 0 0 auto; font-size: 17px; opacity: .3; }
.hhk-lrow .lr-star.on { opacity: 1; }

/* ---- task card (a handheld whose RECORDS are the work: a PRM assist) --------------
   Denser than a list row because everything here is needed before you walk anywhere:
   who, which flight, which seat, which gate, from where to what service, and by when.
   The by-when is the part RSmart shows as plain text — we colour it, because an assist
   that has blown its SLA is the single most important thing on the screen. */
.hhk-card { display: block; padding: 12px 14px; min-height: 92px; }
.hhk-card .tc-top { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
.hhk-card .tc-name { flex: 1; min-width: 0; font-size: 16.5px; font-weight: 750; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-card .tc-name .anon { color: var(--text-3); font-weight: 600; }
.hhk-card .tc-pill { flex: 0 0 auto; padding: 4px 11px; border-radius: 999px; font-size: 13px; font-weight: 700; }
.hhk-card .tc-line { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; font-size: 14px; color: var(--text-2); }
.hhk-card .tc-line:last-child { margin-bottom: 0; }
.hhk-card .tc-flt { font-weight: 750; color: var(--text); font-variant-numeric: tabular-nums; }
.hhk-card .tc-seat, .hhk-card .tc-cabin { font-weight: 650; }
.hhk-card .tc-gate { margin-left: auto; font-weight: 700; color: var(--text); white-space: nowrap; }
/* The pickup point may be long and is safe to clip; the SERVICE CODE is what the agent is
   there to do, so it sits outside the truncating span and never shortens to "WC…". */
.hhk-card .tc-route { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-card .tc-ssr { flex: 0 0 auto; font-weight: 750; color: var(--brand); }
.hhk-card .tc-win { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.hhk-card .tc-agent { font-size: 13px; color: var(--text-3); margin-top: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhk-card .tc-ic { flex: 0 0 auto; width: 17px; height: 17px; color: var(--text-3); }

/* Lifecycle, encoded by fill + pill text so it survives sunlight and colour-blindness. */
.hhk-card.st-pending  { border-left: 6px solid var(--text-3); }
.hhk-card.st-assigned { border-left: 6px solid var(--info); }
.hhk-card.st-assigned .tc-pill { background: color-mix(in srgb, var(--info) 22%, transparent); color: var(--text); }
.hhk-card.st-en_route { border-left: 6px solid var(--brand); background: color-mix(in srgb, var(--brand) 7%, var(--surface)); }
.hhk-card.st-en_route .tc-pill { background: color-mix(in srgb, var(--brand) 24%, transparent); color: var(--text); }
.hhk-card.st-with_pax { border-left: 6px solid var(--warn); background: color-mix(in srgb, var(--warn) 10%, var(--surface)); }
.hhk-card.st-with_pax .tc-pill { background: color-mix(in srgb, var(--warn) 30%, transparent); color: var(--text); }
.hhk-card.st-completed { border-left: 6px solid var(--ok); background: color-mix(in srgb, var(--ok) 12%, var(--surface)); }
.hhk-card.st-completed .tc-pill { background: color-mix(in srgb, var(--ok) 26%, transparent); color: var(--text); }
.hhk-card.st-no_show, .hhk-card.st-cancelled { border-left: 6px solid var(--danger); opacity: .72; }
.hhk-card.st-no_show .tc-pill, .hhk-card.st-cancelled .tc-pill { background: color-mix(in srgb, var(--danger) 24%, transparent); color: var(--text); }
.hhk-card.st-pending .tc-pill { background: var(--surface-3); color: var(--text-2); }

/* SLA state on the time window — the thing that decides what you walk to next. */
.hhk-card .tc-win.due  { color: var(--warn); }
.hhk-card .tc-win.late { color: var(--danger); }
.hhk-card .tc-win.late::after { content: " ⚠"; }

/* Running total across the shift, the way RSmart pins "Pax Handled" above the list. */
.hhk-tally { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 12px 16px;
  border-radius: 13px; background: var(--brand-soft); }
.hhk-tally .lb { flex: 1; font-size: 15px; font-weight: 650; color: var(--text); }
.hhk-tally .n { font-size: 22px; font-weight: 800; color: var(--brand); font-variant-numeric: tabular-nums; }
.hhk-tally .n small { font-size: 13px; font-weight: 600; color: var(--text-3); margin-left: 5px; }

/* ---- the handheld's menu -----------------------------------------------------------
   Slides up from the bottom, because that is where the thumb already is. It replaced a ✕
   in the header: on a device held one-handed at an aircraft, a single stray touch on that
   ✕ dropped the operator onto the mobile home with no route back except retyping a URL.
   Leaving is now a decision with a panel in front of it. */
.hhk-sheet-bg {
  position: fixed; inset: 0; z-index: 70; display: flex; align-items: flex-end;
  background: rgba(0, 0, 0, 0); transition: background .18s;
}
.hhk-sheet-bg.open { background: rgba(0, 0, 0, .45); }
.hhk-sheet {
  width: 100%; max-height: 88dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg); border-radius: 20px 20px 0 0;
  padding: 10px 16px calc(18px + env(safe-area-inset-bottom));
  transform: translateY(100%); transition: transform .22s cubic-bezier(.22,1,.36,1);
}
.hhk-sheet-bg.open .hhk-sheet { transform: translateY(0); }
.hhk-sheet-grab { width: 44px; height: 5px; border-radius: 99px; background: var(--surface-3);
  margin: 2px auto 14px; }
.hhk-sheet-id { padding: 0 4px 14px; border-bottom: 1px solid var(--border); margin-bottom: 14px; }
.hhk-sheet-id .nm { font-size: 18px; font-weight: 750; }
.hhk-sheet-id .sub { font-size: 14px; color: var(--text-3); margin-top: 3px; }
.hhk-sheet-lbl { font-size: 13px; font-weight: 750; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-3); margin: 16px 4px 8px; }
.hhk-sheet-lbl:first-of-type { margin-top: 2px; }
.hhk-menu-item.danger { color: var(--danger); }
/* A COUNT BADGE IS NEUTRAL UNTIL ITS STATE SAYS OTHERWISE. This rule used to paint every
   `.mi-ct` --danger-fill with no condition on it, so "Milestones 31/31" on a finished turn was
   an alarm sitting directly under a full green progress bar, and "0 running" services was the
   same red as a service running late. The tone class comes from `handheld._countTone`; white on
   each -fill step is the 4.5:1 pairing recorded beside the token. */
.hhk-menu-item .mi-ct:not(:empty) { background: var(--surface-3); color: var(--text-2);
  border-radius: 999px; padding: 2px 9px; font-size: 13px; }
.hhk-menu-item .mi-ct.ok:not(:empty)   { background: var(--ok-fill); color: #fff; }
.hhk-menu-item .mi-ct.warn:not(:empty) { background: var(--warn-fill); color: #fff; }
.hhk-menu-item .mi-ct.bad:not(:empty)  { background: var(--danger-fill); color: #fff; }
/* An unread alert is bolder than one already seen — the only difference that matters here. */
.hhk-log.unread { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 7%, var(--surface)); }
.hhk-log.tap { cursor: pointer; }
.hhk-menu-item.danger .mi-ic { color: var(--danger); }
@media (prefers-reduced-motion: reduce) {
  .hhk-sheet { transition: none; }
  .hhk-sheet-bg { transition: none; }
}

/* ---- photo strip in a step's detail ---- */
.hhk-shots { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; margin-bottom: 14px; }
.hhk-shots figure { margin: 0; position: relative; aspect-ratio: 1; border-radius: 11px; overflow: hidden; border: 1px solid var(--border); background: var(--surface-2); }
.hhk-shots img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hhk-shots .rm { position: absolute; top: 4px; right: 4px; width: 30px; height: 30px; border-radius: 50%;
  border: 0; background: rgba(0,0,0,.62); color: #fff; font-size: 15px; cursor: pointer; }

/* ---- a note in a step's log ---- */
.hhk-log { margin-bottom: 10px; padding: 11px 13px; border-radius: 12px; background: var(--surface); border: 1px solid var(--border); }
.hhk-log .who { font-size: 13px; color: var(--text-3); margin-bottom: 4px; }
.hhk-log .what { font-size: 15px; line-height: 1.45; white-space: pre-wrap; }
/* A raw telex is fixed-width by nature — the columns in an LDM or a PTM carry meaning,
   so it wraps nowhere and scrolls inside its own box rather than widening the screen. */
.hhk-raw { margin: 8px 0 0; padding: 10px 12px; border-radius: 10px; background: var(--surface-2);
  font-family: var(--mono); font-size: 13px; line-height: 1.5;
  overflow-x: auto; white-space: pre; -webkit-overflow-scrolling: touch; }

/* Hint shown once, above a board, teaching the gesture. */
.hhk-swipehint { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 12px;
  padding: 10px 12px; border-radius: 11px; background: var(--surface-2); font-size: 13px; color: var(--text-2); }

/* ---- layout utilities ----------------------------------------------------
   Small, named, and here rather than in the engine. The older cargo handheld reached ~630
   inline style attributes precisely by solving each of these one tag at a time, and no rule
   can enforce a contract against a style attribute. tests/test_mobile_ux.py holds the engine
   to single figures so this stays true. */
.hhk-note.gap { display: block; margin-bottom: 14px; }
.hhk-btn.wide { width: 100%; flex: 0 0 auto; }
.hhk-btn.fixed { flex: 0 0 96px; }
.hhk-btn.gap { margin-bottom: 14px; }
.hhk-btn.gap-lg { margin-bottom: 18px; }
.hhk-prog.gap-lg { margin-bottom: 16px; }
.hhk-phase.spaced { margin-top: 22px; }
.hhk-field.tight { margin-bottom: 12px; }
.hhk-inline { display: flex; gap: 10px; }
.hhk-input.grow { flex: 1; min-width: 0; }
.hhk-hdr .hhk-sub { display: block; font-weight: 500; }

.hhk-sw-face, .hhk-menu-item, .hhk-tabs button { -webkit-tap-highlight-color: transparent; }
@media (prefers-reduced-motion: reduce) {
  .hhk-sw-face { transition: none; }
  .hhk-prog .bar > i { transition: none; }
}

/* ---- Builder — Canva-style block authoring surface ---- */
.cbx { position: fixed; inset: 0; z-index: 55; background: var(--bg); display: none; flex-direction: column; }
.cbx-bar { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--bg-2); }
.cbx-title { flex: 1; min-width: 0; font-size: 17px; font-weight: 650; background: transparent; border: 1px solid transparent; border-radius: 8px; padding: 6px 10px; color: var(--text); }
.cbx-title:hover { border-color: var(--border); } .cbx-title:focus { border-color: var(--brand); background: var(--surface); outline: none; }
.cbx-bar-actions { display: flex; gap: 8px; align-items: center; }
.cbx-body { flex: 1; display: grid; grid-template-columns: 190px minmax(0,1fr) 320px; min-height: 0; }
.cbx-palette { border-right: 1px solid var(--border); background: var(--bg-2); padding: 14px 12px; overflow-y: auto; }
.cbx-pal-title { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-3); margin-bottom: 10px; }
.cbx-pal { display: flex; align-items: center; gap: 9px; padding: 9px 11px; margin-bottom: 7px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); cursor: grab; font-size: 13px; font-weight: 550; user-select: none; }
.cbx-pal:hover { border-color: var(--brand); background: var(--surface-2); }
.cbx-pal:active { cursor: grabbing; }
.cbx-pal-ico { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 6px; background: var(--brand-soft); color: var(--brand-ink); font-size: 13px; }
.cbx-pal-hint { font-size: 11px; color: var(--text-3); margin-top: 14px; line-height: 1.5; }
.cbx-canvas { overflow-y: auto; padding: 28px 20px 120px; background: var(--bg); }
.cbx-page { max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 2px; }
.cbx-empty { text-align: center; color: var(--text-3); padding: 70px 20px; border: 2px dashed var(--border); border-radius: 14px; }
.cbx-empty .big { font-size: 34px; margin-bottom: 10px; }
.cbx-block { position: relative; padding: 12px 40px 12px 30px; border: 2px solid transparent; border-radius: 12px; cursor: pointer; }
.cbx-block:hover { border-color: var(--border); background: var(--surface); }
.cbx-block.sel { border-color: var(--brand); background: var(--surface); box-shadow: 0 0 0 3px var(--brand-soft); }
.cbx-block.dragging { opacity: .4; }
.cbx-block.drop-above { box-shadow: -2px -9px 0 -6px var(--brand); }
.cbx-block.drop-below { box-shadow: -2px 9px 0 -6px var(--brand); }
.cbx-block-inner { pointer-events: none; }
.cbx-handle { position: absolute; left: 8px; top: 12px; color: var(--text-3); cursor: grab; font-size: 15px; opacity: 0; }
.cbx-block:hover .cbx-handle, .cbx-block.sel .cbx-handle { opacity: 1; }
.cbx-del { position: absolute; right: 8px; top: 10px; width: 24px; height: 24px; border-radius: 7px; border: 1px solid var(--border); background: var(--surface-2); color: var(--danger-ink); cursor: pointer; opacity: 0; font-size: 12px; }
.cbx-block:hover .cbx-del, .cbx-block.sel .cbx-del { opacity: 1; }
.cbx-props { border-left: 1px solid var(--border); background: var(--bg-2); padding: 16px; overflow-y: auto; }
.cbx-props-title { font-weight: 640; font-size: 14px; margin-bottom: 8px; }
.cbx-props-sub { color: var(--text-3); font-size: 12px; margin: 0 0 14px; }
.cbx-tpl { display: flex; align-items: center; gap: 10px; padding: 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }
@media (max-width: 860px) {
  .cbx-body { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
  .cbx-palette { display: flex; flex-wrap: wrap; gap: 6px; border-right: 0; border-bottom: 1px solid var(--border); }
  .cbx-pal { margin: 0; } .cbx-pal-title, .cbx-pal-hint { width: 100%; }
  .cbx-props { border-left: 0; border-top: 1px solid var(--border); max-height: 40vh; }
}
/* Section marker in the builder canvas */
.cbx-section { display: flex; align-items: center; gap: 10px; color: var(--brand); font-weight: 650; font-size: 13px; }
.cbx-section::before, .cbx-section::after { content: ""; flex: 1; height: 2px; background: var(--brand-soft); border-radius: 2px; }

/* ---- Learner player — fullscreen, section-by-section ---- */
.lpx { position: fixed; inset: 0; z-index: 58; background: var(--bg); display: none; flex-direction: column; }
.lpx-bar { display: flex; align-items: center; gap: 14px; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--bg-2); }
.lpx-bar h3 { margin: 0; font-size: 16px; font-weight: 650; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lpx-count { color: var(--text-3); font-size: 12.5px; font-weight: 600; white-space: nowrap; }
.lpx-track { height: 4px; background: var(--surface-2); }
.lpx-track > div { height: 100%; background: var(--brand); transition: width .35s; }
.lpx-stage { flex: 1; overflow-y: auto; padding: 40px 24px 60px; }
.lpx-page { max-width: 760px; margin: 0 auto; }
.lpx-page h2.lpx-sec { font-size: 24px; margin: 0 0 20px; font-weight: 700; }
.lpx-nav { display: flex; align-items: center; gap: 14px; padding: 14px 20px; border-top: 1px solid var(--border); background: var(--bg-2); }
.lpx-dots { flex: 1; display: flex; gap: 6px; justify-content: center; }
.lpx-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--surface-3); cursor: pointer; }
.lpx-dot.done { background: var(--brand); } .lpx-dot.cur { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.lpx-gate { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-3); margin: 6px 0 14px; }
.lpx-gate.done { color: var(--ok); }
.lpx-result { max-width: 520px; margin: 0 auto; text-align: center; }
.lpx-result .big { font-size: 52px; font-weight: 800; margin: 8px 0; }

/* Assessment question media (image/video stimulus above the options) + hotspot questions */
.qmedia { margin: 2px 0 12px; }
.qmedia img { display: block; max-width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--line); }
.qmedia-empty { padding: 14px; border: 1px dashed var(--line); border-radius: 8px; color: var(--text-3); font-size: 13px; }
.hsimg-wrap { position: relative; display: inline-block; max-width: 100%; line-height: 0; }
.hsimg-wrap img { display: block; max-width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--line); user-select: none; -webkit-user-drag: none; }
.hsimg-wrap.take { cursor: crosshair; }
.hsreg { position: absolute; box-sizing: border-box; border: 2px dashed var(--brand); background: color-mix(in srgb, var(--brand) 16%, transparent); border-radius: 4px; pointer-events: none; }
.hsreg.live { pointer-events: auto; }
.hsreg.ghost { border-style: solid; background: color-mix(in srgb, var(--brand) 22%, transparent); }
.hsreg-del { position: absolute; top: -10px; right: -10px; width: 20px; height: 20px; border-radius: 50%; border: 0;
  background: var(--danger-fill); color: #fff; font-size: 11px; line-height: 20px; text-align: center; cursor: pointer; padding: 0; }
.hsmark { position: absolute; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%;
  border: 3px solid var(--brand); background: color-mix(in srgb, var(--brand) 35%, transparent); pointer-events: none; box-shadow: 0 0 0 2px #fff; }
.hsedit-wrap { position: relative; display: inline-block; max-width: 100%; line-height: 0; cursor: crosshair; touch-action: none; }
.hsedit-wrap img { display: block; max-width: 100%; height: auto; border-radius: 8px; user-select: none; -webkit-user-drag: none; }
.hsedit-layer { position: absolute; inset: 0; }

/* ---- Load Control LIR — fullscreen top-down loading-instruction diagram ---- */
.lirx { position: fixed; inset: 0; z-index: 58; background: var(--bg); display: none; flex-direction: column; }
.lir-bar { display: flex; align-items: center; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--border); background: var(--bg-2); flex-wrap: wrap; }
.lir-bar h3 { margin: 0; font-size: 16px; font-weight: 700; }
.lir-wb { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }
.lir-live { color: var(--ok); font-size: 12px; font-weight: 700; white-space: nowrap; }
.lir-prog { padding: 10px 18px; border-bottom: 1px solid var(--line); background: var(--bg-2); }
.lir-stage { flex: 1; overflow-y: auto; padding: 22px 18px 60px; }
/* THE DECK IS THE AIRCRAFT, seen from above: long and thin, nose-to-tail ACROSS the
   screen. It used to be a column of full-width boxes — the plan view rotated ninety
   degrees, three wide slabs down a tall page on screens that are wider than they are tall.
   Holds now sit side by side, each station is a column, and L stacks over R inside it,
   which is the same geometry as the printed LIR. `fwd-right` puts the nose on the right to
   match the carrier sheet's `FWD ->`. */
/* The whole aircraft has to FIT: a 777 is 22 stations, and fixed-width cells simply ran
   off the end of the screen with the forward hold amputated. Holds flex-grow in proportion
   to how many stations they hold (set inline by lirDeck), so the diagram fills the width
   and stays in the right proportions; cells shrink to a legible floor and only then does
   it scroll. */
.lir-deck { width: 100%; display: flex; flex-direction: row; align-items: stretch; gap: 8px;
  min-height: 186px; overflow-x: auto; overflow-y: hidden; padding-bottom: 6px; }
.lir-nose { flex: 0 0 54px; align-self: stretch; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 12px 90px 90px 12px / 12px 120px 120px 12px; display: flex; align-items: center; justify-content: center;
  color: var(--text-3); font-size: 17px; }
.lir-deck:not(.fwd-right) .lir-nose { border-radius: 90px 12px 12px 90px / 120px 12px 12px 120px; }
.lir-tail { flex: 0 0 34px; align-self: stretch; margin: 26px 0; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 60px 12px 12px 60px; }
.lir-deck:not(.fwd-right) .lir-tail { border-radius: 12px 60px 60px 12px; }
/* THE HOLDS ARE SIZED BY WHAT IS IN THEM, AND THE DECK IS WHAT SCROLLS.
   `flex: 1 1 0` with `min-width: 0` let every level collapse to whatever the container could
   spare, while `.lir-cell`'s own `min-width` floor refused to follow — so the cells painted
   straight over their hold's border instead of making the deck wide enough to scroll, which
   it has been able to do all along (`overflow-x: auto`, above).
   Measured on a B777 in the 653px record drawer: the bulk hold 38px wide holding 76px of
   content, every station column 20px holding 34px, and FORTY-SIX cells drawn outside their
   parent. `flex-shrink: 0` and a content basis is what makes the deck's own scrollbar the
   thing that gives. `flex-grow` is still set per hold inline, so a nine-station forward hold
   is still visibly longer than a seven-station aft one wherever there is room to spare. */
.lir-hold { border: 1px solid var(--line); border-radius: 10px; padding: 8px 9px; background: var(--surface-1);
  display: flex; flex-direction: column; flex: 1 0 auto; }
.lir-hold.bulk, .lir-hold.pallet { background: var(--surface-2); }
.lir-hold-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .5px; margin-bottom: 7px; color: var(--text-2); white-space: nowrap; }
/* stations run along the fuselage; L over R within each */
.lir-rows { display: flex; flex-direction: row; gap: 4px; flex: 1 0 auto; }
.lir-row { display: flex; flex-direction: column; gap: 4px; flex: 1 0 auto; }
.lir-pallets { display: flex; flex-direction: row; gap: 4px; flex: 1 0 auto; align-items: stretch; }
.lir-bulk { display: flex; flex: 1 0 auto; }
.lir-cell { flex: 1 1 0; min-width: 62px; border: 1px solid var(--line); border-radius: 6px; padding: 4px 6px; font-size: 10px;
  background: var(--surface-2); display: flex; flex-direction: column; gap: 1px; line-height: 1.22; min-height: 62px;
  justify-content: center; overflow: hidden; }
.lir-bulk .lir-cell, .lir-hold.bulk .lir-cell { min-width: 92px; }
.lir-cell.tap { cursor: pointer; }
.lir-cell.tap:hover { outline: 2px solid var(--brand); outline-offset: 1px; }
.lir-cell.empty { border-style: dashed; color: var(--text-3); align-items: center; background: transparent; }
.lir-cell.planned { border-left: 4px solid var(--text-3); }
.lir-cell.loaded { border-left: 4px solid var(--info); background: color-mix(in srgb, var(--info) 12%, var(--surface-2)); }
.lir-cell.verified { border-left: 4px solid var(--ok); background: color-mix(in srgb, var(--ok) 14%, var(--surface-2)); }
.lir-cell.offloaded { border-left: 4px solid var(--warn); background: color-mix(in srgb, var(--warn) 12%, var(--surface-2)); }
/* No line of a cell may be squashed below its own line box. The column flex was compressing
   them to fit `min-height`, which measured as `scrollHeight 11 > clientHeight 4` on the ULD
   serials — text rendered four pixels tall. */
.lir-cell > span { flex: none; }
.lir-cell-pos { font-weight: 700; font-size: 10px; color: var(--text-2); }
.lir-cell-uld { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lir-cell-ser { font-weight: 700; font-size: 11px; letter-spacing: .2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lir-cell-kg { font-size: 9.5px; font-weight: 600; color: var(--text-2); white-space: nowrap; }
.lir-cell-sub { color: var(--text-3); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lir-cell-code { color: var(--text-3); font-weight: 600; }
.lir-tray { margin: 16px auto 0; border: 1px solid var(--warn); border-radius: 10px; padding: 10px 12px; background: color-mix(in srgb, var(--warn) 8%, var(--surface-1)); }
.lir-tray-head { font-size: 12px; font-weight: 700; margin-bottom: 8px; }
.lir-tray-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.lir-chip { border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; font-size: 11px; background: var(--surface-2); cursor: pointer; }
.lir-chip:hover { outline: 2px solid var(--brand); }
.lir-legend { margin: 16px auto 0; display: flex; flex-wrap: wrap; gap: 14px; font-size: 11.5px; color: var(--text-2); align-items: center; }
.lir-legend i.lir-sw { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
.lir-sw.planned { background: var(--text-3); } .lir-sw.loaded { background: var(--info); } .lir-sw.verified { background: var(--ok); } .lir-sw.offloaded { background: var(--warn); }
/* read-only mini diagram in the detail drawer */
.lir-deck.mini { gap: 5px; min-height: 0; }
.lir-deck.mini .lir-nose, .lir-deck.mini .lir-tail { display: none; }
.lir-deck.mini .lir-hold { padding: 6px 7px; }
/* `flex: none` so a mini cell is as tall as its own three lines. With the default
   `flex: 1 1 0` from the base rule its height came from the ROW, which took it from the
   hold and ultimately from the tallest thing on the deck — so the position, type and
   serial were clipped at `scrollHeight 33 > clientHeight 26` no matter how much room the
   drawer had. The deck scrolls sideways; it has no reason to ration height. */
.lir-deck.mini .lir-cell { flex: none; min-width: 34px; min-height: 28px; font-size: 8.5px; padding: 2px 3px; }
.lir-deck.mini .lir-cell-uld { font-size: 8.5px; }
.lir-deck.mini .lir-cell-sub, .lir-deck.mini .lir-bags, .lir-deck.mini .lir-cell-kg { display: none; }
.lir-deck.mini .lir-cell-ser { font-size: 9px; }
/* BRS bag-clearance badge overlaid on a ULD cell */
.lir-bags { display: inline-block; margin-top: 3px; font-size: 9.5px; font-weight: 700; padding: 1px 5px; border-radius: 999px;
  background: color-mix(in srgb, var(--ok) 22%, transparent); color: var(--text); }
.lir-bags.off { background: color-mix(in srgb, var(--danger) 28%, transparent); }
/* a table row flagged as a security exception (BRS must-offload) */
tr.row-danger > td { background: color-mix(in srgb, var(--danger) 8%, transparent); }

/* ---------------------------------------------- Passenger Services duty board */
/* Its own KPI strip rather than reusing .prm-kpi-strip: that one is hard-coded to
   repeat(6,1fr), and a 7th cell dropped to a second row on its own, which read as a
   layout bug. auto-fit means the count can change without the grid caring. */
/* FLEX, not an auto-fit grid: the strip carries a divider between the tiles that want a
   decision and the tiles that are context, and a grid would give that divider a full column
   the width of a tile. */
.ps-kpis { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.ps-kpis > .ps-kpi { flex: 1 1 132px; min-width: 132px; }
/* The alert tiles and the context tiles are different KINDS of number, and eight of identical
   weight read as a wall with no hint which want acting on. Collapses to nothing when the row
   wraps, where a vertical rule would be pointing at the wrong thing. */
.ps-kpi-split { flex: 0 0 1px; align-self: stretch; background: var(--border);
                margin: 2px 4px; }
@media (max-width: 900px) { .ps-kpi-split { display: none; } }
.ps-kpi { background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm); padding: 12px 14px; min-width: 0; }
.ps-kpi-val { font-size: 26px; font-weight: 800; line-height: 1.05; }
.ps-kpi-lbl { font-size: 10.5px; text-transform: uppercase; letter-spacing: .6px;
  color: var(--text-3); font-weight: 700; margin-top: 3px; }

/* Type filter chips. A row of .btn.ghost looked like a toolbar of actions; these read as
   what they are — a filter with a count. */
.ps-chips { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 14px; }
.ps-chip { display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface);
  color: var(--text-2); font-size: 12px; font-family: inherit; cursor: pointer; }
.ps-chip:hover { background: var(--surface-2); color: var(--text); }
.ps-chip.on { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--text); }
.ps-chip b { font-variant-numeric: tabular-nums; }
.ps-chip .ps-chip-short { color: var(--danger); font-weight: 700; font-variant-numeric: tabular-nums; }

/* A flight and the positions it needs — deliberately the same construction as PRM's flight
   table, because the two boards answer the same shape of question and reading as one product
   matters more than either being individually clever. The group fills every column it has;
   its positions live in a nested table with a column set of their own. */
/* The board toolbar: search, port, airline, and how many rows survived the filters. */
.ps-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 10px 0 8px; }
.ps-toolbar .input.sm { flex: 0 1 380px; width: 380px; min-width: 200px; }

/* A SEARCH BOX IN A CARD HEAD, sized like one. See the note above: `max-width` cannot win
   against the base form rule's five-`:not()` selector, so this uses `width`, which that rule
   never sets. Without it the box grew to fill the head and pushed the range control off the
   end — which is what "the search box is missing the formatting other search boxes have"
   looked like on the History tab. */
.ps-search { flex: 0 1 320px; width: 320px; min-width: 180px; }
@media (max-width: 900px) { .ps-search { flex: 1 1 100%; width: 100%; } }

/* The head of a tab that owns its whole card — title, count, then its controls hard right.
   `card-head`'s own margin-bottom is for a head sitting above padded card content; here the
   content is a table flush to the card edge, so the rule below trades it for a divider. */
.ps-tab-head { padding: 12px 14px; margin: 0; border-bottom: 1px solid var(--border-soft); }

/* The history summary sits INSIDE its card now, between the head and the rows, so it drops
   the outer margins it had when it floated on the page. */
.ps-hist-summary { margin: 12px 14px; }

/* Padding for the non-table contents of a `card pad-0` — a loader, an empty state, an error.
   The card has none of its own by design (its tables run flush to the edge), so anything that
   is not a table needs it back. Named rather than inlined because the first version of this
   reached for `class="pad"`, which this stylesheet does not define — the same mistake as the
   plan tab's `class="row"`, made in the change that fixed it. */
.ps-card-body { padding: 8px 14px 16px; }
/* The Positions card head carries the two board actions now — they used to sit in the page
   head, where "Fill open positions" changed rows nobody could see from the top of the page,
   and showed on the Report and Roster tabs it has nothing to do with. Wraps rather than
   crowds: the head already holds a title, a count and a two-way segmented control. */
.ps-list-head { flex-wrap: wrap; gap: 8px; }
.ps-list-head .btn { flex: 0 0 auto; }
/* The demand chart's legend, under the columns rather than beside them — the columns already
   use the full width. */
.ps-demand-legend { display: flex; gap: 16px; padding: 2px 2px 4px; font-size: 12px; color: var(--text-2); }
.ps-demand-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px;
  margin-right: 6px; vertical-align: -1px; }

/* A ROW OF FORM FIELDS IN A MODAL. Nine of these were written `class="row" style="gap:10px"`
   with `flex:1` on the children — and `.row` is not a class this stylesheet defines, so none
   of them was a flex container, the inline `gap` did nothing, and every one of those fields
   stacked instead of sitting beside its neighbour. Not `.field-row`, which is a two-column
   grid: three of these rows carry three fields, and two of them size themselves with
   `max-width`, which only means anything in a flex row.
   `flex-end` so the controls line up with each other rather than with their labels, the same
   way `.ps-plan-row` does. */
.ps-form-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.ps-form-row > .field { margin: 0; }

/* Two table cells the module aims its tests and its async fills at. They earn a rule rather
   than an exemption from the undefined-class sweep: both hold chips that must not wrap into a
   second line and push the row's height around. */
.ps-flight-flags, .ps-shifts { white-space: nowrap; }
.ps-count { margin-left: auto; font-size: 12px; color: var(--text-3);
            font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-demand { margin: 4px 0 12px; }

/* A KPI tile that filters. Only the ones that MEAN a filter get this — a tile that looks
   clickable and does nothing is worse than a plain number. */
.ps-kpi-act { cursor: pointer; }
.ps-kpi-act:hover { border-color: var(--brand); }
.ps-kpi-act.on { border-color: var(--brand);
                 background: color-mix(in srgb, var(--brand) 10%, transparent); }

.ps-flight-row td { cursor: pointer; }
.ps-flight-row:hover { background: var(--surface-2); }
.ps-flight-row.open { background: var(--surface-2); }
/* The shortfall cue sits on the FLIGHT cell, not the expander, so it survives a collapsed
   group — the whole point being that you can see a flight needs people without opening it. */
.ps-flight-row.ps-flight-flag td:nth-child(2) { box-shadow: inset 3px 0 0 var(--danger); }
.ps-flight-row .ps-exp { color: var(--text-3); font-size: 13px; }
.ps-flight-row .ps-exp svg { width: 13px; height: 13px; vertical-align: -1px; }
.ps-flight-detail > td { padding: 0 16px 12px !important; background: var(--bg-2); }
.ps-pos-table { width: 100%; border-collapse: collapse; }
.ps-pos-table td { padding: 9px 10px; border-bottom: 1px solid var(--border-soft);
                   font-size: 12.5px; vertical-align: middle; }
.ps-pos-table tr:last-child td { border-bottom: none; }
.ps-pos-table .ps-pos-act { text-align: right; white-space: nowrap; }
.ps-pos-short td:first-child { box-shadow: inset 2px 0 0 var(--danger); }

/* Table. Times and counts are tabular so columns line up when they scroll past. */
.ps-table td { vertical-align: middle; }
.ps-pos { font-weight: 600; }
.ps-ref { font-size: 10.5px; color: var(--text-3); white-space: nowrap; }
.ps-when { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-when b { font-weight: 600; }
.ps-rel { font-size: 11px; color: var(--text-3); }
.ps-rel.late { color: var(--danger); font-weight: 600; }
.ps-rel.soon { color: var(--warn); font-weight: 600; }
.ps-fill { font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--text-3); }
.ps-fill.short { color: var(--danger); font-weight: 700; }
.ps-who { font-size: 12px; color: var(--text-2); }
.ps-who.none { color: var(--text-3); font-style: italic; }
.ps-acts { white-space: nowrap; text-align: right; }

/* The report's three dropdowns. `port-select` gives them the toolbar's height, radius and
   drawn chevron; this gives them ONE WIDTH. A native select sizes to its longest option, so
   "14 days", "All ports" and "All carriers" came out at three different widths in a row of
   three — which reads as three unrelated controls rather than one filter set. */
/* A FIXED WIDTH, not a minimum. `min-width` left the browser free to size each control to its
   own longest OPTION, which is why "All carriers" — whose list holds "Virgin Australia" —
   came out sixty pixels wider than "All ports". The three are one filter set and have to look
   like one. The value shown is short; the list itself is drawn by the OS at its own width. */
.ps-rep-filter { width: 190px; text-overflow: ellipsis; }
@media (max-width: 620px) { .ps-rep-filter { width: 100%; } }

/* The report's help line is a third line inside a tile sized for two, so on the last tile of
   a full row it ran under the card edge. Wrapping is the fix, not a narrower sentence. */
.ps-kpi .ps-ref { white-space: normal; margin-top: 4px; line-height: 1.35; }

/* The plan tab's prose. 70ch is the readability rule and it is right, but left as a bare
   max-width in a full-width card it stranded four lines of text against an empty right half.
   Given as a class so both notes share it and so the rule can say why. */
.ps-plan-note { font-size: 12.5px; margin: 0 0 14px; max-width: 68ch; line-height: 1.55; }

/* The generator row mixes labelled fields with bare buttons. A `.field` is label-over-control
   and a button is not, so on one centred flex line the buttons floated against the middle of
   the fields. Align to the BOTTOM and give the buttons the control height, so everything sits
   on the same baseline as the inputs rather than the labels. */
.ps-plan-row { align-items: flex-end; gap: 10px; }
.ps-plan-acts { display: flex; gap: 8px; align-items: flex-end; }

/* A late count in a table of counts. The column is otherwise a wall of tabular numbers and
   the one that wants attention has to be findable without reading the header. */
.ps-late-cell { color: var(--danger); font-weight: 700; }

/* THE CLOCK, in the drawer. Its own block rather than a badge in the header, because it
   carries two facts — the state and where the deadline came from — and a carrier reading a
   time with no provenance beside it will take a module default for an agreed figure. */
.ps-sla-chip { display: flex; gap: 9px; align-items: flex-start; margin: 12px 0 0;
  padding: 9px 11px; border-radius: var(--radius-sm); font-size: 12.5px;
  border: 1px solid var(--border); background: var(--surface-2); }
.ps-sla-chip svg { flex: none; width: 16px; height: 16px; color: var(--text-3); }
.ps-sla-chip .ps-ref { white-space: normal; }
.ps-sla-chip.ok { border-color: color-mix(in srgb, var(--ok) 35%, transparent);
  background: color-mix(in srgb, var(--ok) 8%, transparent); }
.ps-sla-chip.ok svg { color: var(--ok); }
.ps-sla-chip.warn { border-color: color-mix(in srgb, var(--warn) 40%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent); }
.ps-sla-chip.warn svg { color: var(--warn); }
.ps-sla-chip.danger { border-color: color-mix(in srgb, var(--danger) 40%, transparent);
  background: color-mix(in srgb, var(--danger) 9%, transparent); }
.ps-sla-chip.danger svg { color: var(--danger); }

/* The trail. Newest first, one row per event, the time hard right so a column of them can be
   scanned without reading any of the prose. */
.ps-timeline { margin-top: 4px; }
.ps-tl-row { display: flex; gap: 10px; align-items: flex-start; padding: 7px 0;
  border-bottom: 1px solid var(--border); }
.ps-tl-row:last-child { border-bottom: 0; }
.ps-tl-ic { flex: none; display: inline-flex; color: var(--text-3); }
.ps-tl-ic svg { width: 15px; height: 15px; }
.ps-tl-body { min-width: 0; font-size: 12.5px; }
.ps-tl-body .ps-ref { white-space: normal; }
.ps-tl-when { margin-left: auto; white-space: nowrap; font-size: 11px; color: var(--text-3);
  font-variant-numeric: tabular-nums; }

/* Somebody who has clocked off is still ON the position — releasing them is the
   supervisor's call, not a side effect of the sick call — but they are not cover, so the
   name is struck rather than reading like everybody else's. */
.ps-gone { text-decoration: line-through; color: var(--danger); }

/* WHAT THE REPORT COULD NOT MEASURE is a caveat, not a fault. Sharing `.ps-warn` put a red
   banner across the top of a working report, which reads as "this is broken" rather than
   "here is the part of it you should not lean on". Amber, and the only difference is that. */
.ps-note { margin: 12px 0; padding: 9px 11px; border-radius: var(--radius-sm);
  font-size: 12.5px; color: var(--text);
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent); }

/* The one thing on a position that contradicts its own headcount. Loud enough to stop a
   scan, quiet enough not to compete with the shortfall figure. */
.ps-warn, .tk-warn { margin: 10px 0; padding: 9px 11px; border-radius: var(--radius-sm);
  font-size: 12.5px; color: var(--text);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  background: color-mix(in srgb, var(--danger) 9%, transparent); }

/* One position, opened from the board: the facts first, as label-over-value pairs that
   reflow rather than a table nobody can read on a phone at a gate. */
.ps-detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px 14px; font-size: 13px; }
.ps-detail .ps-ref { display: block; margin-bottom: 1px; }

/* ---- record drawer on a phone -----------------------------------------------------
   Placed at the END deliberately. A media query carries no extra specificity, so these
   sat before .drawer-body's base rule and lost the cascade to it — the padding stayed
   24px and the fix looked like it had simply not worked. Measured, not guessed:
   .timeline ran 27px wide of its container at 375px. */
@media (max-width: 760px) {
  .drawer-body { padding: 16px; }
  .drawer-head { padding-left: 16px; padding-right: 16px; }
  .timeline { padding-left: 22px; }
  .tl-item::before { left: -18px; }
  .drawer-body .grid[class*="cols"] { grid-template-columns: 1fr; }
  /* A long ref, serial or email wraps rather than setting the drawer's width. The audit
     timeline was the last 5px: "· jamie.ashton@demo.skycentrix.app" has no break opportunity. */
  .drawer-body .mono, .drawer-body .kv-v, .drawer-body td,
  .drawer-body .timeline, .drawer-body .tl-item { overflow-wrap: anywhere; }
}

/* =====================================================================
   Chart primitives — stacked bars, columns, heatmap, meters, tooltip
   and the "every number is a way in" hit affordance.
   ===================================================================== */

/* ---- hit targets -------------------------------------------------------------------
   A mark that stands for a set of records IS a link: it must say so to the mouse, the
   keyboard and a screen reader. One rule covers every chart because UI.hit() stamps the
   same data-hit on all of them. :focus-visible (not :focus) so a mouse click doesn't
   leave a ring behind, and the SVG variants are separate because stroke/fill — not
   background — are what an SVG mark has to work with. */
[data-hit] { cursor: pointer; }
[data-hit]:focus { outline: none; }
[data-hit]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
svg [data-hit]:focus-visible, svg[data-hit]:focus-visible { outline: none; }
svg g[data-hit]:focus-visible rect:not(.col-hit),
svg g[data-hit]:focus-visible circle { stroke: var(--text); stroke-width: 2; }
svg circle[data-hit]:focus-visible { filter: brightness(1.35); }

/* ---- shared chart tooltip ---------------------------------------------------------- */
.chart-tip {
  position: fixed; top: 0; left: 0; z-index: 3000; pointer-events: none;
  max-width: 260px; padding: 7px 10px; border-radius: 9px;
  background: var(--surface-3); border: 1px solid var(--border);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .35);
  opacity: 0; transition: opacity .12s ease;
}
.chart-tip.open { opacity: 1; }
.chart-tip .tip-k { font-size: 12px; font-weight: 640; color: var(--text); line-height: 1.35; }
.chart-tip .tip-v { font-size: 11.5px; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* ---- stacked horizontal bars ------------------------------------------------------- */
.sbar-chart { display: flex; flex-direction: column; gap: 9px; padding: 6px 2px 4px; }
.sbar-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.sbar-lbl {
  flex: 0 0 38%; max-width: 190px; font-size: 11.5px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sbar-lbl[data-hit]:hover { color: var(--text); }
.sbar-track {
  flex: 1 1 auto; min-width: 0; height: 17px; border-radius: 5px; overflow: hidden;
  background: var(--panel-2); display: flex; align-items: center;
}
.sbar-fill { display: flex; height: 100%; min-width: 3px; border-radius: 5px; overflow: hidden; transition: width .35s; }
.sbar-seg { display: block; height: 100%; min-width: 2px; transition: filter .12s ease; }
.sbar-seg[data-hit]:hover { filter: brightness(1.25); }
.sbar-none { font-size: 10.5px; color: var(--text-3); padding-left: 7px; }
.sbar-val {
  flex: 0 0 auto; min-width: 34px; text-align: right; font-size: 11.5px;
  font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums;
}
.sbar-legend { margin-top: 12px; }
.legend span[data-hit]:hover { color: var(--text); }
.legend-pct { color: var(--text-3); font-variant-numeric: tabular-nums; }

/* ---- column chart ------------------------------------------------------------------ */
.col-chart { display: block; width: 100%; overflow: visible; }
.col-chart g[data-hit] rect:not(.col-hit) { transition: filter .12s ease; }
.col-chart g[data-hit]:hover rect:not(.col-hit) { filter: brightness(1.25); }
.col-chart g[data-hit]:hover .col-hit { fill: var(--surface-3); fill-opacity: .5; }

/* ---- heatmap ----------------------------------------------------------------------- */
/* Scrolls inside its own card rather than setting the page width — six ports fit on a
   laptop, a station list does not. */
.heat-wrap { overflow-x: auto; padding: 2px 0 4px; }
/* Drawn at its natural size and SCROLLED when the card is narrower, never scaled down —
   shrinking the drawing shrinks the numbers printed in the cells, which are the point. */
.heat { display: block; }
.heat g[data-hit] rect { transition: fill-opacity .12s ease, stroke .12s ease; }
.heat g[data-hit]:hover rect { fill-opacity: 1; }

/* ---- meter (bullet) ---------------------------------------------------------------- */
.meter {
  position: relative; height: 5px; border-radius: 3px; background: var(--panel-2);
  overflow: hidden; margin-top: 8px;
}
.meter-fill { display: block; height: 100%; border-radius: 3px; transition: width .4s; }
.meter-tick { position: absolute; top: -2px; width: 2px; height: 9px; background: var(--text-3); }

/* ---- KPI tiles that are also destinations ------------------------------------------
   The department rollup's numbers each stand for a list. They get a pointer, a lift on
   hover and a caret that only appears then — the tile has to read as static until the
   reader is on it, or four blue links compete with the numbers themselves. */
.kpi.kpi-link { transition: transform .12s ease, border-color .12s ease, background .12s ease; }
.kpi.kpi-link:hover { transform: translateY(-2px); border-color: var(--brand); }
.kpi.kpi-link .kpi-go {
  position: absolute; right: 14px; top: 14px; font-size: 13px; color: var(--brand);
  opacity: 0; transform: translateX(-3px); transition: opacity .12s ease, transform .12s ease;
}
.kpi.kpi-link:hover .kpi-go, .kpi.kpi-link:focus-visible .kpi-go { opacity: 1; transform: none; }
.kpi.kpi-link.is-active { border-color: var(--brand); background: var(--brand-soft); }
.kpi .foot [data-hit] {
  color: var(--warn); font-weight: 640; border-bottom: 1px dashed currentColor; padding-bottom: 1px;
}
.kpi .foot [data-hit]:hover { color: var(--text); }

/* ---- department rollup: drill-down panel ------------------------------------------- */
.drill { border-color: var(--brand); }
.drill-head { display: flex; align-items: center; gap: 10px; padding: 14px 18px 10px; flex-wrap: wrap; }
.drill-head h3 { margin: 0; font-size: 15px; font-weight: 640; }
.drill-head .drill-count { font-size: 12.5px; color: var(--text-3); }
.drill-close {
  margin-left: auto; background: none; border: 1px solid var(--border); color: var(--text-2);
  border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer;
}
.drill-close:hover { color: var(--text); border-color: var(--brand); }
.drill-note { padding: 9px 18px; font-size: 12px; color: var(--text-3); border-top: 1px solid var(--border-soft); }

/* ---- deep-linked filter chips on a module list -------------------------------------- */
.filter-chips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
.chip-filter {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 6px 4px 11px;
  border-radius: 999px; font-size: 12px; font-weight: 620;
  background: var(--brand-soft); color: var(--brand-ink); border: 1px solid var(--brand);
}
.chip-filter .x {
  background: none; border: 0; color: inherit; cursor: pointer; font-size: 12px;
  line-height: 1; padding: 2px 4px; border-radius: 50%; opacity: .75;
}
.chip-filter .x:hover { opacity: 1; background: rgba(255, 255, 255, .12); }

@media (max-width: 760px) {
  .sbar-lbl { flex-basis: 44%; max-width: 44%; }
  .kpi.kpi-link:hover { transform: none; }
}

/* Row count on a module's record list — the arriving half of a dashboard click-through. */
.rec-count { font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Department rollup — what the page could not count. Never styled as a normal muted note:
   an unreadable area reads as zero everywhere above it, and that has to be visible. */
.dash-caveat {
  margin-bottom: 16px; padding: 10px 14px; border-radius: 10px;
  border: 1px solid var(--border-soft); color: var(--text-3);
  font-size: 12.5px; display: flex; flex-direction: column; gap: 4px;
}
/* A missing area is not a footnote — it is the page telling you a number is wrong. */
.dash-caveat.gap { background: var(--warn-soft); border-color: var(--warn); color: var(--text-2); }

/* ============================================================================
   In-app assistant — topbar button + chat panel
   The panel is a plain fixed element on <body>, NOT a drawer or a modal:
   `App.router()` closes both of those on every navigation, and the chat has to
   survive navigating (answering "show me that flight" means going there while
   the conversation stays open).
   ============================================================================ */
.asst-btn { gap: 7px; }
.asst-btn.active { border-color: var(--brand); color: var(--brand); }
@media (max-width: 900px) { .asst-btn-label { display: none; } }

.assistant-panel {
  position: fixed; right: 18px; bottom: 18px; z-index: 60;
  width: min(420px, calc(100vw - 36px));
  height: min(560px, calc(100vh - 120px));
  display: none; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
  box-shadow: 0 18px 48px rgba(0,0,0,.42);
}
.assistant-panel.open { display: flex; }

.asst-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px; border-bottom: 1px solid var(--border-soft);
  background: var(--surface-2); flex-shrink: 0;
}
.asst-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 13px; color: var(--text); }
.asst-title svg { color: var(--brand); }
.asst-actions { display: flex; align-items: center; gap: 6px; }
.asst-icon {
  background: none; border: 1px solid transparent; color: var(--text-3);
  border-radius: var(--radius-sm); padding: 3px 8px; font-size: 12px; cursor: pointer;
}
.asst-icon:hover { color: var(--text); border-color: var(--border); }

.asst-log { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }

.asst-msg { max-width: 88%; padding: 9px 12px; border-radius: 12px; font-size: 13px; line-height: 1.5; word-wrap: break-word; }
.asst-msg.you { align-self: flex-end; background: var(--brand-fill); color: #fff; border-bottom-right-radius: 4px; }
.asst-msg.asst { align-self: flex-start; background: var(--surface-3); color: var(--text); border-bottom-left-radius: 4px; }
.asst-msg code { background: rgba(0,0,0,.28); padding: 1px 5px; border-radius: 5px; font-size: 12px; }

/* A tool call, so a pause has a visible reason rather than looking hung. */
.asst-tool { align-self: flex-start; font-size: 11px; color: var(--text-3); font-style: italic; padding-left: 4px; }

.asst-empty { margin: auto 0; text-align: center; color: var(--text-3); font-size: 13px; }
.asst-empty p { margin: 0 0 12px; }
.asst-suggest { display: flex; flex-direction: column; gap: 6px; }
.asst-suggest button {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-2);
  border-radius: var(--radius-sm); padding: 7px 10px; font-size: 12px; cursor: pointer; text-align: left;
}
.asst-suggest button:hover { border-color: var(--brand); color: var(--text); }

.asst-form {
  display: flex; align-items: flex-end; gap: 8px; padding: 10px;
  border-top: 1px solid var(--border-soft); background: var(--surface-2); flex-shrink: 0;
}
.asst-form textarea {
  flex: 1; resize: none; background: var(--surface); border: 1px solid var(--border);
  color: var(--text); border-radius: var(--radius-sm); padding: 8px 10px;
  font-size: 13px; font-family: inherit; outline: none; line-height: 1.4; max-height: 140px;
}
.asst-form textarea:focus { border-color: var(--brand); }
.asst-form textarea::placeholder { color: var(--text-3); }
.asst-send {
  background: var(--brand-fill); border: none; color: #fff; border-radius: var(--radius-sm);
  width: 34px; height: 34px; display: grid; place-items: center; cursor: pointer; flex-shrink: 0;
}
.asst-send:disabled { opacity: .5; cursor: default; }

@media (max-width: 620px) {
  .assistant-panel { right: 10px; left: 10px; bottom: 10px; width: auto; height: min(70vh, 520px); }
}

/* Assistant action card — a server-rendered offer, not model prose.
   Visually distinct from a message bubble on purpose: the user must be able to tell at a
   glance that this is something the platform will DO, not something the assistant said. */
.asst-action {
  align-self: stretch; border: 1px solid var(--brand); border-radius: 12px;
  background: var(--brand-soft); padding: 11px 12px; display: flex;
  flex-direction: column; gap: 8px;
}
.asst-action.is-done { border-color: var(--accent); background: var(--accent-soft); }
.asst-action.is-failed { border-color: var(--danger, #e5484d); background: rgba(229,72,77,.10); }
.ac-effect { font-size: 13px; color: var(--text); line-height: 1.45; }
.ac-move { font-size: 11px; color: var(--text-2); font-family: var(--mono); }
.ac-btn {
  background: var(--brand-fill); border: none; color: #fff; border-radius: var(--radius-sm);
  padding: 8px 12px; font-size: 13px; font-weight: 600; cursor: pointer; align-self: flex-start;
}
.ac-btn:disabled { opacity: .6; cursor: default; }
.ac-done { font-size: 13px; font-weight: 600; color: var(--accent-dark, var(--accent)); }
.ac-failed { font-size: 13px; font-weight: 600; color: var(--danger, #e5484d); }
.ac-foot { font-size: 11px; color: var(--text-3); }
.ac-foot code { background: rgba(0,0,0,.20); padding: 1px 5px; border-radius: 4px; }


/* =====================================================================
   Interface consistency pass
   Every rule here exists because the same element was rendering at a
   different size, weight or radius depending on which screen you were on.
   ===================================================================== */

:root {
  --control-h-sm: 30px;
  --control-h-lg: 44px;
  --icon-btn: 36px;          /* every icon-only control, everywhere */
  --ease: cubic-bezier(.4, 0, .2, 1);
  --pad-tight: 10px;
  --pad: 16px;
  --pad-loose: 24px;
}

/* Icon-only buttons were 28px, 34px and 38px on different screens. One size. */
.icon-btn, .drawer-head .close, .modal-head .close, .bell-btn, .theme-btn {
  width: var(--icon-btn); height: var(--icon-btn);
  display: inline-grid; place-items: center; flex-shrink: 0;
  border-radius: 10px; background: var(--surface-2);
  border: 1px solid var(--border); color: var(--text-2);
  transition: background .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
}
.icon-btn:hover, .drawer-head .close:hover, .modal-head .close:hover,
.bell-btn:hover, .theme-btn:hover { background: var(--surface-3); color: var(--text); }

/* A pressed control should acknowledge the press. Applied once, not per-button. */
.btn:active, .icon-btn:active { transform: translateY(1px); }
.btn:focus-visible, .icon-btn:focus-visible, .input:focus-visible,
select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px;
}

/* Destructive actions should not be the loudest thing on a read-only screen. Keeping the
   red for the label and the border rather than a full fill stops "Delete" out-shouting the
   record it belongs to. */
.btn.danger-soft {
  background: transparent; border-color: var(--danger, #dc4646);
  color: var(--danger, #dc4646);
}
.btn.danger-soft:hover { background: var(--danger-soft, rgba(220,70,70,.12)); }

/* Form density. The edit drawer packed label+field pairs with no vertical rhythm. */
.form-row + .form-row, .field + .field { margin-top: var(--pad); }
/* ...but inside a .field-row those fields are side-by-side grid COLUMNS, not stacked
   siblings. The adjacent-sibling rule above still matched the right-hand one and pushed it
   down by --pad while its neighbour stayed put, so every two-column form on the platform
   had its second field sitting a step low — Email/Role on My Account is where it shows
   worst. The grid's own `gap` already spaces these on both axes, including when the row
   collapses to one column on mobile. */
.field-row > .field + .field, .field-row > .form-row + .form-row { margin-top: 0; }
label, .field-label { display: block; margin-bottom: 6px; font-size: 12.5px;
  font-weight: 600; color: var(--text-2); }
.field-help { margin-top: 5px; font-size: 11.5px; color: var(--text-3); line-height: 1.5; }

/* A checkbox grid — the Covers / Stations blocks. Fixed columns forced awkward wrapping;
   auto-fit lets them reflow to the space actually available. */
.check-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px 18px; }
.check-grid label { display: flex; align-items: center; gap: 9px; margin: 0;
  font-weight: 500; color: var(--text); cursor: pointer; }
/* A list long enough to bury what follows it scrolls itself. The Access dialog's Roles block
   is 40 entries — 1206px of it — so Groups, Ports and the Save button all sat below the fold,
   and an admin who came to change a PORT scrolled the whole role catalogue to reach it. The
   cap only bites when the list is actually long. Padding so a focus ring is not clipped by
   the scroll box. */
.check-grid.scroll { max-height: 270px; overflow-y: auto; padding: 4px 2px; }

/* Section headings inside forms: the uppercase rules were defined three times. */
.form-section { margin: 28px 0 14px; padding-bottom: 8px;
  border-bottom: 1px solid var(--border-soft, var(--border));
  font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-3); }
.form-section:first-child { margin-top: 0; }

/* Entrance motion. Subtle and short — a board an operator reloads every minute must not
   feel like it is animating at them.

   SCOPED TO `.view-enter`, which app.js puts on the fresh #content node for one navigation
   and removes once the first paint resolves. Unscoped, this rule fired on every element
   render, and the live poller repaints #modContent every 20 seconds: eleven cards slid up
   again, every twenty seconds, while an operator was reading the board. The rule was already
   written to avoid exactly that; it just had no way to tell a navigation from a refresh. */
/* THE SAME ENTRANCE FOR A TAB CHANGE, which is why `.pane-enter` is a second SELECTOR on
   these rules rather than a second set of keyframes: arriving at a screen from the sidebar
   animated, while moving between tabs on that same screen replaced the whole page body with a
   hard cut. One entrance, two names, so the two cannot drift into different motion.

   `UI.tabEnter` puts `.pane-enter` on the PANE container and only when the tab key actually
   moved. That guard is the whole reason this is safe: every tabbed board here also repaints
   its pane from a live poller, from a filter keystroke and after a save, which is the defect
   described above. */
@keyframes sx-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.view-enter .card, .view-enter .panel, .view-enter .kpi,
.pane-enter .card, .pane-enter .panel, .pane-enter .kpi { animation: sx-rise .22s var(--ease) both; }

/* The page itself moves as one, slightly ahead of its cards, so a navigation reads as a
   change of screen rather than a flicker of contents. */
@keyframes sx-view-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.view-enter, .pane-enter { animation: sx-view-in .26s var(--ease) both; }
/* A short stagger across the first row of tiles. Capped at six: past that it stops reading
   as sequence and starts reading as lag on a board somebody is waiting to use. */
.view-enter .grid > .card:nth-child(1),  .view-enter .grid > .kpi:nth-child(1)  { animation-delay: .02s; }
.pane-enter .grid > .card:nth-child(1),  .pane-enter .grid > .kpi:nth-child(1)  { animation-delay: .02s; }
.view-enter .grid > .card:nth-child(2),  .view-enter .grid > .kpi:nth-child(2)  { animation-delay: .05s; }
.pane-enter .grid > .card:nth-child(2),  .pane-enter .grid > .kpi:nth-child(2)  { animation-delay: .05s; }
.view-enter .grid > .card:nth-child(3),  .view-enter .grid > .kpi:nth-child(3)  { animation-delay: .08s; }
.pane-enter .grid > .card:nth-child(3),  .pane-enter .grid > .kpi:nth-child(3)  { animation-delay: .08s; }
.view-enter .grid > .card:nth-child(4),  .view-enter .grid > .kpi:nth-child(4)  { animation-delay: .11s; }
.pane-enter .grid > .card:nth-child(4),  .pane-enter .grid > .kpi:nth-child(4)  { animation-delay: .11s; }
.view-enter .grid > .card:nth-child(5),  .view-enter .grid > .kpi:nth-child(5)  { animation-delay: .13s; }
.pane-enter .grid > .card:nth-child(5),  .pane-enter .grid > .kpi:nth-child(5)  { animation-delay: .13s; }
.view-enter .grid > .card:nth-child(6),  .view-enter .grid > .kpi:nth-child(6)  { animation-delay: .15s; }
.pane-enter .grid > .card:nth-child(6),  .pane-enter .grid > .kpi:nth-child(6)  { animation-delay: .15s; }

/* ---- Modal entrance. The backdrop already faded; the panel simply appeared, so a dialog
   arrived with no sense of coming from anywhere. Matches the drawer's easing and duration so
   the two overlay surfaces feel like one system. */
.modal { transform: translateY(10px) scale(.985); opacity: 0;
  transition: transform .22s var(--ease), opacity .18s var(--ease); }
.modal-backdrop.open > .modal, .modal-backdrop.open .modal { transform: none; opacity: 1; }

/* ---- Interactive feedback. These are the three surfaces a user clicks most and the only
   ones with no state change at all: a card that opens something, a segmented control and a
   tab. Hover is deliberately reserved for things that DO something — a static KPI tile that
   lifts under the cursor is noise pretending to be affordance. */
.card[onclick], .card.clickable, a.card { cursor: pointer;
  transition: transform .14s var(--ease), box-shadow .16s var(--ease), border-color .16s var(--ease); }
.card[onclick]:hover, .card.clickable:hover, a.card:hover {
  transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--border-strong, var(--border)); }
.card[onclick]:active, .card.clickable:active, a.card:active { transform: translateY(0); }

.seg button, .tabs button, .tab {
  transition: background .15s var(--ease), color .15s var(--ease), box-shadow .15s var(--ease); }
.seg button:active, .tabs button:active, .tab:active { transform: scale(.97); }

/* A press should be felt on every button, not just the styled ones. */
.btn:active { transform: translateY(1px); }

/* Rows in a data table are click targets on most boards; give the press the same weight. */
table.data tbody tr { transition: background .12s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  /* Durations alone are not enough — a transform of 0ms still MOVES. These are the rules
     that displace something, so they are neutralised outright. */
  .view-enter, .view-enter .card, .view-enter .panel, .view-enter .kpi,
  .pane-enter, .pane-enter .card, .pane-enter .panel, .pane-enter .kpi { animation: none !important; }
  .card[onclick]:hover, .card.clickable:hover, a.card:hover,
  .seg button:active, .tabs button:active, .tab:active, .btn:active { transform: none !important; }
  .modal { transform: none !important; }
}

/* =====================================================================
   Flight view — the search screen and today's active turns
   It was a bare centred form on a flat background: no card, no hierarchy,
   and a raw browser date control, so it read as unstyled beside every
   other screen.
   ===================================================================== */
.fv-search { max-width: 880px; margin: 5vh auto 0; padding: 0 20px 60px; }

.fv-hero { text-align: center; padding: 40px 32px 32px; }
.fv-hero-icon { color: var(--brand); display: grid; place-items: center; margin-bottom: 14px; }
.fv-hero-icon svg { width: 34px; height: 34px; }
.fv-hero h1 { margin: 0 0 8px; font-size: 26px; letter-spacing: -.01em; }
.fv-hero p { margin: 0 auto 26px; font-size: 13.5px; max-width: 52ch; line-height: 1.6; }

.fv-form { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.fv-num {
  flex: 1 1 260px; max-width: 320px; text-transform: uppercase;
  font-family: var(--mono);
  font-size: 15px; letter-spacing: .06em; text-align: center;
}
/* The browser's native date control renders its own placeholder and icon at its own size,
   which is why it looked foreign next to the text input. Matching the height and chrome is
   the most we can do without replacing it outright — and a native picker is better on
   mobile than anything hand-rolled. */
.fv-date { flex: 0 0 172px; }
.fv-hint { margin-top: 14px; font-size: 12px; }

.fv-turns { margin-top: 34px; }
.fv-turns-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.fv-turns-head h3 { margin: 0; font-size: 14px; }
.fv-empty { padding: 22px; text-align: center; font-size: 13px;
  border: 1px dashed var(--border); border-radius: var(--radius); }

.fv-turn-grid { display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
.fv-turn {
  text-align: left; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 14px 16px; cursor: pointer;
  display: flex; flex-direction: column; gap: 7px;
  transition: border-color .15s var(--ease), transform .12s var(--ease),
              box-shadow .15s var(--ease);
}
.fv-turn:hover {
  border-color: var(--brand); transform: translateY(-2px);
  box-shadow: 0 8px 22px rgba(0,0,0,.16);
}
.fv-turn-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.fv-turn-no { font-weight: 700; font-size: 15px; letter-spacing: .02em; }
.fv-turn-meta { font-size: 12px; }
.fv-turn-time { font-size: 12px; font-family: var(--mono); color: var(--text-2); }

/* The AI settings card's capability list. */
.ai-caps { margin: 0 0 18px; padding-left: 20px; font-size: 13px; color: var(--text-2);
  line-height: 1.7; }
.ai-caps li { margin-bottom: 5px; }
.ai-caps b { color: var(--text); }
.ok-text { color: var(--ok); }

/* =====================================================================
   Command centre (#/now)
   Density matters here more than anywhere else in the platform: this is
   read at a glance, mid-shift, to find the handful of things that need a
   person. Everything is sized so a row is scannable without focusing.
   ===================================================================== */
.now-wrap { padding: 4px 0 60px; }
.now-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
.now-title { margin: 0 0 4px; font-size: 22px; letter-spacing: -.01em; }
.now-sub { font-size: 13px; }
.now-ports { display: flex; flex-wrap: wrap; gap: 6px; }

.now-tiles { display: grid; gap: 12px; margin-bottom: 20px;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.now-tile { text-align: left; padding: 14px 16px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--border); cursor: pointer;
  transition: border-color .15s var(--ease), transform .12s var(--ease); }
.now-tile:hover { transform: translateY(-2px); }
.now-tile.on { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand) inset; }
.now-tile-n { font-size: 28px; font-weight: 700; line-height: 1.1; }
.now-tile-l { font-size: 13px; font-weight: 600; margin-top: 2px; }
.now-tile-s { font-size: 11px; margin-top: 3px; }
.now-tile.breaching .now-tile-n { color: var(--danger, #dc4646); }
.now-tile.at_risk   .now-tile-n { color: var(--warn, #e6a01e); }
.now-tile.on_track  .now-tile-n { color: var(--ok); }

/* "Nothing is breaching" must look like an ANSWER, not an empty state — a blank board and a
   healthy board are the same pixels otherwise, and the difference matters at 05:00. */
.now-clear { display: flex; align-items: center; gap: 14px; padding: 16px 18px;
  border: 1px solid var(--ok); background: var(--ok-soft); border-radius: var(--radius);
  margin-bottom: 20px; font-size: 13px; }
.now-clear-mark { font-size: 22px; color: var(--ok); }

.now-tablewrap { overflow-x: auto; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface); }
.now-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 760px; }
.now-table th { text-align: left; padding: 11px 14px; font-size: 10.5px;
  letter-spacing: .07em; text-transform: uppercase; color: var(--text-3);
  border-bottom: 1px solid var(--border); white-space: nowrap; }
.now-table td { padding: 12px 14px; border-bottom: 1px solid var(--border-soft, var(--border));
  vertical-align: middle; }
.now-table tr:last-child td { border-bottom: none; }
.now-row.breaching { background: rgba(220, 70, 70, .06); }
.now-row.at_risk   { background: rgba(230, 160, 30, .05); }
.now-row.acked     { opacity: .62; }
.now-when { font-family: var(--mono); white-space: nowrap; }
.now-row.breaching .now-when { color: var(--danger, #dc4646); }
.now-ref { font-weight: 600; white-space: nowrap; }
.now-mod { font-size: 11px; margin-left: 8px; }
.now-target { white-space: nowrap; }
/* The provenance of a target is small but never hidden: "contract" and "module default" are
   commercially different claims. */
.now-src { display: inline-block; margin-left: 6px; font-size: 10px; color: var(--text-3);
  text-transform: uppercase; letter-spacing: .04em; }
.now-act { display: flex; gap: 6px; justify-content: flex-end; white-space: nowrap; }
.now-foot { margin-top: 14px; font-size: 12px; line-height: 1.6; }

@media (max-width: 700px) {
  .now-tiles { grid-template-columns: repeat(2, 1fr); }
  .now-tile-n { font-size: 22px; }
}

/* ============================== 16:9 slide player + builder frames ==============================
   One slide fills as much of the stage as the viewport allows while HOLDING 16:9 — width-bound
   on wide screens, height-bound on short ones. Typography uses container-query units so the
   same slide reads identically in a small window and on a projector: the text scales with the
   slide, not the browser. */
.lpx-stage-slides { display: grid; place-items: center; padding: 24px; overflow: hidden; }
.lpx-slide169 {
  aspect-ratio: 16 / 9;
  width: min(100%, calc((100dvh - 190px) * 16 / 9));  /* 190px ≈ bar + track + nav */
  max-height: 100%;
  container-type: size;
  background: var(--bg-2); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: 0 18px 50px rgba(0,0,0,.16);
  overflow: hidden; display: flex; flex-direction: column;
}
.sld { padding: 5cqh 5cqw; }
.sld > * { min-height: 0; }
.sld-title { font-size: 6.2cqh; font-weight: 750; line-height: 1.15; margin: 0 0 3cqh; letter-spacing: -.01em; }
.sld p { font-size: 3.4cqh; line-height: 1.55; }
.sld h3 { font-size: 4.2cqh; }
.sld .card { font-size: 3cqh; }
.sld-flow { overflow-y: auto; min-height: 0; flex: 1; }
.sld-center { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; overflow-y: auto; }
.sld-center .sld-title { font-size: 8cqh; margin-bottom: 2.5cqh; }
.sld-statement { font-size: 6.5cqh; font-weight: 700; line-height: 1.3; max-width: 80cqw; margin: 0 auto;
  white-space: pre-wrap; }
.sld-cols { flex: 1; display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 4cqw; min-height: 0; }
.sld-cols.flip { direction: rtl; } .sld-cols.flip > * { direction: ltr; }
.sld-cols.even .sld-body { overflow-y: auto; }
.sld-media { display: grid; place-items: center; min-height: 0; }
.sld-media figure, .sld-media .lpx-vid { margin: 0; width: 100%; height: 100%; display: grid; place-items: center; }
.sld-media img, .sld-media video { max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; border-radius: 10px; }
.sld-media-empty { color: var(--text-3); font-size: 3cqh; border: 2px dashed var(--border);
  border-radius: 12px; padding: 6cqh 4cqw; }
.sld-body { overflow-y: auto; min-height: 0; align-self: center; width: 100%; }
.sld-full { position: relative; flex: 1; margin: -5cqh -5cqw; min-height: 0; display: grid; }
.sld-full .sld-media { position: absolute; inset: 0; padding: 2cqh; }
.sld-full .sld-media img, .sld-full .sld-media video { border-radius: 0; max-height: 100%; }
.sld-full-strip { position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2.5cqh 5cqw; background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(6px); }
.sld-full-strip .sld-title { font-size: 4.5cqh; margin: 0 0 1cqh; }
/* slide backgrounds */
.lpx-slide169.bg-tint, .cbx-frame.bg-tint > .cbx-frame-body { background: color-mix(in srgb, var(--brand) 7%, var(--bg-2)); }
.lpx-slide169.bg-brand, .cbx-frame.bg-brand > .cbx-frame-body { background: color-mix(in srgb, var(--brand) 88%, #000); }
.lpx-slide169.bg-brand { color: #fff; } .lpx-slide169.bg-brand .muted { color: rgba(255,255,255,.75); }
.lpx-slide169.bg-dark, .cbx-frame.bg-dark > .cbx-frame-body { background: #14202b; }
.lpx-slide169.bg-dark { color: #e8eef4; } .lpx-slide169.bg-dark .muted { color: rgba(232,238,244,.7); }

/* Builder canvas frames: the slide's CHROME — number, layout, background — around the same
   draggable blocks. The true rendering is one ▶ Preview away, deliberately (a half-faithful
   canvas imitation would teach authors the wrong picture). */
.cbx-frame { border: 1.5px solid var(--border); border-radius: 14px; margin: 0 0 18px; overflow: hidden;
  background: var(--bg-2); box-shadow: 0 4px 14px rgba(0,0,0,.05); }
.cbx-frame-head { display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  background: var(--surface); border-bottom: 1px solid var(--border-soft); }
.cbx-frame-no { font-size: 11px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--text-3); }
.cbx-frame-lay { font-size: 11.5px; color: var(--brand); font-weight: 650;
  background: color-mix(in srgb, var(--brand) 10%, transparent); padding: 2px 9px; border-radius: 20px; }
.cbx-frame-body { padding: 14px 16px 8px; }

/* A skeleton must never inherit the entrance animation — the placeholder would rise in, then
   the real content would rise in behind it, which reads as the page loading twice. */
.view-enter .sk-card { animation: none !important; }
.sk-card { pointer-events: none; }
/* The shared `.skeleton` ramp is surface-on-surface, which is nearly invisible on the light
   theme at 12px — a placeholder you cannot see is just an empty box. Mixed against the text
   colour instead, so it reads on both themes without shouting. */
.sk-card .skeleton {
  display: block; border-radius: 6px;
  background: linear-gradient(90deg,
      color-mix(in srgb, var(--text) 9%, transparent) 25%,
      color-mix(in srgb, var(--text) 16%, transparent) 37%,
      color-mix(in srgb, var(--text) 9%, transparent) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.4s infinite;
}

/* Learner-transcript search results. CAPPED AND SCROLLED rather than left to grow: the hit
   list shows up to twelve people, and twelve full-width rows push the record the reader came
   for below the fold — so the search would hide the thing it just found. The cap is on the
   LIST, not on the matches; the foot underneath still states the true total. */
.ti-hits { max-height: 264px; overflow-y: auto; margin-bottom: 6px; }

/* ── Arrivals wall ──────────────────────────────────────────────────────────
   A screen left open on a wall, so it is sized for a room rather than a desk: the
   scope holds its aspect at any width, the tile numbers carry from across an office,
   and nothing depends on hover — a wall display has no pointer on it. */
.wall-wrap { padding: 4px 0 60px; }
.wall-loading { padding: 60px; text-align: center; color: var(--text-3); }
.wall-head { display: flex; align-items: flex-start; justify-content: space-between;
             gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
.wall-title { margin: 0 0 2px; font-size: 26px; letter-spacing: -0.01em; }
.wall-sub { font-size: 13px; }
.wall-ports { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.wall-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }

.wall-tiles { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 14px; }
.wall-tile { background: var(--surface); border: 1px solid var(--border);
             border-radius: 12px; padding: 12px 14px; }
.wall-tile-n { font-size: 30px; font-weight: 650; line-height: 1.05; letter-spacing: -0.02em; }
.wall-tile-l { font-size: 13px; font-weight: 600; margin-top: 2px; }
.wall-tile-s { font-size: 11px; }
.wall-tile.warn  { border-color: var(--warn);   background: var(--warn-soft); }
.wall-tile.warn .wall-tile-n { color: var(--warn-ink, var(--warn)); }
.wall-tile.danger { border-color: var(--danger); background: var(--danger-soft); }
.wall-tile.danger .wall-tile-n { color: var(--danger-ink, var(--danger)); }
.wall-tile.ok .wall-tile-n { color: var(--ok-ink, var(--ok)); }

/* Map beside board. The scope is given the larger share because it is the half read from
   a distance; the list is the half read up close. */
.wall-main { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(320px, 0.75fr);
             gap: 14px; align-items: start; }
@media (max-width: 1100px) { .wall-main { grid-template-columns: 1fr; }
                             .wall-tiles { grid-template-columns: repeat(3, 1fr); }
                             /* Stacked, the two halves no longer share a row, so the cap that
                                keeps them level side by side would only shrink both. */
                             .wall-scope svg { max-width: 100%; min-width: 0; }
                             .wall-list { max-height: none; } }

.wall-scope { background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
              padding: 8px; }
/* The viewBox is square, so capping WIDTH caps height and the disc keeps its circle. Capped
   against the viewport because the whole point of this screen is that a room reads it without
   anybody walking over to scroll: unbounded, the 1000-unit scope rendered a metre tall on a
   wide monitor and pushed its own compass rose below the fold. */
.wall-scope svg { width: 100%; max-width: calc(100vh - 372px); min-width: 420px;
                  height: auto; display: block; margin: 0 auto; }
.wall-disc { fill: var(--wall-sea); }
/* FILLED, not outlined. Stroke-only coastlines on a near-tone disc were all but invisible
   from a metre away, which for a wall display is the whole failure. */
.wall-land { fill: var(--wall-land); stroke: var(--wall-land-line); stroke-width: 0.9;
             stroke-linejoin: round; }
.wall-rim { fill: none; stroke: var(--wall-land-line); stroke-width: 1; opacity: 0.9; }
.wall-ring { fill: none; stroke: var(--wall-land-line); stroke-width: 1; opacity: 0.55;
             stroke-dasharray: 3 7; }
.wall-spoke { stroke: var(--wall-land-line); stroke-width: 1; stroke-dasharray: 2 9; opacity: 0.45; }
/* Haloed, because a range label lands wherever the ring does — over ocean on one station and
   over the middle of Asia on the next, and it has to stay legible on both. */
.wall-ring-lbl { fill: var(--text-3); font-size: 12px; paint-order: stroke;
                 stroke: var(--wall-sea); stroke-width: 3.5px; stroke-linejoin: round; }
.wall-compass { fill: var(--text-3); font-size: 17px; font-weight: 600; text-anchor: middle; }

/* Sector and marker carry the SAME three states as the list beside them, from the same field —
   see `_severity` in backend/arrivals_wall.py. Colour here is a second reading of a badge that
   already says it in words, never the only one. */
.wall-sector { stroke: var(--ok); stroke-width: 1.6; opacity: 0.5; }
.wall-sector.warn   { stroke: var(--warn);   opacity: 0.72; stroke-width: 1.9; }
.wall-sector.danger { stroke: var(--danger); opacity: 0.9;  stroke-width: 2.2; }
.wall-sector.sel { opacity: 1; stroke-width: 3.4; }
.wall-port line { stroke: var(--wall-land-line); stroke-width: 1; }
.wall-port circle { fill: var(--text-3); }
.wall-port text { fill: var(--text-2); font-size: 13px; text-anchor: middle; font-weight: 600;
                  paint-order: stroke; stroke: var(--wall-sea); stroke-width: 3.5px;
                  stroke-linejoin: round; }
.wall-home circle { fill: var(--brand); }
.wall-home text { fill: var(--text); font-size: 18px; font-weight: 700; text-anchor: middle;
                  paint-order: stroke; stroke: var(--wall-sea); stroke-width: 4px;
                  stroke-linejoin: round; }

.wall-plane { cursor: pointer; }
.wall-plane-hit { fill: transparent; }
.wall-plane-mark { fill: var(--ok); stroke: var(--wall-sea); stroke-width: 1.2; }
.wall-plane.warn   .wall-plane-mark { fill: var(--warn); }
.wall-plane.danger .wall-plane-mark { fill: var(--danger); }
.wall-plane.sel .wall-plane-mark { fill: var(--brand); stroke: var(--text); stroke-width: 2.2; }

/* A MODELLED MARKER MUST NOT LOOK LIKE AN OBSERVED ONE. The footer says which is which in
   words, and a wall is read from across a room where nobody reads the footer — so the marker
   carries it too. A schedule-estimated aircraft is drawn HOLLOW: the same silhouette in the
   same severity colour, so the board still scans as one picture, with the fill removed so it
   reads as an outline of where something probably is. Tracked and projected keep the solid
   fill because both come from the provider; they are separated in the footer, and the sensor
   is on the flight's own panel.

   `fill: none` rather than a lower opacity: opacity dims the SEVERITY colour too, so a
   danger-red aircraft on the schedule basis would have read as a warning. */
.wall-plane.basis-schedule .wall-plane-mark { fill: none; stroke-width: 2.2; stroke: var(--ok); }
.wall-plane.basis-schedule.warn   .wall-plane-mark { stroke: var(--warn); }
.wall-plane.basis-schedule.danger .wall-plane-mark { stroke: var(--danger); }
.wall-plane.basis-schedule.sel    .wall-plane-mark { stroke: var(--brand); stroke-width: 2.6; }

/* The three states named where they are used. The list beside the map already says each one in
   words on the row it belongs to, so this is the walk-up reader's key, not the only labelling. */
.wall-key { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap;
            padding: 2px 0 6px; font-size: 12px; color: var(--text-2); }
.wall-key-i { display: inline-flex; align-items: center; gap: 6px; }
.wall-key-s { width: 16px; height: 3px; border-radius: 2px; background: var(--ok); }
.wall-key-s.warn { background: var(--warn); }
.wall-key-s.danger { background: var(--danger); }

.wall-nomap { display: flex; gap: 12px; align-items: flex-start; background: var(--surface);
              border: 1px solid var(--border); border-radius: 14px; padding: 22px; }

.wall-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto;
             max-height: calc(100vh - 372px); min-height: 420px; }
/* Set from measured overflow in `Views.wall.wire`, never unconditionally. */
.wall-list.more { -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent);
                  mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent); }
.wall-row { display: flex; gap: 12px; align-items: center; width: 100%; text-align: left;
            background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
            padding: 9px 12px; cursor: pointer; font: inherit; color: inherit; }
.wall-item { display: flex; flex-direction: column; }
.wall-item:has(.wall-why) .wall-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0;
                                      border-bottom: 0; }
.wall-why { display: flex; gap: 10px; align-items: center; justify-content: space-between;
            flex-wrap: wrap; background: var(--surface); border: 1px solid var(--brand);
            border-top: 1px dashed var(--border); border-radius: 0 0 10px 10px;
            padding: 8px 12px; font-size: 12px; }
.wall-why-r { min-width: 0; color: var(--text-2); }
.wall-row.warn   { border-left: 3px solid var(--warn); }
.wall-row.danger { border-left: 3px solid var(--danger); }
.wall-row.sel  { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand) inset; }
.wall-row-eta { min-width: 92px; font-weight: 650; font-size: 15px; font-variant-numeric: tabular-nums; }
.wall-row-eta.soon { color: var(--brand-ink, var(--brand)); }
.wall-row-eta.down { color: var(--text-3); font-weight: 600; }
.wall-row-eta.overdue { color: var(--danger-ink, var(--danger)); }
.wall-row-main { min-width: 0; flex: 1; }
.wall-row-top { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.wall-flt { font-size: 15px; letter-spacing: 0.01em; }
.wall-dist { margin-left: auto; color: var(--text-3); font-size: 12px; font-variant-numeric: tabular-nums; }
.wall-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 4px; }
.wall-chip { font-size: 11px; padding: 1px 7px; border-radius: 20px; background: var(--surface-2);
             border: 1px solid var(--border); color: var(--text-3); }
.wall-chip.ok     { color: var(--ok-ink, var(--ok));     border-color: var(--ok);     background: var(--ok-soft); }
.wall-chip.warn   { color: var(--warn-ink, var(--warn)); border-color: var(--warn);   background: var(--warn-soft); }
.wall-chip.danger { color: var(--danger-ink, var(--danger)); border-color: var(--danger); background: var(--danger-soft); }
.wall-chip.live   { color: var(--brand-ink, var(--brand)); border-color: var(--brand); background: var(--brand-soft); }

.wall-empty { display: flex; gap: 12px; align-items: flex-start; background: var(--surface);
              border: 1px solid var(--border); border-radius: 12px; padding: 22px; }
.wall-unplaced { font-size: 12px; padding: 8px 4px 0; }
.wall-foot { font-size: 12px; margin-top: 12px; max-width: 900px; line-height: 1.5; }

/* ==========================================================================
   TERMINAL MAPS — an uploaded floor plan with markers placed on it.
   ==========================================================================
   THE PIN PALETTE IS ITS OWN, and not one of the status families. Status is a
   STATE (ok / warn / danger) and these are IDENTITIES — six groups a reader
   scans for together: passenger-facing, baggage, airside, staff-only, safety,
   assistance. Reusing --danger for "safety" would have said an evacuation
   muster point is a problem, which it is the opposite of.

   The six were SEARCHED rather than picked, against three constraints at once:
   white glyph on the fill >= 4.5 (worst 5.17), every pair >= 15 apart in OKLab
   for normal vision, and every pair >= 8 apart under simulated protanopia,
   deuteranopia AND tritanopia (worst 11.3). The obvious first set — blue,
   amber, violet, slate, rose, teal — failed four pairs, blue/violet at a
   deuteran separation of 1.0, i.e. the same colour. Each pin also carries its
   own ICON, so colour is never the only encoding.

   These do NOT change with the theme. A pin sits on the uploaded plan, not on
   a panel, and that plan is whatever the airport drew — usually white, in
   both themes. A theme-swapped palette would be chosen against a surface the
   pin is never on. */
:root {
  --tm-pax: #2563eb; --tm-bags: #7c2d12; --tm-airside: #5b21b6;
  --tm-staff: #1e293b; --tm-safety: #c2410c; --tm-assist: #0f766e;
}

.tmap-wrap { display: flex; flex-direction: column; gap: 14px; }
.tmap-boot { padding: 40px; text-align: center; }
.tmap-top { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
.tmap-title h2 { margin: 0; font-size: 20px; }
.tmap-sub { font-size: 13px; margin-top: 2px; }
.tmap-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.tmap-actions select { min-width: 190px; }

.tmap-grid { display: grid; grid-template-columns: 272px minmax(0, 1fr); gap: 14px; align-items: start; }
@media (max-width: 1080px) { .tmap-grid { grid-template-columns: 1fr; } }

.tmap-rail { background: var(--surface); border: 1px solid var(--border);
             border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.tmap-find input { width: 100%; }
.tmap-rail-h { font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
               color: var(--text-3); margin-top: 6px; }
.tmap-none { font-size: 12.5px; padding: 6px 2px; }
.tmap-grp { font-size: 11px; font-weight: 700; color: var(--text-2); margin: 8px 0 2px; }

/* EVERY CELL PLACED EXPLICITLY. Auto-placement put the count — last in the DOM, and spanning
   two rows — into column one, so the row rendered as a stretched grey bar with the plan's name
   pushed to the right of it. A span always wants an explicit column. */
.tmap-item { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 0 8px;
             width: 100%; text-align: left; background: transparent; border: 1px solid transparent;
             border-radius: 9px; padding: 7px 9px; cursor: pointer; color: var(--text); }
.tmap-item:hover { background: var(--surface-2); }
.tmap-item.on { background: var(--brand-soft); border-color: var(--brand); }
.tmap-item-n { grid-column: 1; grid-row: 1; font-size: 13px; font-weight: 600; }
.tmap-item-m { grid-column: 1; grid-row: 2; font-size: 11.5px; color: var(--text-3); }
.tmap-item-c { grid-column: 2; grid-row: 1 / span 2; align-self: center; justify-self: end;
               font-size: 12px; color: var(--text-2); background: var(--surface-3);
               border-radius: 20px; padding: 1px 8px; }
.tmap-draft { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
              color: var(--text-3); border: 1px solid var(--border); border-radius: 5px;
              padding: 0 4px; margin-left: 6px; }

.tmap-legend { display: flex; flex-direction: column; gap: 3px; }
.tmap-key { display: flex; align-items: center; gap: 8px; width: 100%; padding: 4px 6px;
            background: transparent; border: 0; border-radius: 7px; cursor: pointer;
            color: var(--text-2); font-size: 12.5px; text-align: left; }
.tmap-key:hover { background: var(--surface-2); }
.tmap-key.off { opacity: .42; }
.tmap-key b { margin-left: auto; color: var(--text-3); font-weight: 600; }

/* One dot shape for the legend, the search results and the detail header, so a marker looks
   the same everywhere it is named. The 1px ring is what keeps the near-black `staff` dot from
   disappearing into a dark panel — on the plan itself the drop shadow does that job. */
.tmap-dot { width: 22px; height: 22px; border-radius: 50%; display: inline-flex;
            align-items: center; justify-content: center; color: #fff; flex: 0 0 auto;
            box-shadow: 0 0 0 1px var(--border); }
.tmap-dot svg { width: 13px; height: 13px; }
.t-pax .tmap-dot, .tmap-dot.t-pax { background: var(--tm-pax); }
.t-bags .tmap-dot, .tmap-dot.t-bags { background: var(--tm-bags); }
.t-airside .tmap-dot, .tmap-dot.t-airside { background: var(--tm-airside); }
.t-staff .tmap-dot, .tmap-dot.t-staff { background: var(--tm-staff); }
.t-safety .tmap-dot, .tmap-dot.t-safety { background: var(--tm-safety); }
.t-assist .tmap-dot, .tmap-dot.t-assist { background: var(--tm-assist); }

.tmap-res { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
.tmap-res-i { display: flex; gap: 8px; align-items: flex-start; width: 100%; text-align: left;
              background: transparent; border: 0; border-radius: 8px; padding: 6px;
              cursor: pointer; color: var(--text); font-size: 12.5px; }
.tmap-res-i:hover { background: var(--surface-2); }
.tmap-res-i em { font-style: normal; color: var(--text-2); }
.tmap-res-i small { display: block; font-size: 11px; }

.tmap-stage-wrap { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.tmap-bar { display: flex; align-items: center; gap: 12px; background: var(--surface);
            border: 1px solid var(--border); border-radius: 12px; padding: 10px 14px; }
.tmap-bar-l { min-width: 0; }
.tmap-name { font-size: 15px; font-weight: 700; }
.tmap-meta { font-size: 12.5px; }
.tmap-bar-r { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.tmap-zlbl { font-size: 12px; color: var(--text-3); min-width: 42px; text-align: center; }

.tmap-palette { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
                padding: 10px 14px; display: flex; flex-direction: column; gap: 8px; }
.tmap-pal-h { font-size: 12.5px; color: var(--text-2); }
.tmap-pal-row { display: flex; flex-wrap: wrap; gap: 6px; }
.tmap-pal { display: inline-flex; align-items: center; gap: 6px; font-size: 12px;
            background: var(--surface-2); border: 1px solid var(--border); border-radius: 20px;
            padding: 3px 10px 3px 4px; cursor: pointer; color: var(--text-2); }
.tmap-pal:hover { border-color: var(--brand); color: var(--text); }
.tmap-pal.on { background: var(--brand-soft); border-color: var(--brand); color: var(--text); }
.tmap-pal .tmap-dot { width: 18px; height: 18px; }
.tmap-pal .tmap-dot svg { width: 11px; height: 11px; }
.tmap-pal-foot { display: flex; gap: 8px; flex-wrap: wrap; border-top: 1px solid var(--border-soft);
                 padding-top: 8px; }

/* The stage clips; the canvas inside it is what moves. Panning by scrolling the stage would
   fight the wheel-to-zoom, which is the gesture a plan actually wants. */
.tmap-stage { position: relative; overflow: hidden; border-radius: 12px;
              border: 1px solid var(--border); background: var(--surface-2);
              height: min(68vh, 760px); cursor: grab; touch-action: none; }
.tmap-stage.grabbing { cursor: grabbing; }
.tmap-canvas { position: absolute; top: 0; left: 0; width: 100%;
               aspect-ratio: 1 / var(--ratio, 0.62);
               transform: translate(var(--tx, 0px), var(--ty, 0px)) scale(var(--tz, 1));
               transform-origin: 0 0; will-change: transform; }
.tmap-canvas img { width: 100%; height: 100%; object-fit: contain; display: block;
                   user-select: none; -webkit-user-drag: none; }
.tmap-pins { position: absolute; inset: 0; }

/* A PIN DOES NOT SCALE WITH THE PLAN. Zooming to read a gate number must not grow the marker
   over the thing it is pointing at, so the counter-scale below keeps every pin the same
   physical size at 40% and at 800%. `transform-origin` sits at the TIP, so the point the pin
   marks stays put while the body of it shrinks. */
.tmap-pin { position: absolute; left: calc(var(--px) * 100%); top: calc(var(--py) * 100%);
            transform: translate(-50%, -100%) scale(calc(1 / var(--tz, 1)));
            transform-origin: 50% 100%;
            width: 28px; height: 34px; padding: 0; border: 0; background: transparent;
            cursor: pointer; }
.tmap-pin i { position: absolute; inset: 0 0 6px 0; border-radius: 50% 50% 50% 50%;
              display: flex; align-items: center; justify-content: center; color: #fff;
              box-shadow: 0 0 0 2px #fff, 0 2px 6px rgba(0, 0, 0, .45); }
.tmap-pin i svg { width: 15px; height: 15px; }
/* The stem — a triangle under the disc, so the pin points at a place rather than covering it. */
.tmap-pin::after { content: ""; position: absolute; left: 50%; bottom: 0; width: 0; height: 0;
                   transform: translateX(-50%);
                   border-left: 5px solid transparent; border-right: 5px solid transparent;
                   border-top: 8px solid #fff; }
.tmap-pin.t-pax i { background: var(--tm-pax); }
.tmap-pin.t-bags i { background: var(--tm-bags); }
.tmap-pin.t-airside i { background: var(--tm-airside); }
.tmap-pin.t-staff i { background: var(--tm-staff); }
.tmap-pin.t-safety i { background: var(--tm-safety); }
.tmap-pin.t-assist i { background: var(--tm-assist); }
.tmap-pin.on i { box-shadow: 0 0 0 3px var(--brand), 0 0 0 5px #fff, 0 2px 8px rgba(0, 0, 0, .5); }
.tmap-pin.on { z-index: 3; }
.tmap-ref { position: absolute; left: 50%; top: -9px; transform: translateX(-50%);
            font-size: 10px; font-weight: 700; line-height: 1.5; color: #1e293b;
            background: #fff; border-radius: 5px; padding: 0 4px; white-space: nowrap;
            box-shadow: 0 1px 3px rgba(0, 0, 0, .35); }

.tmap-drop, .tmap-empty { display: flex; flex-direction: column; align-items: center;
                          justify-content: center; text-align: center; gap: 4px;
                          min-height: 320px; padding: 32px; height: 100%; }
.tmap-drop h3, .tmap-empty h3 { margin: 8px 0 0; font-size: 16px; }
.tmap-empty-ic svg { width: 34px; height: 34px; color: var(--text-3); }

.tmap-foot { font-size: 12.5px; padding: 2px 4px; }
.tmap-detail { padding: 14px; }
.tmap-detail-h { display: flex; align-items: center; gap: 10px; }
.tmap-detail-n { font-size: 14px; font-weight: 700; }
.tmap-detail-g { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
                 gap: 10px; margin-top: 12px; padding-top: 12px;
                 border-top: 1px solid var(--border-soft); font-size: 13px; }
.tmap-detail-g span { display: block; font-size: 11px; text-transform: uppercase;
                      letter-spacing: .06em; }

/* =====================================================================
   Ad-hoc task board
   The unplanned work: raised on the spot, dispatched, timed. Five tiles that
   FILTER, a list that can be late, and a drawer that has to hold people and a
   lifecycle without becoming a form.
   ===================================================================== */

/* A tile that filters has to show WHICH one is holding the list down, and `.card[onclick]`
   cannot say that — it supplies the cursor and the hover lift and nothing at rest. Only the
   tiles that MEAN a filter carry the onclick at all, which is the rule `.ps-kpi-act` states in
   its own words: a tile that looks clickable and does nothing is worse than a plain number. */
.tk-tile { padding: 14px 16px; }
.tk-tile.on { border-color: var(--brand);
  background: color-mix(in srgb, var(--brand) 9%, transparent); }
.tk-tile-sub { color: var(--text-3); font-size: 11px; margin-top: 4px; }

/* Overdue is already a badge in the Due cell; this is the row-level echo of it, deliberately
   faint. A filled red row down a whole board reads as an outage rather than as three late jobs
   — the finding the roster wall of red buttons produced, written into a rule this time. */
.tk-late td { background: color-mix(in srgb, var(--danger) 6%, transparent); }
.tk-late td:first-child { box-shadow: inset 3px 0 0 var(--danger); }

/* A checkbox that belongs to the filter ROW, so it sits on the selects' baseline rather than
   floating above them — `.field label` is display:block and would have stacked the box over its
   own text. */
/* THE PICKER NEEDS MORE THAN 560px, and only the picker does. A marker div inside the body
   widens its own modal rather than every modal in the platform — six columns in the default
   width clipped the action button off the right edge and wrapped one reason over five lines,
   which is the same defect the qualification drawer had and the same fix. */
.modal:has(.tk-picker) { width: 760px; }
.tk-picker { display: none; }

.tk-mine { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px;
  color: var(--text-2); font-weight: 600; white-space: nowrap; cursor: pointer; }
.tk-mine input { margin: 0; }

.tk-count { margin-left: auto; font-size: 12px; color: var(--text-3);
  font-variant-numeric: tabular-nums; white-space: nowrap; }

/* One person on the job, with the control to take them off. A row rather than a list item
   because the button has to sit at the end and stay there when the name wraps. */
.tk-person { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 0; border-top: 1px solid var(--border-soft, var(--border)); font-size: 13px; }
.tk-person:first-of-type { border-top: 0; }

/* Raised → started → finished, as the ROW records it. Monospaced figures so three stamps read
   as a column rather than as three sentences of different length. */
.tk-trail { font-size: 12.5px; }
.tk-step { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0;
  border-top: 1px solid var(--border-soft, var(--border));
  font-variant-numeric: tabular-nums; color: var(--text-2); }
.tk-step:first-child { border-top: 0; }
.tk-step-l { color: var(--text-3); font-weight: 600; }

@media (max-width: 620px) {
  .tk-person { flex-wrap: wrap; }
  .tk-step { flex-direction: column; gap: 2px; }
}
