/* ============================================================================
   ODRdesk — foundation
   Plain CSS, no build step. Custom properties only.

   HOW THEMING WORKS
   -----------------
   Every colour in this file resolves through a semantic token. Light values are
   declared on :root. Dark values are declared once as --d-* raw tokens, then
   mapped onto the semantic names by TWO identical blocks, marked
   [THEME MAP A] and [THEME MAP B]:
       A  @media (prefers-color-scheme:dark) body:not(.light):not(.dark)  — OS default
       B  body.dark                                                        — user choice
   CSS cannot share one declaration block across a media query and a class, so
   the mapping is written twice. THE TWO BLOCKS MUST STAY IDENTICAL. Values live
   only in the --d-* group, so edits happen in one place.

   There are deliberately ZERO `body.dark .component {
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239CB0BD' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239CB0BD' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}` rules. If a component
   needs to differ between themes, add a token — do not add a theme override.

   WHAT IS WHERE
   -------------
   Read top to bottom; the order is the cascade and moving things silently
   changes how they render, so sections grow in place rather than being sorted.
   Search for the banner text to jump.

       FOUNDATION        tokens, reset, type scale, theme maps A and B
       LAYOUT            app shell, sidebar, main, grids
       CONTROLS          buttons, inputs, tabs, toasts, modals
       DATA DISPLAY      tables, badges, status tones, cards
       DASHBOARD         .q-*   queue cards, .sbar-* status bars
       IDLE + AGEING     .idle-*  bands, .ar-*  status-deadline settings rows
       TICKET DETAIL     .tk-*   grouped facts, ownership markers
       FIRST RUN         .frun-* the one-line hint on first launch
       EMPTY STATES      .empty-*

   RULES THAT KEEP THIS HONEST
   ---------------------------
   1. Every colour resolves to a token. A literal hex in this file is a bug
      unless it is a --d-* raw value or documented in DESIGN.md.
   2. Two channels for state. Never hue alone: colour PLUS shape, length, or
      position. ~6% of men cannot separate this palette's red from its amber.
   3. No coloured stripe down the side of a card. It is the most recognisable
      tell of generated UI, and it is almost always duplicating a signal the
      element already carries in its text colour or background. Five were
      removed on 2026-07-27 for exactly that reason.
   4. Text clears 4.5:1 in BOTH themes. Not one. The shop runs Windows and
      will default to light; this machine runs dark.

   Rules 1-3 are enforced by a test — see `design_rules` in
   crates/odrdesk-app/src/tickets.rs. It fails the build, not a review.
   ========================================================================== */

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

:root{
/* ── TYPE SCALE ────────────────────────────────────────────────────────────
   Nine steps. 9/10/11px collapsed to 11px (bench legibility), 15/16 to 16,
   20/22 to 22. --fs-4xl is decorative glyphs only, never text. */
/* 🔴 The platform's own interface face, named in the platform's own order.
   'Inter' led this stack and was never loaded — no @font-face, no link — so
   every machine has silently been rendering the fallback. Naming a face the
   app does not ship is a stack that reads as a decision and behaves as an
   accident.
   What it actually resolved to is the right answer anyway, and it is the
   Apple approach: an application wears the system's interface face. SF Pro on
   macOS, Segoe UI Variable on Windows 11 — the shop's own machines — and both
   are drawn for exactly this job at exactly these sizes. A shipped webfont
   would make the demo and the shop floor look the same as each other and
   worse than either. */
--ff:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI Variable Text','Segoe UI',system-ui,sans-serif;
--chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2344535E' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ff-mono:ui-monospace,'SF Mono',Menlo,Consolas,monospace;
--fs-2xs:11px;--fs-xs:12px;--fs-sm:13px;--fs-md:14px;--fs-lg:16px;
--fs-xl:18px;--fs-2xl:22px;--fs-3xl:28px;--fs-4xl:36px;

/* ── CONTROL SIZING ───────────────────────────────────────────────────────
   --tap    full 44px target: primary actions, form fields, nav, tabs
   --tap-md 40px visible box paired with a 44px invisible hit area (header)
   --tap-sm 36px dense inline control inside a table row or toolbar
   --tap-xs 32px overlay/secondary control on a card or tile */
--tap:44px;--tap-md:40px;--tap-sm:36px;--tap-xs:32px;

/* --r-xs is for things 4-8px tall, where --r-sm would round them away: the
   fill inside a 4px bar, an 8px legend swatch, a sparkline column. It was
   four literal 2px values before it was a token, which is the definition of
   drift rather than design. */
--r-xs:2px;--r-sm:4px;--r-md:8px;--r-lg:12px;--r-xl:16px;--r-full:9999px;
--hh:64px;--cp:24px;
/* 240px was a default, not a measurement. The longest label in the nav is
   "Repair Tickets" — 16px of padding, an 18px icon, a 10px gap, the text, and
   16px again comes to roughly 160px, so 240 was carrying 80px of empty
   gutter down the whole height of the window. Every pixel of it was taken
   from the tables, which are the part that actually runs out of room.
   204px leaves the longest label comfortable and gives the content back 36px.
   A future auto-collapse can take it further; this is the version that needs
   no new behaviour to explain. */
--sidebar-width:204px;

/* ── SURFACES & TEXT (light) ─────────────────────────────────────────────── */
/* Retuned 2026-09-06 to the world of the 4 September mockup: a cooler,
   flatter ground, near-black ink, and the brand blue as the one accent.
   --text-muted is #5F6875 rather than the mockup's #8A929E because the
   latter reads 3.3:1 on white and the floor here is 4.5. */
--bg:#F4F5F7;--surface:#fff;--surface-2:#F8F9FB;
--surface-raised:#fff;         /* a card sitting on a card */
--surface-sunken:#EEF0F4;      /* an inset well inside a card */
--field-bg:#fff;               /* text input / select fill */
--border:#E3E6EB;
--text:#111318;--text-2:#4B5563;--text-muted:#5F6875;
--on-color:#fff;               /* text on a saturated fill (danger, success, avatars) */
--on-accent:#fff;              /* text on the accent fill; the dark map swaps it, see --d-on-accent */

/* ── ACCENT ───────────────────────────────────────────────────────────────
   Two blues, because one cannot do both jobs: white-on-accent needs a DARK
   blue, accent-as-text-on-dark needs a LIGHT one.
     --accent       accent as TEXT, and as border/outline. Safe on any surface.
                    app.js writes `color:var(--accent)` inline 29 times, so this
                    is the one that has to be text-safe.
     --accent-fill  solid interactive fill that carries --on-color text.
     --accent-soft  tinted accent surface.
   --accent-ink is an alias of --accent, kept because it reads clearly at the
   call site. --accent-light is a legacy alias: app.js emits it inline. */
/* #0068C4 is the brand blue (#008EFF) pulled down until text and white-on-it
   both clear the floor with headroom: 5.56:1 on white, 5.10:1 on --bg. The
   brand value itself sits at 3.3 and fails. It is the shared light accent of
   the storefront and the app: ../ODR-KB/3-reference/design-tokens.md is the
   source, this block implements it. */
--accent:#0068C4;--accent-ink:#0068C4;
--accent-fill:#0068C4;--accent-fill-hover:#005FB0;--accent-hover:#005FB0;
--accent-soft:#E6F2FC;--accent-light:#E6F2FC;
--focus:#0068C4;--ring:0 0 0 3px rgba(0,142,255,.25);

/* ── FEEDBACK ─────────────────────────────────────────────────────────────── */
--success:#22C55E;--success-bg:#F0FDF4;--success-text:#15803D;
/* ⚠️ --warning as a GRAPHIC FILL on white measures 3.19:1 — passes the 3:1
   graphics floor with 0.19 to spare (audited 2026-08-24 by the website
   session). If the light theme ever gains a tinted panel behind a lamp,
   this is the value that fails first: re-measure before adding the ground. */
--warning:#D97706;--warning-bg:#FFFBEB;--warning-text:#92400E;
--danger:#EF4444;--danger-bg:#FEF2F2;--danger-text:#B91C1C;

/* ── STATUS / STAGE TONES ─────────────────────────────────────────────────
   Ten tones drive all 41 status and stage badges. Each is bg / fg / bd, and
   every fg clears 4.5:1 on its own bg in BOTH themes. Adding a status means
   picking a tone, never picking a hex. */
--tone-neutral-bg:#F3F4F6;--tone-neutral-fg:#374151;--tone-neutral-bd:#D1D5DB;
--tone-blue-bg:#EFF6FF;--tone-blue-fg:#1D4ED8;--tone-blue-bd:#BFDBFE;
--tone-indigo-bg:#EEF2FF;--tone-indigo-fg:#4338CA;--tone-indigo-bd:#C7D2FE;
--tone-violet-bg:#F5F3FF;--tone-violet-fg:#6D28D9;--tone-violet-bd:#DDD6FE;
--tone-teal-bg:#F0FDFA;--tone-teal-fg:#0F766E;--tone-teal-bd:#99F6E4;
--tone-green-bg:#F0FDF4;--tone-green-fg:#15803D;--tone-green-bd:#BBF7D0;
--tone-amber-bg:#FFFBEB;--tone-amber-fg:#92400E;--tone-amber-bd:#FDE68A;
--tone-gold-bg:#FEF3C7;--tone-gold-fg:#854D0E;--tone-gold-bd:#FCD34D;
--tone-orange-bg:#FFF7ED;--tone-orange-fg:#9A3412;--tone-orange-bd:#FED7AA;
--tone-red-bg:#FEF2F2;--tone-red-fg:#B91C1C;--tone-red-bd:#FECACA;

/* ── ROW TINTS ────────────────────────────────────────────────────────────── */
--row-warn-bg:#FFFBEB;--row-warn-hover:#FEF3C7;
--row-danger-bg:#FEF2F2;--row-danger-hover:#FEE2E2;
--row-sel-bg:#EFF6FF;

/* ── SIDEBAR & BRAND ──────────────────────────────────────────────────────── */
--brand-deep:#0F1B2D;--brand-mid:#1E3A5F;
/* The sidebar sits on the page's own surface since 2026-09-06 — white by
   day, graphite by night — with the active row as a soft blue pill. The
   navy panel it replaced was the one part of the light theme that was not
   light, and it made the app read as two products side by side. */
--sidebar-bg:#fff;--sidebar-hover:#F1F3F6;
--sidebar-active:rgba(0,142,255,.10);--sidebar-border:#008EFF;
--sidebar-text:#4B5563;--sidebar-text-hover:#111318;--sidebar-active-text:#111318;
--sidebar-muted:#5F6875;--sidebar-divider:#E3E6EB;
--sidebar-focus:#0068C4;   /* 5.6:1 on the light sidebar; the dark map swaps it */
--avatar-bg:#0068C4;

/* ── OVERLAYS ─────────────────────────────────────────────────────────────── */
--scrim:rgba(0,0,0,.5);
--overlay-ctrl-bg:rgba(255,255,255,.92);--overlay-ctrl-bg-hover:#fff;
--overlay-del-bg:rgba(15,23,42,.7);
--tooltip-bg:#1E293B;--tooltip-fg:#fff;

/* ── SHADOWS ──────────────────────────────────────────────────────────────── */
--shadow-sm:0 1px 2px rgba(0,0,0,.05);
--shadow-md:0 1px 2px rgba(17,19,24,.06),0 8px 24px -16px rgba(17,19,24,.25);
--shadow-lg:0 4px 12px rgba(0,0,0,.08);
--shadow-xl:0 10px 30px rgba(0,0,0,.12);
--shadow-modal:0 20px 60px rgba(0,0,0,.20);

/* ── DARK PALETTE SOURCE ──────────────────────────────────────────────────
   Inert here. Consumed only by [THEME MAP A] and [THEME MAP B] below. */
/* Retuned 2026-09-06 to the mockup's night palette: a graphite ground with
   cards one step lighter, and rules that are a lift rather than a line. */
--d-bg:#0E1014;--d-surface:#15181E;--d-surface-2:#1B1F26;
--d-surface-raised:#1B1F26;--d-surface-sunken:#0E1014;--d-field-bg:#0E1014;
--d-border:#262B34;
--d-text:#E8EAF0;
/* text-2 and muted are the foreground AT AN ALPHA, not sibling greys.
   Solid #ADBAC7/#8B949E drifted up to 0.5:1 in contrast across this theme's
   three grounds; the same tokens as foreground-at-alpha drift a third of
   that, because they inherit whatever they sit on. Measured 2026-08-24 on
   #0D1117 / #161B22 / #1C2129: text-2 8.06-9.05:1, muted 5.76-6.25:1 —
   everything clears 4.5 with margin on every ground. */
--d-text-2:#A3ABB8;--d-text-muted:#8A93A2;   /* 7.5:1 and 5.8:1 on the card */
/* Dark ink is the brand blue itself, #008EFF, which is the thing a person
   recognises between the storefront and the app: 5.72:1 on --d-bg, 5.34 on
   the card, and 4.97 on the raised surface — passing, but the thinnest number
   in the set, so --d-surface-raised must not lighten without re-measuring.
   The fill follows the ground and the ink follows the fill (the token file's
   correction of 2026-09-06): on dark the fill is the brand blue too, 5.72:1
   at its edge against --d-bg where #0068C4 managed 3.52, and it carries
   near-black ink #06202C (5.05:1) because white on #008EFF is 3.3:1. On
   light the pair is #0068C4 with white. The two inks never cross.
   Shared with the storefront: ../ODR-KB/3-reference/design-tokens.md. */
--d-accent:#008EFF;--d-accent-ink:#008EFF;
--d-accent-fill:#008EFF;--d-accent-fill-hover:#2B9DFF;--d-accent-hover:#2B9DFF;--d-on-accent:#06202C;
--d-accent-soft:rgba(0,142,255,.14);--d-focus:#008EFF;--d-ring:0 0 0 3px rgba(0,142,255,.35);
--d-success:#3FB950;--d-success-bg:#0C2A18;--d-success-text:#7EE787;
--d-warning:#D29922;--d-warning-bg:#3A2600;--d-warning-text:#FBBF24;
--d-danger:#F85149;--d-danger-bg:#3B1113;--d-danger-text:#FF8A80;
--d-tone-neutral-bg:#282E36;--d-tone-neutral-fg:#C9D1D9;--d-tone-neutral-bd:#3D444D;
--d-tone-blue-bg:#0D2847;--d-tone-blue-fg:#79C0FF;--d-tone-blue-bd:#1F4C7A;
--d-tone-indigo-bg:#1E1B4B;--d-tone-indigo-fg:#A5B4FC;--d-tone-indigo-bd:#33307A;
--d-tone-violet-bg:#2A1A5E;--d-tone-violet-fg:#C4B5FD;--d-tone-violet-bd:#453091;
--d-tone-teal-bg:#0B2E2C;--d-tone-teal-fg:#5EEAD4;--d-tone-teal-bd:#14514C;
--d-tone-green-bg:#0C2A18;--d-tone-green-fg:#7EE787;--d-tone-green-bd:#17512C;
--d-tone-amber-bg:#3A2600;--d-tone-amber-fg:#FBBF24;--d-tone-amber-bd:#5C3D00;
--d-tone-gold-bg:#3B2E00;--d-tone-gold-fg:#FCD34D;--d-tone-gold-bd:#5E4900;
--d-tone-orange-bg:#3B1D06;--d-tone-orange-fg:#FDBA74;--d-tone-orange-bd:#5E300B;
--d-tone-red-bg:#3B1113;--d-tone-red-fg:#FF8A80;--d-tone-red-bd:#5E1D20;
--d-row-warn-bg:#2D2000;--d-row-warn-hover:#3A2A00;
--d-row-danger-bg:#2D0000;--d-row-danger-hover:#3B1113;--d-row-sel-bg:#1E3A5F;
--d-sidebar-bg:#15181E;--d-sidebar-divider:#262B34;--d-sidebar-text:#A3ABB8;
--d-sidebar-text-hover:#E8EAF0;--d-sidebar-active:rgba(0,142,255,.14);
--d-sidebar-muted:#8A93A2;--d-avatar-bg:#0068C4;
--d-sidebar-hover:rgba(255,255,255,.06);--d-sidebar-active-text:#E8EAF0;--d-sidebar-focus:#008EFF;
--d-scrim:rgba(0,0,0,.72);
--d-overlay-ctrl-bg:rgba(28,35,51,.92);--d-overlay-ctrl-bg-hover:#1C2333;
--d-overlay-del-bg:rgba(0,0,0,.75);
--d-tooltip-bg:#30363D;--d-tooltip-fg:#E6EDF3;
--d-shadow-sm:0 1px 2px rgba(0,0,0,.5);
--d-shadow-md:0 1px 0 rgba(255,255,255,.03) inset,0 8px 24px -12px rgba(0,0,0,.6);
--d-shadow-lg:0 4px 12px rgba(0,0,0,.7);
--d-shadow-xl:0 10px 30px rgba(0,0,0,.75);
--d-shadow-modal:0 20px 60px rgba(0,0,0,.8);
}

/* ─────────────────────────────────────────────────────────────────────────
   [THEME MAP A] — OS preference is the initial default.
   Yields to body.dark and body.light, so an explicit user choice always wins.
   KEEP IDENTICAL TO [THEME MAP B].
   ───────────────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme:dark){
body:not(.light):not(.dark){
color-scheme:dark;
--bg:var(--d-bg);--surface:var(--d-surface);--surface-2:var(--d-surface-2);
--surface-raised:var(--d-surface-raised);--surface-sunken:var(--d-surface-sunken);
--field-bg:var(--d-field-bg);--border:var(--d-border);
--text:var(--d-text);--text-2:var(--d-text-2);--text-muted:var(--d-text-muted);
--accent:var(--d-accent);--accent-ink:var(--d-accent-ink);
--accent-fill:var(--d-accent-fill);--accent-fill-hover:var(--d-accent-fill-hover);--accent-hover:var(--d-accent-hover);
--accent-soft:var(--d-accent-soft);--accent-light:var(--d-accent-soft);--on-accent:var(--d-on-accent);
--focus:var(--d-focus);--ring:var(--d-ring);
--success:var(--d-success);--success-bg:var(--d-success-bg);--success-text:var(--d-success-text);
--warning:var(--d-warning);--warning-bg:var(--d-warning-bg);--warning-text:var(--d-warning-text);
--danger:var(--d-danger);--danger-bg:var(--d-danger-bg);--danger-text:var(--d-danger-text);
--tone-neutral-bg:var(--d-tone-neutral-bg);--tone-neutral-fg:var(--d-tone-neutral-fg);--tone-neutral-bd:var(--d-tone-neutral-bd);
--tone-blue-bg:var(--d-tone-blue-bg);--tone-blue-fg:var(--d-tone-blue-fg);--tone-blue-bd:var(--d-tone-blue-bd);
--tone-indigo-bg:var(--d-tone-indigo-bg);--tone-indigo-fg:var(--d-tone-indigo-fg);--tone-indigo-bd:var(--d-tone-indigo-bd);
--tone-violet-bg:var(--d-tone-violet-bg);--tone-violet-fg:var(--d-tone-violet-fg);--tone-violet-bd:var(--d-tone-violet-bd);
--tone-teal-bg:var(--d-tone-teal-bg);--tone-teal-fg:var(--d-tone-teal-fg);--tone-teal-bd:var(--d-tone-teal-bd);
--tone-green-bg:var(--d-tone-green-bg);--tone-green-fg:var(--d-tone-green-fg);--tone-green-bd:var(--d-tone-green-bd);
--tone-amber-bg:var(--d-tone-amber-bg);--tone-amber-fg:var(--d-tone-amber-fg);--tone-amber-bd:var(--d-tone-amber-bd);
--tone-gold-bg:var(--d-tone-gold-bg);--tone-gold-fg:var(--d-tone-gold-fg);--tone-gold-bd:var(--d-tone-gold-bd);
--tone-orange-bg:var(--d-tone-orange-bg);--tone-orange-fg:var(--d-tone-orange-fg);--tone-orange-bd:var(--d-tone-orange-bd);
--tone-red-bg:var(--d-tone-red-bg);--tone-red-fg:var(--d-tone-red-fg);--tone-red-bd:var(--d-tone-red-bd);
--row-warn-bg:var(--d-row-warn-bg);--row-warn-hover:var(--d-row-warn-hover);
--row-danger-bg:var(--d-row-danger-bg);--row-danger-hover:var(--d-row-danger-hover);
--row-sel-bg:var(--d-row-sel-bg);
--sidebar-bg:var(--d-sidebar-bg);--sidebar-divider:var(--d-sidebar-divider);
--sidebar-text:var(--d-sidebar-text);--sidebar-text-hover:var(--d-sidebar-text-hover);
--sidebar-active:var(--d-sidebar-active);--sidebar-muted:var(--d-sidebar-muted);
--sidebar-hover:var(--d-sidebar-hover);--sidebar-active-text:var(--d-sidebar-active-text);--sidebar-focus:var(--d-sidebar-focus);
--avatar-bg:var(--d-avatar-bg);--scrim:var(--d-scrim);
--overlay-ctrl-bg:var(--d-overlay-ctrl-bg);--overlay-ctrl-bg-hover:var(--d-overlay-ctrl-bg-hover);
--overlay-del-bg:var(--d-overlay-del-bg);
--tooltip-bg:var(--d-tooltip-bg);--tooltip-fg:var(--d-tooltip-fg);
--shadow-sm:var(--d-shadow-sm);--shadow-md:var(--d-shadow-md);--shadow-lg:var(--d-shadow-lg);
--shadow-xl:var(--d-shadow-xl);--shadow-modal:var(--d-shadow-modal);
}}

/* ─────────────────────────────────────────────────────────────────────────
   [THEME MAP B] — explicit user choice, set by applyDark() in app.js.
   KEEP IDENTICAL TO [THEME MAP A].
   ───────────────────────────────────────────────────────────────────────── */
body.dark{
color-scheme:dark;
--bg:var(--d-bg);--surface:var(--d-surface);--surface-2:var(--d-surface-2);
--surface-raised:var(--d-surface-raised);--surface-sunken:var(--d-surface-sunken);
--field-bg:var(--d-field-bg);--border:var(--d-border);
--text:var(--d-text);--text-2:var(--d-text-2);--text-muted:var(--d-text-muted);
--accent:var(--d-accent);--accent-ink:var(--d-accent-ink);
--accent-fill:var(--d-accent-fill);--accent-fill-hover:var(--d-accent-fill-hover);--accent-hover:var(--d-accent-hover);
--accent-soft:var(--d-accent-soft);--accent-light:var(--d-accent-soft);--on-accent:var(--d-on-accent);
--focus:var(--d-focus);--ring:var(--d-ring);
--success:var(--d-success);--success-bg:var(--d-success-bg);--success-text:var(--d-success-text);
--warning:var(--d-warning);--warning-bg:var(--d-warning-bg);--warning-text:var(--d-warning-text);
--danger:var(--d-danger);--danger-bg:var(--d-danger-bg);--danger-text:var(--d-danger-text);
--tone-neutral-bg:var(--d-tone-neutral-bg);--tone-neutral-fg:var(--d-tone-neutral-fg);--tone-neutral-bd:var(--d-tone-neutral-bd);
--tone-blue-bg:var(--d-tone-blue-bg);--tone-blue-fg:var(--d-tone-blue-fg);--tone-blue-bd:var(--d-tone-blue-bd);
--tone-indigo-bg:var(--d-tone-indigo-bg);--tone-indigo-fg:var(--d-tone-indigo-fg);--tone-indigo-bd:var(--d-tone-indigo-bd);
--tone-violet-bg:var(--d-tone-violet-bg);--tone-violet-fg:var(--d-tone-violet-fg);--tone-violet-bd:var(--d-tone-violet-bd);
--tone-teal-bg:var(--d-tone-teal-bg);--tone-teal-fg:var(--d-tone-teal-fg);--tone-teal-bd:var(--d-tone-teal-bd);
--tone-green-bg:var(--d-tone-green-bg);--tone-green-fg:var(--d-tone-green-fg);--tone-green-bd:var(--d-tone-green-bd);
--tone-amber-bg:var(--d-tone-amber-bg);--tone-amber-fg:var(--d-tone-amber-fg);--tone-amber-bd:var(--d-tone-amber-bd);
--tone-gold-bg:var(--d-tone-gold-bg);--tone-gold-fg:var(--d-tone-gold-fg);--tone-gold-bd:var(--d-tone-gold-bd);
--tone-orange-bg:var(--d-tone-orange-bg);--tone-orange-fg:var(--d-tone-orange-fg);--tone-orange-bd:var(--d-tone-orange-bd);
--tone-red-bg:var(--d-tone-red-bg);--tone-red-fg:var(--d-tone-red-fg);--tone-red-bd:var(--d-tone-red-bd);
--row-warn-bg:var(--d-row-warn-bg);--row-warn-hover:var(--d-row-warn-hover);
--row-danger-bg:var(--d-row-danger-bg);--row-danger-hover:var(--d-row-danger-hover);
--row-sel-bg:var(--d-row-sel-bg);
--sidebar-bg:var(--d-sidebar-bg);--sidebar-divider:var(--d-sidebar-divider);
--sidebar-text:var(--d-sidebar-text);--sidebar-text-hover:var(--d-sidebar-text-hover);
--sidebar-active:var(--d-sidebar-active);--sidebar-muted:var(--d-sidebar-muted);
--sidebar-hover:var(--d-sidebar-hover);--sidebar-active-text:var(--d-sidebar-active-text);--sidebar-focus:var(--d-sidebar-focus);
--avatar-bg:var(--d-avatar-bg);--scrim:var(--d-scrim);
--overlay-ctrl-bg:var(--d-overlay-ctrl-bg);--overlay-ctrl-bg-hover:var(--d-overlay-ctrl-bg-hover);
--overlay-del-bg:var(--d-overlay-del-bg);
--tooltip-bg:var(--d-tooltip-bg);--tooltip-fg:var(--d-tooltip-fg);
--shadow-sm:var(--d-shadow-sm);--shadow-md:var(--d-shadow-md);--shadow-lg:var(--d-shadow-lg);
--shadow-xl:var(--d-shadow-xl);--shadow-modal:var(--d-shadow-modal);
}
body.light{color-scheme:light}

body{font-family:var(--ff);background:var(--bg);color:var(--text);overflow:hidden;height:100vh}
#app{display:flex;height:100vh}

/* ── KEYBOARD FOCUS ───────────────────────────────────────────────────────
   One treatment, everywhere. Declared before the components so component
   rules can still adjust offset, and repeated at the end of the file for the
   controls that set their own :focus styles. */
:focus-visible{outline:3px solid var(--focus);outline-offset:2px}

/* SIDEBAR */
#sidebar{width:var(--sidebar-width);background:var(--sidebar-bg);display:flex;flex-direction:column;flex-shrink:0;z-index:50;--focus:var(--sidebar-focus)}
.sb-brand{display:flex;align-items:center;gap:10px;padding:20px 16px;border-bottom:1px solid var(--sidebar-divider)}
.sb-brand-name{font-size:var(--fs-xl);font-weight:700;color:var(--sidebar-active-text);letter-spacing:-0.3px}
.sb-nav{flex:1;padding:8px 0;overflow-y:auto}
/* 40px rather than the 44px tap tier, and this is the stated reason the design
   rules ask for. The 44px floor exists for controls a technician commits or
   types into; a nav row is neither, and it spans the full 240px of the sidebar,
   so the target is enormous in the axis that actually matters. At 44px the
   fourteenth item fell off the bottom of a laptop screen, which costs more
   than four pixels of height ever could. */
/* 🔴 THE INACTIVE ITEMS ARE QUIET SO THE ACTIVE ONE READS.
   Fourteen items at one weight makes a wall: the eye has to search for where
   it is instead of being told. The active row keeps full strength and full
   icon; the rest sit back. Nothing moves and no row is lost — this is
   contrast doing the work that another divider would otherwise be asked to
   do, and the height budget below is untouched. */
.nav-item{display:flex;align-items:center;gap:10px;min-height:40px;padding:9px 16px;cursor:pointer;color:var(--sidebar-muted);font-size:var(--fs-md);font-weight:500;border-left:3px solid transparent;transition:all .15s;user-select:none}
.nav-item:hover{background:var(--sidebar-hover);color:var(--sidebar-text-hover)}
.nav-item.active{background:var(--sidebar-active);border-left-color:var(--sidebar-border);color:var(--sidebar-active-text);font-weight:600}
.nav-item:focus-visible{outline-offset:-3px}
.nav-item svg{flex-shrink:0;opacity:.55;transition:opacity .15s}
.nav-item:hover svg{opacity:.85}
.nav-item.active svg{opacity:1}
/* ── Sidebar groups ──────────────────────────────────────────────────────
   Fifteen flat items gave no clue which screens belong together, and once the
   list is filtered per person it would leave arbitrary gaps where somebody
   else's pages used to be.

   Grouped by what the screen is FOR, a technician's sidebar reads as short and
   complete rather than censored. A group with nothing visible in it renders
   nothing at all, so the shape of the list never advertises what is missing.

   The headings are quiet on purpose: they are wayfinding, not navigation, and
   at sidebar-muted they sit below the items in the reading order without
   needing a rule or a box.

   ⚠️ NO divider rule between groups, and the spacing is deliberately tight.
   The first version had a 1px rule plus 14px of padding on each group and it
   pushed Settings off the bottom of a 868px window — an administrator had to
   scroll a primary navigation to reach it. Fourteen items and five headings is
   already close to the height budget, so anything added here costs a row at
   the bottom. The heading alone separates the groups perfectly well.

   THE HEIGHT BUDGET, measured rather than guessed. At an 868px window the nav
   has ~738px of room and fourteen 44px items with five headings wanted 762px,
   so Settings sat below the fold for an administrator.

   Two things were tried. Tightening the group spacing recovered part of it.
   Moving Settings to the pinned slot recovered NOTHING — the slot costs the
   same height the row gave back — and it is kept anyway because pinning the
   one item people go hunting for is right on its own merits.

   What actually fixed it is the 40px nav row below. */
.nav-group + .nav-group{margin-top:4px}
.nav-group-h{padding:6px 16px 2px;font-size:var(--fs-2xs);font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;color:var(--sidebar-muted);opacity:.55;user-select:none;line-height:1.35}

/* Pinned nav slot: sits between the scrolling list and the shop name, so a
   short window steals rows from the middle of the nav rather than from the
   one item people go looking for. */
#sb-pinned:not(:empty){padding-top:4px;border-top:1px solid var(--sidebar-divider)}
.sb-footer{padding:16px;border-top:1px solid var(--sidebar-divider)}
.sb-shop{font-size:var(--fs-xs);font-weight:600;color:var(--sidebar-text)}
.sb-email{font-size:var(--fs-2xs);color:var(--sidebar-muted);margin-top:2px}
/* Quiet enough to ignore, present enough to read off when somebody asks which
   build you are running. */
.sb-build{font-size:var(--fs-2xs);color:var(--sidebar-muted);opacity:.6;margin-top:6px;
  font-variant-numeric:tabular-nums;cursor:default}
.sb-build-local{opacity:.85}
/* The update line is a button: same type as the build line, lit with the
   accent so it reads as "something you can do", never as an alarm. */
.sb-update{display:block;width:100%;text-align:left;background:none;border:0;padding:0;cursor:pointer;
  color:var(--sidebar-focus);opacity:.95;font:inherit;font-size:var(--fs-2xs);font-weight:600}
.sb-update:hover{text-decoration:underline}
.upd-meta{font-size:var(--fs-sm);color:var(--text-muted);margin-bottom:14px}
.upd-notes{white-space:pre-wrap;font-size:var(--fs-sm);line-height:1.55;max-height:40vh;overflow-y:auto;
  padding:12px 14px;border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface-2)}
.upd-progress{margin-top:16px}
.upd-bar{height:6px;border-radius:999px;background:var(--surface-2);overflow:hidden}
.upd-bar i{display:block;height:100%;width:100%;transform:scaleX(0);transform-origin:left;background:var(--accent);border-radius:inherit;transition:transform .25s ease}
.upd-bar.upd-indeterminate i{transform:scaleX(.35);animation:upd-slide 1.1s ease-in-out infinite alternate}
@keyframes upd-slide{from{transform:translateX(0) scaleX(.35)}to{transform:translateX(186%) scaleX(.35)}}
@media (prefers-reduced-motion:reduce){.upd-bar.upd-indeterminate i{animation:none;transform:scaleX(1)}}
.upd-progress-t{font-size:var(--fs-xs);color:var(--text-muted);margin-top:6px;font-variant-numeric:tabular-nums}

/* HEADER */
#hdr{position:fixed;top:0;left:var(--sidebar-width);right:0;height:var(--hh);background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 var(--cp);z-index:40;box-shadow:var(--shadow-sm)}
.hdr-id{display:flex;flex-direction:column;justify-content:center;gap:1px;min-width:0}
.hdr-title{font-size:var(--fs-xl);font-weight:600;line-height:1.2}
/* The area's tabs sit on the title row. One tab is a title, not a choice, so
   the strip is empty (and takes no room) for a single-page area. */
.hdr-row{display:flex;align-items:center;gap:18px;min-width:0}
/* Diagnosis: one row per area; the state is a segmented control, the finding
   is a field. The row tints with the state so the eye reads the column. */
.dx-seg{display:inline-flex;border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden}
.dx-st{min-height:30px;padding:4px 10px;border:0;background:none;font:inherit;font-size:var(--fs-xs);font-weight:500;color:var(--text-2);cursor:pointer;border-right:1px solid var(--border)}
.dx-st:last-child{border-right:0}
.dx-st.active{color:var(--text);background:var(--surface-2);font-weight:600}
.dx-st.dx-st-ok{background:var(--success-bg);color:var(--success-text)}
.dx-st.dx-st-damaged{background:var(--danger-bg);color:var(--danger-text)}
.dx-st.dx-st-later{background:var(--warning-bg);color:var(--warning-text)}
.dx-row-damaged td:first-child b{color:var(--danger-text)}
.dx-finding{min-height:32px}
/* The bell: a count on the icon, a list under it. */
.icon-btn.bell{position:relative}
.icon-btn.bell.has::after{content:attr(data-count);position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--accent-fill);color:var(--on-accent);font-size:var(--fs-2xs);font-weight:700;line-height:16px;text-align:center}
.bell-panel{position:absolute;top:calc(var(--hh) - 8px);right:120px;width:360px;max-height:70vh;overflow:auto;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-lg);z-index:60}
.bell-h{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid var(--border)}
.bell-item{display:block;width:100%;text-align:left;padding:10px 12px;border:0;border-bottom:1px solid var(--border);background:none;color:var(--text);font:inherit;cursor:pointer}
.bell-item:hover{background:var(--surface-2)} .bell-item b{display:block;font-size:var(--fs-sm)} .bell-item span{display:block;color:var(--text-2);font-size:var(--fs-xs);margin-top:2px} .bell-item i{display:block;color:var(--text-muted);font-size:var(--fs-xs);font-style:normal;margin-top:4px}
/* Messages: list left, thread right, the repair pinned above the thread. */
.msg-wrap{display:grid;grid-template-columns:340px 1fr;gap:16px;min-height:calc(100vh - var(--hh) - 48px)}
.msg-list{border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface);overflow:auto}
.msg-filters{display:flex;gap:2px;padding:10px 10px 6px;border-bottom:1px solid var(--border)}
.msg-row{display:block;width:100%;text-align:left;padding:10px 14px;border:0;border-bottom:1px solid var(--border);background:none;color:var(--text);font:inherit;cursor:pointer;position:relative}
.msg-row:hover{background:var(--surface-2)} .msg-row.on{background:var(--accent-soft)}
.msg-row-top{display:flex;justify-content:space-between;gap:8px;font-size:var(--fs-sm)} .msg-when{color:var(--text-muted);font-family:var(--font-mono,ui-monospace);font-size:var(--fs-xs)}
.msg-row-sub{display:block;color:var(--text-2);font-size:var(--fs-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px;margin-top:2px}
.msg-dot{position:absolute;right:12px;bottom:12px;width:8px;height:8px;border-radius:50%;background:var(--accent-fill)}
.msg-thread{border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface);display:flex;flex-direction:column;min-height:480px}
.msg-head{padding:12px 16px;border-bottom:1px solid var(--border)} .msg-pin{margin-top:6px;font-size:var(--fs-sm);color:var(--text-2)}
.msg-stream{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:12px;max-height:52vh}
.msg-b{max-width:78%;padding:10px 12px;border-radius:12px;background:var(--surface-2);font-size:var(--fs-sm);line-height:1.5}
.msg-b.me{align-self:flex-end;background:var(--accent-soft)} .msg-b.auto{align-self:flex-end;border:1px dashed var(--border);background:transparent}
.msg-subj{font-weight:600;margin-bottom:4px} .msg-meta{margin-top:6px;font-size:var(--fs-xs);color:var(--text-muted)} .msg-meta .ok{color:var(--success-text)} .msg-meta .bad{color:var(--danger-text)}
.msg-compose{padding:12px 16px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:8px} .msg-sign{font-size:var(--fs-xs)}
/* Home's greeting: a sentence, not a stat. */
.home-hello{margin:4px 0 20px}
.home-hello h2{font-size:var(--fs-2xl);font-weight:600;letter-spacing:-.01em;margin:0 0 4px}
.home-hello p{margin:0;color:var(--text-2);font-size:var(--fs-md)}
.hdr-tabs{display:flex;gap:2px;flex-wrap:nowrap}
.hdr-tabs:empty{display:none}
.hdr-tab{min-height:30px;padding:5px 10px;border-radius:var(--r-sm);border:0;background:none;font:inherit;font-size:var(--fs-sm);font-weight:500;color:var(--text-2);cursor:pointer;white-space:nowrap}
.hdr-tab:hover{background:var(--surface-2);color:var(--text)}
.hdr-tab.active{background:var(--accent-soft);color:var(--accent-ink);font-weight:600}
.hdr-tab:focus-visible{outline-offset:-2px}
/* The page's own summary, on the row that already names the page. Two lines
   fit inside the 64px header, which is 64px the content does not have to
   spend on a second header of its own. */
.hdr-sub{font-size:var(--fs-xs);color:var(--text-2);line-height:1.3;
  display:flex;align-items:center;flex-wrap:wrap;gap:5px}
.hdr-sub:empty{display:none}
.hdr-actions{display:flex;align-items:center;gap:8px}
.hdr-actions:empty{display:none}
.hdr-right{display:flex;align-items:center;gap:12px}
.hdr-search{position:relative;flex:1;max-width:520px;margin:0 24px}
.gs-input{width:100%;min-height:var(--tap);padding:8px 12px;border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface-2);font-size:var(--fs-sm);color:var(--text);transition:border-color .15s, box-shadow .15s}
.gs-input:focus{border-color:var(--accent);box-shadow:var(--ring);background:var(--surface)}
.gs-results{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-md);max-height:480px;overflow-y:auto;display:none;z-index:60}
.gs-results.open{display:block}
.gs-group{padding:6px 10px;font-size:var(--fs-2xs);font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;background:var(--surface-2);border-bottom:1px solid var(--border);position:sticky;top:0}
.gs-row{min-height:var(--tap);padding:8px 12px;display:flex;align-items:center;gap:10px;cursor:pointer;border-bottom:1px solid var(--border)}
.gs-row:hover,.gs-row.gs-active{background:var(--accent-soft)}
.gs-row:focus-visible{outline-offset:-3px}
.gs-row:last-child{border-bottom:0}
.gs-row .gs-id{font-weight:700;color:var(--accent-ink);font-size:var(--fs-xs);min-width:80px;font-family:var(--ff-mono)}
.gs-row .gs-main{flex:1;font-size:var(--fs-sm);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gs-row .gs-meta{font-size:var(--fs-2xs);color:var(--text-muted);text-align:right;flex:none}
.gs-empty{padding:16px;text-align:center;color:var(--text-muted);font-size:var(--fs-sm)}
.icon-btn{position:relative;background:none;border:none;cursor:pointer;color:var(--text-2);width:var(--tap-md);height:var(--tap-md);border-radius:var(--r-md);display:flex;align-items:center;justify-content:center;transition:background .15s}
.icon-btn:hover{background:var(--surface-2)}
.user-av{position:relative;width:var(--tap-md);height:var(--tap-md);border-radius:var(--r-full);background:var(--avatar-bg);color:var(--on-color);font-size:var(--fs-sm);font-weight:700;display:flex;align-items:center;justify-content:center;cursor:pointer;border:1px solid transparent}

/* MAIN */
#main{flex:1;margin-top:var(--hh);overflow-y:auto;height:calc(100vh - var(--hh))}
.page{display:none;padding:var(--cp);min-height:100%}
.page.active{display:block}
/* PAGE HEADER */
.ph{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.ph h2{font-size:var(--fs-2xl);font-weight:700}

/* BUTTONS */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:var(--tap);min-width:var(--tap);padding:0 16px;border-radius:var(--r-md);font-size:var(--fs-md);font-weight:500;cursor:pointer;border:none;transition:all .15s;font-family:inherit;line-height:1.2}
.btn-primary{background:var(--accent-fill);color:var(--on-accent)}.btn-primary:hover{background:var(--accent-fill-hover)}
.btn-secondary{background:var(--surface);color:var(--text-2);border:1px solid var(--border)}.btn-secondary:hover{background:var(--surface-2)}
.btn-success{background:var(--success-bg);color:var(--success-text)}
.btn-danger{background:var(--danger-bg);color:var(--danger-text)}
/* .btn-sm lives inside table rows — a visible 44px box would double row height,
   so the box is 36px and an invisible ::after carries the hit area to 44px. */
.btn-sm{position:relative;min-height:var(--tap-sm);min-width:0;padding:0 12px;font-size:var(--fs-xs)}
/* Set by handlers.js while a click's work is on its way; further clicks are ignored. */
[aria-busy="true"]{cursor:progress}

/* CARDS */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-md);padding:20px 24px}
/* STAT CARDS */
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-md);padding:20px;position:relative;overflow:hidden}
.stat-label{font-size:var(--fs-sm);font-weight:500;color:var(--text-2);margin-bottom:8px}
.stat-val{font-size:var(--fs-3xl);font-weight:700}/* GRID */
/* Back to `stretch`, deliberately, after trying the opposite.
   
   `align-items:start` was set to stop a short panel being padded out to match
   a tall one — sound in principle, and it produced a visibly ragged bottom
   edge that reads as a layout fault rather than as honesty about content. The
   real problem was never the alignment: it was that the status rows were 30px
   against a 44px neighbour, so the panels differed by 54px.
   
   With the rows at 38px the two come within a few pixels of each other on
   their own, and `stretch` absorbs the remainder invisibly. Fixing the cause
   and then letting the default do its job beats overriding the default to
   accommodate the cause. */
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
/* TABLES
   .tbl-wrap is given a bounded height so it becomes the scroll container the
   sticky header needs. Without this, `overflow-x:auto` alone makes .tbl-wrap a
   scroll container whose vertical scrollport never scrolls, and position:sticky
   on thead silently does nothing while the page scrolls behind it.
   Scoped to div.tbl-wrap: app.js also puts the class on a bare <table>. */
.tbl-wrap{overflow-x:auto}
div.tbl-wrap{overflow:auto;max-height:max(340px,calc(100vh - var(--hh) - 200px))}
table{width:100%;border-collapse:collapse;font-size:var(--fs-md)}
/* border-collapse hands borders to the table, so a sticky th loses its bottom
   rule while scrolling. The inset shadow rides with the cell instead. */
thead th{position:sticky;top:0;z-index:2;background:var(--surface-2);color:var(--text-2);font-weight:600;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;padding:10px 14px;text-align:left;border-bottom:1px solid var(--border);box-shadow:inset 0 -1px 0 var(--border);white-space:nowrap}
tbody tr{border-bottom:1px solid var(--border);transition:background .1s;cursor:pointer}
tbody tr:hover{background:var(--surface-2)}
tbody tr:last-child{border-bottom:none}
tbody tr:focus-visible{outline:3px solid var(--focus);outline-offset:-3px;background:var(--surface-2)}
/* COLUMN STABILITY — NOT SOLVED HERE, ON PURPOSE.
   Auto table-layout lets the widest customer name in the current result set
   decide where every later column starts, so the Status column moves when the
   filter changes. `max-width` on a td does NOT fix this: the auto algorithm
   treats it as a hint and redistributes anyway — measured live, a 51-character
   name still took 412px against a declared 280px cap, and the Status column
   still shifted 188px between filters. Shipping the cap would have added
   clipping risk for zero benefit.
   The fix needs a per-column hook — `table-layout:fixed` plus a <colgroup>
   (or widths on the th) — which is a markup change. See the report. */
/* One row height for every row, whatever is in it.
   Padding alone cannot do that: 12px above and below a line of text makes a
   42px row and the same padding around a 36px button makes a 60px one, so a
   table read as eight calm rows and then a tall one wherever an invoice
   happened to still be unpaid — the rhythm broke exactly where the eye was
   meant to keep moving. `height` on a table cell is a MINIMUM, so a genuinely
   two-line cell still grows; what it stops is a control setting the height of
   its row. 52px fits a 36px button and a 32px avatar with air around both. */
tbody td{padding:8px 14px;height:52px;vertical-align:middle}
tbody :focus-visible{outline-offset:-2px}
.td-muted{color:var(--text-2);font-size:var(--fs-sm)}

/* BADGES
   One construction, four roles. Every badge class below picks a tone token;
   none carries a colour of its own, so light and dark both follow the tokens. */
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:var(--r-full);font-size:var(--fs-xs);font-weight:500;white-space:nowrap}

/* ── FOUR ROLES, ONE QUESTION: WHOSE MOVE IS IT NOW ────────────────────────
   Ten tone families across four badge vocabularies collapsed to four roles.
   docs/status-tones-proposal.md holds the mapping and what died; statusRole()
   in src/app/01-core.js holds the rule, including the deadline flip.

   🔴 MOVING IS THE SOFT PILL AND NEVER ACCENT-FILL. Accent-fill is this app's
   button language; a filled pill sitting in a row of links reads pressable,
   and a status is not something you can press. Same construction as every
   other role — tinted ground, readable ink, hairline ring.

   THEIRS and SETTLED share one muted paint on purpose. The difference between
   them is that a THEIRS row always carries its age and a SETTLED row may drop
   it — a number is information, where a second grey would be decoration.

   The four roles draw on four of the ten tone families and introduce no new
   colour: neutral, blue, amber, red. Indigo, violet, teal, green, gold and
   orange stay defined — charts, lamps and sector chips still use them. What
   ended is their service as a status language. */
.role-ours{background:var(--tone-amber-bg);color:var(--tone-amber-fg);box-shadow:inset 0 0 0 1px var(--tone-amber-bd)}
.role-moving{background:var(--tone-blue-bg);color:var(--tone-blue-fg);box-shadow:inset 0 0 0 1px var(--tone-blue-bd)}
.role-theirs,.role-settled{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
.role-broken{background:var(--tone-red-bg);color:var(--tone-red-fg);box-shadow:inset 0 0 0 1px var(--tone-red-bd)}

/* ── Chips that are NOT repair statuses ────────────────────────────────────
   🔴 A SEPARATE VOCABULARY, ON PURPOSE.
   The four roles above answer one question: WHOSE MOVE IS IT ON A REPAIR.
   That question is meaningless for a user's permission tier, a "Tax exempt"
   flag, a read-only lock, or a purchase order's stage — none of them has a
   next actor in a repair workflow. Borrowing the role classes for them was a
   category error, and it showed: Admin rendered muted while Technician
   rendered amber, so the least-privileged role became the loudest chip in the
   table.

   Two axes, because these chips are saying two different things:

     EMPHASIS  chip-strong / chip-quiet    rank or flag. Admin outranks
                                           Technician; PRIMARY outranks the
                                           rest. Louder means more, not worse.
     PROGRESS  chip-open / chip-active /   a stage in a workflow that is not
               chip-done                   repair — purchasing, wholesale.

   ⚠️ GREEN LIVES HERE, AND IT DIED IN THE ROLES. That is not an inconsistency.
   Green died on repair statuses because it lied: "Ready for Pickup" read as
   success while 86 aircraft sat uncollected for a fortnight. A purchase order
   marked RECEIVED is genuinely finished and needs nothing from anybody, which
   is the thing green is honestly for. Same colour, different claim. */
.chip-strong{background:var(--tone-blue-bg);color:var(--tone-blue-fg);box-shadow:inset 0 0 0 1px var(--tone-blue-bd)}
.chip-quiet{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
.chip-open{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
.chip-active{background:var(--tone-amber-bg);color:var(--tone-amber-fg);box-shadow:inset 0 0 0 1px var(--tone-amber-bd)}
.chip-done{background:var(--tone-green-bg);color:var(--tone-green-fg);box-shadow:inset 0 0 0 1px var(--tone-green-bd)}

/* ── Legacy vocabularies, kept as aliases ─────────────────────────────────
   The four badge functions no longer emit these, but a dozen call sites still
   write them by hand — and screens outside this app's status language borrow
   them as a general palette (PO stages, wholesale stages, user roles). Each
   class maps to the role its statuses became, so a stale caller renders the
   new language rather than an unstyled pill.

   Two of them span a role boundary and had to be chosen rather than derived:
   `.s-await` covers Awaiting Estimate Approval (THEIRS) and Awaiting Repair
   (OURS), and `.s-pickup` is THEIRS only until its deadline. Both resolve to
   OURS here, because a hand-written class cannot do the deadline flip and a
   pill that is louder than the truth costs a glance, where one that is quieter
   than the truth costs a missed chase. Anything routed through statusBadge()
   gets the exact role and does not rely on this. */
.s-new,.s-repo,.s-await,.s-pickup,.b-draft,.stg-draft,.stg-approved,.stg-revised,
.dji-prep,.dji-ret,.s-dji-prep{background:var(--tone-amber-bg);color:var(--tone-amber-fg);box-shadow:inset 0 0 0 1px var(--tone-amber-bd)}
.s-diag,.s-repair,.s-qa,.dji-ship,.s-dji-ship{background:var(--tone-blue-bg);color:var(--tone-blue-fg);box-shadow:inset 0 0 0 1px var(--tone-blue-bd)}
.s-parts,.s-vendor,.s-dji,.s-closed,.s-done,.s-declined,.s-abandoned,
.b-sent,.b-paid,.b-void,.stg-published,.stg-declined,.stg-converted,
.dji-atdji,.dji-comp,.dji-decl,.dji-repl,
.s-dji-at,.s-dji-acc,.s-dji-dec,.s-dji-quote{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
/* The one legacy class that stays red: money promised and not paid is the only
   failed promise in the set. Every other red pill marked a CONCLUDED outcome —
   declined, abandoned, void — and red on a conclusion is the alarm crying wolf
   next to the one place it means something. */
.b-overdue{background:var(--tone-red-bg);color:var(--tone-red-fg);box-shadow:inset 0 0 0 1px var(--tone-red-bd)}

/* SECTOR — customers.segment, seven values, one tone each.
   A sector has to be readable while scanning a list, which is the whole reason
   the column exists: public safety is the segment this business leads with and
   the records could not name it. Same construction as every badge above, so
   both themes follow the tokens. There is deliberately no class for
   unclassified — that renders nothing at all, not a grey chip saying so. */
.seg-ps{background:var(--tone-blue-bg);color:var(--tone-blue-fg);box-shadow:inset 0 0 0 1px var(--tone-blue-bd)}
.seg-gov{background:var(--tone-indigo-bg);color:var(--tone-indigo-fg);box-shadow:inset 0 0 0 1px var(--tone-indigo-bd)}
.seg-edu{background:var(--tone-violet-bg);color:var(--tone-violet-fg);box-shadow:inset 0 0 0 1px var(--tone-violet-bd)}
.seg-com{background:var(--tone-teal-bg);color:var(--tone-teal-fg);box-shadow:inset 0 0 0 1px var(--tone-teal-bd)}
.seg-ag{background:var(--tone-green-bg);color:var(--tone-green-fg);box-shadow:inset 0 0 0 1px var(--tone-green-bd)}
.seg-util{background:var(--tone-gold-bg);color:var(--tone-gold-fg);box-shadow:inset 0 0 0 1px var(--tone-gold-bd)}
.seg-other{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
/* The agency type is a refinement of Public Safety, not a peer of it, so it
   stays neutral and lets the sector badge beside it carry the colour. */
.seg-agency{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}

/* TOOLBAR */
.toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
.search-in{flex:1;min-width:180px;max-width:320px;min-height:var(--tap);padding:9px 12px;border:1px solid var(--border);border-radius:var(--r-md);font-size:var(--fs-md);background:var(--field-bg);color:var(--text);font-family:inherit}
.search-in:focus{border-color:var(--accent);box-shadow:var(--ring)}
/* Selects.
   These kept the platform's own control chrome — macOS draws a double chevron
   in a tinted well — so a select sitting in a row of buttons looked like it
   came from a different application, which on the ticket page it visibly did.
   appearance:none strips that; the chevron below is drawn as a mask so it
   takes a token colour and follows the theme, rather than being baked into a
   data URI per theme. */
.filter-sel,
select.finput{
  appearance:none;
  -webkit-appearance:none;
  min-height:var(--tap);
  padding:0 36px 0 12px;
  border:1px solid var(--border);
  border-radius:var(--r-md);
  font-size:var(--fs-md);
  font-family:inherit;
  background:var(--field-bg);
  color:var(--text);
  cursor:pointer;
  line-height:1.2;
}
.filter-sel:hover,
select.finput:hover{border-color:var(--line-strong,var(--text-muted))}
.filter-sel:focus,
select.finput:focus{border-color:var(--accent);box-shadow:var(--ring);outline:none}
.filter-sel:disabled,
select.finput:disabled{cursor:not-allowed;color:var(--text-muted)}

/* The chevron rides on the select itself rather than on a wrapper: 31 other
   selects in this app are bare .finput, and a wrapper-only solution would have
   left every one of them with appearance:none and no affordance at all —
   strictly worse than the platform control we were replacing.

   A data URI cannot use currentColor, so the whole image is a token and the
   theme maps swap it, which is the same mechanism every colour here uses. */
.filter-sel,
select.finput{
  background-image:var(--chevron);
  background-repeat:no-repeat;
  background-position:right 13px center;
  background-size:10px 6px;
}

/* MODALS */
.overlay{display:none;position:fixed;inset:0;background:var(--scrim);z-index:1000;align-items:center;justify-content:center;padding:20px}
.overlay.open{display:flex}
.modal{background:var(--surface);border-radius:var(--r-xl);box-shadow:var(--shadow-modal);width:100%;max-width:640px;max-height:90vh;overflow-y:auto}
.modal-lg{max-width:820px}
.modal-hdr{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);z-index:10}
.modal-title{font-size:var(--fs-xl);font-weight:700}
.modal-x{position:relative;background:none;border:none;cursor:pointer;font-size:var(--fs-2xl);color:var(--text-muted);line-height:1;width:var(--tap-md);height:var(--tap-md);border-radius:var(--r-sm);display:flex;align-items:center;justify-content:center}
.modal-x:hover{background:var(--surface-2);color:var(--text)}
.modal-body{padding:24px}
.modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px;border-top:1px solid var(--border)}

/* FORMS */
.fg{margin-bottom:16px}
.frow{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.flabel{display:block;font-size:var(--fs-sm);font-weight:600;color:var(--text-2);margin-bottom:6px}
.finput{width:100%;height:var(--tap);padding:0 12px;border:1px solid var(--border);border-radius:var(--r-md);font-size:var(--fs-md);font-family:inherit;background:var(--field-bg);color:var(--text);transition:border-color .15s}
.finput:focus{border-color:var(--accent);box-shadow:var(--ring)}
.finput.err{border-color:var(--danger)}
/* A read-only field holding a machine value somebody copies — a payment link,
   a token, a serial. Monospace because the whole point is reading it character
   by character to check it. Defined here rather than inline because the inline
   version put `font-family:monospace` next to an `onclick` attribute in the
   same generated string, which is the apostrophe-trap shape ODRDesk/CLAUDE.md
   warns about and which the design detector could not parse either. */
.finput.fld-mono{flex:1;min-width:240px;font-family:var(--ff-mono);font-size:var(--fs-xs)}
textarea.finput{height:80px;padding:10px 12px;resize:vertical}

.ferr{font-size:var(--fs-xs);color:var(--danger-text);margin-top:4px;display:none}
.ferr.show{display:block}
/* The always-on twin of .ferr: what a field MEANS, rather than what is wrong
   with it. A constrained list is only usable if the distinction between its
   options travels with the control — "commercial vs dealer?" is not answerable
   from the labels alone. Same measurements as .rc-hint, which does this job on
   the receiving page; named for forms because that is where it is reached from. */
.fhint{font-size:var(--fs-xs);color:var(--text-muted);margin-top:6px;line-height:1.45}
.cbgroup{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}
.cbitem{display:flex;align-items:center;min-height:var(--tap-xs);gap:6px;cursor:pointer;font-size:var(--fs-sm)}
.cbitem input{cursor:pointer;accent-color:var(--accent);width:18px;height:18px}

/* TOAST */
#toasts{position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px}
.toast{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:12px 16px;box-shadow:var(--shadow-lg);font-size:var(--fs-md);font-weight:500;min-width:200px;max-width:300px;display:flex;align-items:center;gap:8px;animation:toastIn .3s ease}
.toast.ok{color:var(--success-text)}
.toast.err{color:var(--danger-text)}
.toast.info{color:var(--accent-ink)}
@keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
/* AVATAR — fill colour is supplied inline by app.js */
.av{border-radius:var(--r-full);color:var(--on-color);font-weight:700;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
/* CHARTS */
.chart-box{position:relative;height:220px}/* EMPLOYEE CARDS */
.emp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
.emp-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-md);padding:24px;text-align:center}
.emp-av{width:64px;height:64px;border-radius:var(--r-full);color:var(--on-color);font-size:var(--fs-2xl);font-weight:700;display:flex;align-items:center;justify-content:center;margin:0 auto 12px}
.emp-name{font-size:var(--fs-lg);font-weight:700}
.emp-role{font-size:var(--fs-sm);color:var(--text-2);margin:4px 0 16px}.dl-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.dl-label{font-size:var(--fs-xs);color:var(--text-muted);margin-bottom:2px}
.dl-val{font-size:var(--fs-md);font-weight:500}
/* TABS */
.tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:16px}
.tab-btn{min-height:var(--tap);padding:10px 16px;font-size:var(--fs-md);font-weight:500;cursor:pointer;background:none;border:none;color:var(--text-2);border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .15s;font-family:inherit}
.tab-btn.active{color:var(--accent-ink);border-bottom-color:var(--accent)}
.tab-btn:focus-visible{outline-offset:-3px}
.tab-pane{display:none}.tab-pane.active{display:block}
/* KANBAN */
.kanban{display:flex;gap:14px;overflow-x:auto;padding-bottom:16px}
.kb-col{flex-shrink:0;width:220px;background:var(--surface-2);border-radius:var(--r-lg);padding:10px}
.kb-col-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.kb-col-title{font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.kb-cnt{font-size:var(--fs-2xs);color:var(--text-2);background:var(--border);padding:1px 7px;border-radius:var(--r-full)}
.kb-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:10px;margin-bottom:8px;cursor:pointer;box-shadow:var(--shadow-sm);transition:box-shadow .15s}
.kb-card:hover{box-shadow:var(--shadow-md)}
.kb-id{font-size:var(--fs-2xs);font-weight:700;color:var(--accent-ink);margin-bottom:3px}
.kb-cust{font-size:var(--fs-sm);font-weight:600;margin-bottom:2px}
.kb-drone{font-size:var(--fs-xs);color:var(--text-2);margin-bottom:6px}
.kb-foot{display:flex;align-items:center;justify-content:space-between;font-size:var(--fs-2xs);color:var(--text-muted)}
.kb-card[draggable="true"]{cursor:grab}
.kb-card[draggable="true"]:active{cursor:grabbing}
.kb-col.kb-drag-over{outline:2px dashed var(--accent);outline-offset:-2px;background:var(--accent-soft)}
/* VIEW TOGGLE */
.vt{display:flex;border:1px solid var(--border);border-radius:var(--r-md);overflow:hidden}
/* Segmented control: the group clips overflow, so a ::after hit expander would
   be cut off. Real height instead — 40px, the header/toolbar control size. */
.vt-btn{min-height:var(--tap-md);padding:0 14px;background:none;border:none;cursor:pointer;font-size:var(--fs-sm);color:var(--text-2);transition:all .15s;font-family:inherit}
.vt-btn.active{background:var(--accent-fill);color:var(--on-accent)}
.vt-btn:focus-visible{outline-offset:-3px}
/* INVENTORY HIGHLIGHTS */
tr.low{background:var(--row-warn-bg)!important}tr.low:hover{background:var(--row-warn-hover)!important}
tr.oos{background:var(--row-danger-bg)!important}tr.oos:hover{background:var(--row-danger-hover)!important}
/* TIMELINE */
.tl{display:flex;flex-direction:column}.tl-dot{width:32px;height:32px;border-radius:50%;background:var(--surface-2);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:var(--fs-sm);z-index:1}
.tl-dot.done{background:var(--accent-fill);border-color:var(--accent-fill);color:var(--on-accent)}/* SECTION CARD */
.sc{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-md);margin-bottom:20px;overflow:hidden}
.sc-hdr{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border)}
.sc-title{font-size:var(--fs-lg);font-weight:700}
/* The right-hand side of a panel header. Says what the panel is showing and
   how it is ordered — which a title cannot, and which a reader otherwise has
   to infer from the rows. */
.sc-note{font-size:var(--fs-xs);color:var(--text-muted);text-align:right}
.sc-note-warn{color:var(--warning-text);font-weight:500}

/* Dashboard summary line. This replaced a heading that duplicated the one in
   the header bar with different words. Only the numbers survive, because only
   the numbers were information. */
.ph-tight{align-items:center}.dash-dot{color:var(--border)}
.dash-as-of{display:inline-flex;align-items:center;gap:2px;color:var(--text-muted)}
/* Reload is a maintenance action, not a task. It earns an icon beside the
   timestamp it refreshes, rather than a full button next to + New Ticket —
   where it competed with the one control on this screen that starts work. */
.dash-refresh{display:inline-flex;align-items:center;justify-content:center;
  width:24px;height:24px;padding:0;border:0;background:none;cursor:pointer;
  color:var(--text-muted);border-radius:var(--r-sm)}
.dash-refresh:hover{background:var(--surface-2);color:var(--text-2)}
.dash-refresh svg{width:14px;height:14px}
.dash-refresh:focus-visible{outline:3px solid var(--focus);outline-offset:1px}
/* QUICK ACTIONS */
.qa{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}/* INLINE QTY — 36px, not 44px: it sits inside an already-44px table row and a
   full-height field would force the row taller across the whole inventory list. */
.qty-in{width:68px;height:var(--tap-sm);padding:4px 8px;border:1px solid var(--border);border-radius:var(--r-sm);font-size:var(--fs-md);text-align:center;font-family:inherit;background:var(--field-bg);color:var(--text)}
.qty-in:focus{border-color:var(--accent);box-shadow:var(--ring)}
/* CUSTOMER DETAIL */
.cust-hdr{display:flex;align-items:center;gap:16px;padding:20px 24px;border-bottom:1px solid var(--border)}

/* ── EDIT MODE EXTENSIONS ── */
/* Edit-mode overlay controls sit in a row of three or four. A 44px hit expander
   would overlap its neighbours and steal their clicks, so these get a real
   32px box and no expander. */
.card-ctrl-btn{background:var(--overlay-ctrl-bg);border:1px solid var(--border);border-radius:var(--r-sm);width:var(--tap-xs);height:var(--tap-xs);font-size:var(--fs-xs);cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;transition:all .1s;box-shadow:var(--shadow-sm);padding:0;color:var(--text-2)}/* Isolated in a widget corner, so it can safely take a 44px expander. */
.widget-delete-btn{position:absolute;top:6px;right:6px;background:var(--overlay-ctrl-bg);border:1px solid var(--border);border-radius:50%;width:var(--tap-xs);height:var(--tap-xs);font-size:var(--fs-2xs);cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;z-index:5;line-height:1;padding:0;color:var(--text-2)}
/* PRINT */
/* Print. Literal black is intentional here: paper has no theme, and ink is
   ink. This is the one place a token would be wrong. */
@media print{#sidebar,#hdr,#toasts,#overlay,#edit-page-btn,#widget-panel,#edit-toolbar,.action-bar,.no-print{display:none!important}#main{margin:0;height:auto;overflow:visible}div.tbl-wrap{max-height:none;overflow:visible}thead th{position:static;box-shadow:none}.print-header{display:block!important}body{overflow:auto;background:#fff;color:#000;font-size:11pt}}
/* Priority, marked at the edge of the row rather than inside a cell.
   
   inset box-shadow rather than border-left: `border-collapse:collapse` on the
   table hands borders to the table itself, so a border on a <tr> is dropped
   silently. The shadow rides with the cell and survives it.
   
   Applied to the first cell only — one stripe at the start of the row, not a
   line under every column. */
.tk-prio-row td:first-child{box-shadow:inset 3px 0 0 var(--danger)}
.kb-prio{box-shadow:inset 3px 0 0 var(--danger)}

/* The identifier column must never be the thing that truncates. Every other
   cell can lose its tail to an ellipsis and stay useful; a half-printed ticket
   number is not a ticket number.
   
   🔴 The selector has to out-specify `table.tbl-fixed tbody td` (0,1,3), which
   is where the ellipsis comes from. Written first as plain `td.tk-id-cell`
   (0,1,1) it lost silently — the rule was in the stylesheet, the computed
   style still said `overflow:hidden`, and nothing anywhere said why. A rule
   that reads as protection and protects nothing is worse than no rule. */
table.tbl-fixed tbody td.tk-id-cell{white-space:nowrap;overflow:visible;width:1%}
/* ─── ODRdesk v7.1 ─── */
body,#hdr,#sidebar,.modal,.card,.sc,.toast,tbody tr,.stat-card{transition:background .2s,border-color .2s,color .2s}
.dm-btn{position:relative;width:var(--tap-md);height:var(--tap-md);border-radius:var(--r-full);border:1px solid var(--border);background:none;cursor:pointer;font-size:var(--fs-lg);display:flex;align-items:center;justify-content:center;transition:all .15s;color:var(--text-2)}
.dm-btn:hover{background:var(--surface-2)}
#confetti-cvs{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999}.cmt{display:flex;gap:10px;padding:10px 12px;border-radius:var(--r-md);background:var(--surface-sunken);border:1px solid var(--border);position:relative}
.cmt.pinned{background:var(--accent-soft)}.cmt-author{font-size:var(--fs-xs);font-weight:700}/* Text actions in a thread: 32px, sized to sit inside a comment card without
   pushing every comment 12px taller. */
.cmt-act{display:inline-flex;align-items:center;min-height:var(--tap-xs);background:none;border:none;font-size:var(--fs-2xs);color:var(--text-2);cursor:pointer;padding:2px 8px;border-radius:var(--r-sm);font-family:inherit}.batch-pill{background:var(--tone-blue-bg);color:var(--tone-blue-fg);border:1px solid var(--tone-blue-bd);padding:2px 9px;border-radius:var(--r-full);font-size:var(--fs-xs);font-weight:600;white-space:nowrap}
.row-sel{background:var(--row-sel-bg)!important}
.batch-bar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--accent-soft);border-radius:var(--r-md);border:1px solid var(--tone-blue-bd);margin-bottom:12px;flex-wrap:wrap}
.batch-bar-cnt{font-size:var(--fs-sm);font-weight:700;color:var(--accent-ink)}
.stitle{font-size:var(--fs-2xs);font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text-muted);margin-bottom:8px}.note-input{flex:1;padding:7px 10px;border:1px solid var(--border);border-radius:var(--r-md);font-size:var(--fs-sm);font-family:inherit;background:var(--field-bg);color:var(--text);resize:none;min-height:var(--tap)}
.note-input:focus{border-color:var(--accent);box-shadow:var(--ring)}
/* ===== AUTH UI ===== */
.login-overlay{position:fixed;inset:0;background:linear-gradient(135deg,var(--brand-deep) 0%,var(--brand-mid) 100%);z-index:2000;display:flex;align-items:center;justify-content:center;padding:20px}
.login-card{background:var(--surface);padding:32px;border-radius:var(--r-xl);box-shadow:var(--shadow-modal);width:100%;max-width:380px;display:flex;flex-direction:column}
.login-brand{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:18px;border-bottom:1px solid var(--border)}
/* The card title and its subtitle. These were five inline `font-size:20px`
   declarations across the login, setup and invitation cards — off the ramp, and
   duplicated once per card, so a sixth card meant a sixth copy. `--fs-xl` is the
   Title step the modal titles and the sidebar wordmark already use. */
.login-brand-title{font-size:var(--fs-xl);font-weight:700;line-height:1.2}
.login-brand-sub{font-size:var(--fs-xs);color:var(--text-muted)}
@keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-8px)}40%,80%{transform:translateX(8px)}}

/* ===== AVATAR DROPDOWN ===== */
.avatar-menu{position:absolute;top:56px;right:var(--cp);min-width:220px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-lg);z-index:60;padding:6px;animation:avatarMenuIn .12s ease-out}
@keyframes avatarMenuIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
.avatar-menu-hdr{padding:10px 12px;border-bottom:1px solid var(--border);margin-bottom:6px}
.avatar-menu-item{display:flex;align-items:center;gap:10px;width:100%;min-height:var(--tap);padding:9px 12px;background:none;border:none;color:var(--text);font-size:var(--fs-md);font-family:inherit;text-align:left;cursor:pointer;border-radius:var(--r-sm)}
.avatar-menu-item:hover{background:var(--surface-2)}
.avatar-menu-item:focus-visible{outline-offset:-3px}
.avatar-menu-divider{height:1px;background:var(--border);margin:6px 0}

/* ── running-timer chip in the header ───────────────────────────────── */
.timer-chip{display:flex;align-items:center;gap:6px;min-height:var(--tap-xs);padding:4px 10px;border-radius:var(--r-full);background:var(--accent-soft);color:var(--accent-ink);font-size:var(--fs-xs);font-weight:600;font-family:var(--ff-mono);cursor:pointer;border:1px solid transparent;transition:border-color .15s}
.timer-chip:hover{border-color:var(--accent)}
.timer-dot{width:8px;height:8px;border-radius:50%;background:var(--accent-fill);animation:timer-pulse 1.4s ease-in-out infinite}
.timer-sep{color:var(--text-muted);font-weight:400}
.timer-stop{color:var(--danger-text);font-weight:600;font-family:inherit;font-size:var(--fs-2xs);text-decoration:underline}
@keyframes timer-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}

/* ── ticket attachments (photos / docs / signatures) ────────────────── */
.att-drop{border:2px dashed var(--border);border-radius:var(--r-md);padding:18px;text-align:center;cursor:pointer;background:var(--surface-2);transition:border-color .15s,background .15s}
.att-drop:hover{border-color:var(--accent);background:var(--accent-soft)}
.att-drop.over{border-color:var(--accent);background:var(--accent-soft)}
.att-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px}
/* The tile is a real <button>, so the delete control cannot live inside it —
   nested buttons are invalid and get hoisted out by the parser. The wrapper
   owns the positioning context the delete is absolutely placed against, and the
   two buttons are siblings. */
.att-tile-wrap{position:relative}
.att-tile{width:100%;text-align:left;font:inherit;color:inherit;padding:0;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);overflow:hidden;cursor:pointer;display:flex;flex-direction:column;transition:border-color .15s,box-shadow .15s}
.att-tile:hover,.att-tile-wrap:hover .att-tile{border-color:var(--accent);box-shadow:var(--shadow-md)}
.att-tile:focus-visible{outline-offset:-3px}
.att-thumb-img{width:100%;height:120px;object-fit:cover;display:block;background:var(--surface-2)}
.att-thumb-doc{width:100%;height:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:var(--fs-4xl);background:var(--surface-2);color:var(--text-2)}
.att-thumb-ext{font-size:var(--fs-2xs);margin-top:4px;letter-spacing:.05em;color:var(--text-muted);font-weight:600}
.att-meta{display:block;padding:8px 10px;border-top:1px solid var(--border);min-width:0}
.att-name{display:block;font-size:var(--fs-xs);font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.att-sub{display:block;font-size:var(--fs-2xs);color:var(--text-muted);margin-top:2px}
/* .att-tile clips overflow, so a 44px expander would be cut off by the tile
   edge. Real 32px box, positioned clear of the corner. */
.att-del{position:absolute;top:6px;right:6px;background:var(--overlay-del-bg);color:var(--on-color);border:none;border-radius:50%;width:var(--tap-xs);height:var(--tap-xs);font-size:var(--fs-xs);line-height:1;cursor:pointer;padding:0;display:none;align-items:center;justify-content:center}
.att-tile-wrap:hover .att-del,.att-del:focus-visible{display:flex}
.att-del:hover{background:var(--danger)}

/* ── first-run / DB-recovery setup wizard ───────────────────────────── */
.setup-overlay{position:fixed;inset:0;background:linear-gradient(135deg,var(--brand-deep) 0%,var(--brand-mid) 100%);z-index:2100;display:flex;align-items:center;justify-content:center;padding:20px}
.setup-card{background:var(--surface);padding:32px;border-radius:var(--r-xl);box-shadow:var(--shadow-modal);width:100%;max-width:520px;display:flex;flex-direction:column}
.setup-brand{display:flex;align-items:center;gap:14px;margin-bottom:18px}
.setup-choices{display:flex;flex-direction:column;gap:10px}
.setup-choice{text-align:left;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:14px 16px;min-height:var(--tap);cursor:pointer;font-family:inherit;transition:border-color .15s,background .15s}
.setup-choice:hover{border-color:var(--accent);background:var(--surface-2)}
.setup-choice-title{font-size:var(--fs-md);font-weight:600;color:var(--text);margin-bottom:4px}
.setup-choice-sub{font-size:var(--fs-xs);color:var(--text-2);line-height:1.4}
.setup-err{background:var(--danger-bg);color:var(--danger-text);padding:10px 12px;border-radius:var(--r-md);font-size:var(--fs-xs);margin-bottom:14px;word-break:break-word}
.setup-spinner{display:inline-block;width:32px;height:32px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:setup-spin 0.8s linear infinite}
@keyframes setup-spin{to{transform:rotate(360deg)}}

/* ============================================================================
   TOUCH TARGET EXPANDERS
   An invisible ::after carries the hit area out to 44px on controls whose
   visible box is deliberately smaller. Only applied where the control is
   visually isolated — next to a neighbour, an expander steals its clicks.
   ========================================================================== */
.btn-sm::after,.icon-btn::after,.dm-btn::after,.user-av::after,.modal-x::after,
.widget-delete-btn::after{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:max(100%,var(--tap));height:var(--tap)}

/* ============================================================================
   KEYBOARD FOCUS — declared last so it wins over any component :focus rule.
   `tr` gets a ring too: app.js adds tabindex to table rows.
   ========================================================================== */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible,
[role="button"]:focus-visible,[contenteditable]:focus-visible,
tr:focus-visible,.nav-item:focus-visible,.gs-row:focus-visible,.kb-card:focus-visible,
.cbitem:focus-visible,.bc-item:focus-visible,.cb-item:focus-visible,.bc-hdr:focus-visible,
.ps-banner-title:focus-visible,.timer-chip:focus-visible,.att-tile:focus-visible,
.setup-choice:focus-visible,.avatar-menu-item:focus-visible,.kb-col:focus-visible{
outline:3px solid var(--focus);outline-offset:2px}
tr:focus-visible,tbody :focus-visible,.nav-item:focus-visible,.gs-row:focus-visible,
.tab-btn:focus-visible,.vt-btn:focus-visible,.avatar-menu-item:focus-visible,
.att-tile:focus-visible,.bc-hdr:focus-visible{outline-offset:-3px}
tr:focus-visible{background:var(--surface-2)}
/* Windows High Contrast / forced colours: keep a ring the OS will honour. */
@media (forced-colors:active){:focus-visible{outline:3px solid Highlight;outline-offset:2px}}
/* Respect a reduced-motion preference. */
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}

/* ── Stable table columns ────────────────────────────────────────────────
   Scoped to tables that declare a <colgroup>, never applied globally: the
   app renders ~20 tables and blanket fixed-layout would give every one of
   them equal columns. Auto-layout sized the Customer column from whichever
   name was widest in the current result set, so Status shifted horizontally
   between filters and muscle memory never formed. Truncation is recoverable
   because the cells carry a title attribute. */
table.tbl-fixed{table-layout:fixed}
table.tbl-fixed tbody td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
table.tbl-fixed tbody td .badge,
table.tbl-fixed tbody td .td-muted{max-width:100%}

/* ── Idle indicator ──────────────────────────────────────────────────────
   How long a ticket has sat untouched. Nothing in the shop's current tools
   can answer this, and every documented failure here is a state nobody
   moved on, so it earns column two. The bar is a magnitude cue for
   scanning; the number is the fact. Colour and position both encode the
   band, so it survives a monochrome screen and colour-blindness. */
.idle{display:flex;align-items:center;gap:8px;}
.idle-d{font-variant-numeric:tabular-nums;font-weight:600;min-width:42px;text-align:right;}
.idle-bar{flex:1;min-width:36px;height:4px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.idle-bar>i{display:block;height:100%;border-radius:var(--r-xs);}
.idle-fresh .idle-d{color:var(--text-muted);}   .idle-fresh .idle-bar>i{background:var(--tone-green-fg);}
.idle-warn  .idle-d{color:var(--tone-amber-fg);} .idle-warn  .idle-bar>i{background:var(--tone-amber-fg);}
.idle-stale .idle-d{color:var(--tone-red-fg);}   .idle-stale .idle-bar>i{background:var(--tone-red-fg);}
.idle-none  .idle-d{color:var(--text-muted);font-weight:400;}
/* The date the idle count is measured from. Deliberately quiet and after the
   bar: "233d" is what you scan the column with, the date is what you say out
   loud once you have stopped on a row. Tabular figures so a column of dates
   does not jitter. */
.idle-on{font-size:var(--fs-2xs);color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;}

/* 🔴 `.btn-link` was used on the DJI lookup control and defined nowhere, so it
   rendered as a default platform button — grey, chunky, and louder than the
   serial it sits beside. Written 2026-08-06 alongside the control itself and
   caught the same day by a review of the ticket page.

   It reads as a link because that is what it is: a quiet way to go and check
   something, sitting inline after a value. Not a primary action competing with
   the status control at the top of the page. */
.btn-link{
  display:inline; padding:0; border:0; background:none;
  font:inherit; font-size:var(--fs-xs);
  color:var(--accent); text-decoration:underline; text-underline-offset:2px;
  cursor:pointer;
}
.btn-link:hover{ color:var(--accent-ink,var(--accent)); }
.btn-link:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--r-xs); }
.tk-dji-check{ margin-left:6px; white-space:nowrap; }

/* The customer, on the identity line under the ticket number. It used to be a
   row of its own reading "Customer: <name> [Edit]" — a label stating the
   obvious and a button whose text matched a different button nearby. */
.tk-cust{ margin-top:2px; font-size:var(--fs-sm); }
.tk-cust a{ color:var(--accent); text-decoration:underline; text-underline-offset:2px; cursor:pointer; }
.tk-cust a:hover{ color:var(--accent-ink,var(--accent)); }

/* ── Dashboard: queues, not statistics ───────────────────────────────────
   Each card names work nobody is moving and is a real button into the
   filtered list. The screen it replaced showed four numbers that were
   constant on a daily timescale and named no action. */

/* ============================================================================
   DASHBOARD — queue cards and status bars
   The three doors on the front page, plus the sorted bar list. Severity
   carries on a dot (filled = critical, hollow = warning) as well as hue.
   ========================================================================== */
/* Flex rather than grid, and the reason is the ownership card. auto-fit
   collapses empty tracks, so a card that needs to be wider than its
   neighbours cannot express that as a span — the tracks it would span may
   not exist. Flex lets one card ask for more of the row while the others
   share what is left, and still wraps on a narrow window. */


/* ── The public repair endpoint panel ──────────────────────────────────────
   Built around one warning. `127.0.0.1` in the allowlist is correct while this
   endpoint is on loopback and dangerous the moment it is not, so the panel has
   to make that state legible at a glance rather than bury it in a field. */
.pt-card{padding:20px 22px;}
.pt-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;
  flex-wrap:wrap;margin-bottom:8px;}
.pt-state{font-size:var(--fs-xs);color:var(--text-2);display:inline-flex;
  align-items:center;gap:7px;}
/* Two channels, never colour alone: the dot changes fill AND the words change.
   Roughly 6% of men cannot separate this palette's red from its green. */
.pt-dot{width:8px;height:8px;border-radius:var(--r-full);flex:none;}
.pt-ok{background:var(--success);}
.pt-off{background:var(--text-muted);}
.pt-lede{font-size:var(--fs-xs);color:var(--text-muted);margin-bottom:14px;max-width:64ch;
  line-height:1.5;}
.pt-note{border-radius:var(--r-md);padding:12px 14px;font-size:var(--fs-xs);
  line-height:1.55;margin-bottom:16px;border:1px solid;}
.pt-warn{background:var(--tone-amber-bg);border-color:var(--tone-amber-bd);
  color:var(--tone-amber-fg);}
.pt-bad{background:var(--tone-red-bg);border-color:var(--tone-red-bd);
  color:var(--tone-red-fg);}
.pt-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;}
.pt-k{font-size:var(--fs-2xs);font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  color:var(--text-muted);margin-bottom:4px;}
.pt-v{font-size:var(--fs-sm);font-weight:560;}
.pt-h{font-size:var(--fs-2xs);color:var(--text-muted);margin-top:3px;line-height:1.45;}
.pt-origins{list-style:none;margin-top:6px;display:flex;flex-direction:column;gap:6px;}
.pt-origins li{font-family:var(--ff-mono);font-size:var(--fs-xs);padding:7px 10px;
  border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface-2);
  display:flex;align-items:center;gap:8px;}
/* The local origin is the one that has to come out later, so it is marked
   rather than sitting anonymously in a list of three. */
.pt-origins li.pt-local{border-color:var(--tone-amber-bd);background:var(--tone-amber-bg);
  color:var(--tone-amber-fg);}
.pt-tag{margin-left:auto;font-family:var(--ff);font-size:var(--fs-2xs);font-weight:600;
  text-transform:uppercase;letter-spacing:.06em;}
.pt-origins li.pt-none{font-family:var(--ff);color:var(--text-muted);background:none;
  border-style:dashed;}

/* ── A ticket card folded down to one line ─────────────────────────────────
   The clutter fix. Seven equally loud cards made the reader do the ranking the
   screen should have done; nothing was removed, it just stays shut until it is
   wanted. The header carries a summary so the whole ticket is readable without
   opening anything — see tkFold/tkFoldSummary in 01-core.js.

   The card keeps .card, so padding and border come from there and a fold can
   never drift away from the cards it sits between. */
.tk-fold{padding:0!important;overflow:hidden;}
.tk-fold-h{display:flex;align-items:center;gap:10px;width:100%;
  min-height:var(--tap);padding:12px 20px;border:0;background:none;cursor:pointer;
  font:inherit;text-align:left;color:var(--text);}
.tk-fold-h:hover{background:var(--surface-2);}
.tk-fold-h:focus-visible{outline:3px solid var(--focus);outline-offset:-3px;}
.tk-fold-t{font-size:var(--fs-sm);font-weight:600;display:flex;align-items:center;gap:8px;}
/* The summary is the whole point of folding, so it gets room and the title
   does not push it off the end. */
.tk-fold-s{margin-left:auto;font-size:var(--fs-xs);color:var(--text-muted);
  text-align:right;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tk-fold .chev{color:var(--text-muted);flex:none;transition:transform .16s;}
.tk-fold.open .chev{transform:rotate(90deg);}
.tk-fold-b{display:none;}
.tk-fold.open .tk-fold-b{display:block;}
/* The card inside a fold already carries its own padding and border, so it must
   not draw a second one within the fold's frame. */
.tk-fold-b > .card{border:0;box-shadow:none;margin-top:0!important;}

/* ── The one thing to do about this repair ─────────────────────────────────
   Reads the same nextActionFor rule the dashboard uses. Amber rather than the
   accent: it is a state worth noticing, not a link, and accent already means
   "this is an action" 29 times over in this app. */
.tk-next{display:flex;align-items:center;gap:14px;padding:13px 20px;
  border:1px solid var(--tone-blue-bd);background:var(--tone-blue-bg);
  border-radius:var(--r-md);margin-top:16px;}
.tk-next-t{font-size:var(--fs-sm);font-weight:650;color:var(--tone-blue-fg);}
.tk-next-s{font-size:var(--fs-xs);color:var(--tone-blue-fg);opacity:.85;margin-top:2px;}
.tk-next-late{border-color:var(--tone-amber-bd);background:var(--tone-amber-bg);}
.tk-next-late .tk-next-t,.tk-next-late .tk-next-s{color:var(--tone-amber-fg);}
/* Waiting on somebody else is information, not an alarm. */
.tk-next-wait{border-color:var(--border);background:var(--surface-2);}
.tk-next-wait .tk-next-t{color:var(--text-2);}
.tk-next-wait .tk-next-s{color:var(--text-muted);}

/* ── The queue cards ───────────────────────────────────────────────────────
   🔴 Four things were changed on 2026-08-12 because together they were the
   whole reason this screen read as generated rather than designed. None of
   them is about colour, and none changed a word of the content.

   1. EVERY CARD WAS THE SAME WIDTH. `flex:1 1 210px` on every child is the
      single most recognisable dashboard tell there is: a row of identical
      boxes where nothing is more important than anything else, so the eye has
      nowhere to land and the reader has to do the ranking the screen should
      have done. The cards now have a floor and no ceiling, so a card carrying
      a longer headline takes the room it needs and the row stops being a grid.

   2. THE CARDS LIFTED ON HOVER. `transform:translateY(-1px)` is decoration
      that says nothing true — a queue card does not become closer when you
      point at it. It also fights the focus ring, which moves with it.

   3. EVERY CARD TURNED BLUE ON HOVER. Accent is this app's "this is a link or
      an action" signal, spent 29 times inline in app.js on that meaning.
      Spending it again on "the mouse is here" devalues it everywhere else.

   4. 12px RADIUS ON A DENSE OPERATIONAL CARD. --r-lg is right for a container
      somebody reads; these are instruments in a row and --r-md sits tighter
      under a 28px figure. */
.q-grid{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;}
.q-grid>*{flex:1 1 auto;min-width:210px;}
.q-card{display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;
  padding:16px 18px;border:1px solid var(--border);border-radius:var(--r-md);
  background:var(--surface);cursor:pointer;font:inherit;min-height:var(--tap);
  transition:background-color .12s,border-color .12s;}
/* Background only. The first version reached for a --border-strong that
   this app has never defined — carried in from a standalone mockup with
   its own tokens. A shift in ground is enough signal for a hover, and it
   needs no new token in two theme maps that must stay identical. */
.q-card:hover{background:var(--surface-2);}
.q-card:focus-visible{outline:3px solid var(--focus);outline-offset:2px;}
/* Tighter than the default. At 28px, default tracking makes a figure look
   like body copy that grew; -0.02em makes it look like a measurement. */
.q-n{font-size:var(--fs-3xl);font-weight:650;line-height:1.05;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;}
/* A zero on an attention block is the win condition and should look calm,
   not absent: muted numeral, unlit lamp. If every block shouts all the time
   the strip is decoration. */
.q-card.q-calm .q-n{color:var(--text-muted);font-weight:500}
/* 🔴 A block with nothing to count renders NO NUMERAL AT ALL.
   It used to render an em-dash in the numeral slot. At body weight that read
   as a stray rule sitting under the caption — it looked like a broken progress
   bar, and was most of what made the top of this dashboard feel cluttered.
   Sizing the dash up to numeral scale only made it a longer rule. A block with
   no number should show no number; its caption and qualifier already say what
   is true, and the strip's shared anatomy tolerates the gap better than it
   tolerates a mark nobody can read. */
/* The caption is a label in plain sentence case, the same size and weight
   as a stat card's label (.stat-label), so a number on Home and a number on
   Money → Today read as one family. The tracked all-caps version was an
   eyebrow, and eyebrows over every number were a tell. */
.q-l{font-size:var(--fs-sm);font-weight:500;color:var(--text-2)}
/* No lamp. The numeral's colour carries the state (.q-crit / .q-warn below),
   and a dot beside every caption was one of the generated-dashboard tells the
   2026-09-04 direction removed. */
/* The critical card's second channel: a shape, not a colour. Only the
   critical tone carries it, so it is a marker rather than a decoration on
   every caption. */
.q-mark{display:inline-flex;align-items:center;color:var(--tone-red-fg);flex:none}
.q-mark svg{width:14px;height:14px}
.q-cap{display:flex;align-items:center;gap:7px;width:100%;
  padding-right:18px} /* clears the corner chevron so context text never sits under the handle */
.q-cap .q-of{margin-left:auto}
/* Age leading a quiet row: bold, fixed width so six rows align as a column. */
.q-row-idle-lead{flex:none;width:44px;text-align:right;font-weight:700;
  font-variant-numeric:tabular-nums}

/* The page's closing line: a door plus what it currently means. Replaces a
   whole section that existed to disclaim its own signal. */
.dash-foot{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  padding:14px 2px 0;border-top:1px solid var(--border);margin-top:4px}
.dash-foot-note{font-size:var(--fs-xs);color:var(--text-muted)}
.q-s{font-size:var(--fs-xs);color:var(--text-muted);}
.q-crit .q-n{color:var(--tone-red-fg);}
.q-warn .q-n{color:var(--tone-amber-fg);}

/* ── The ownership card's segmented bar ──────────────────────────────────
   One card, an honest headline, and the rest of the population in proportion
   underneath. Chosen over four separate cards (the row is already the widest
   thing on the screen) and over leaving one number at 700 (which names a
   condition rather than a task).

   Built from the same construction as .sbar-t below — a track with children
   sized by percentage — so this adds a class, not a charting library.

   🔴 The segments carry a border between them rather than relying on hue
   alone. Amber and green at 8px tall, abutting, is exactly the comparison
   that fails under deuteranopia, and the legend beneath is what actually
   carries the meaning. */
.qseg{display:flex;width:100%;height:8px;border-radius:4px;overflow:hidden;
  background:var(--surface-2);margin-top:10px}
.qseg>i{display:block;height:100%}
.qseg>i+i{border-left:1px solid var(--surface)}
.qseg-key{display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;margin-top:7px;
  font-size:var(--fs-2xs);color:var(--text-muted)}
.qkey{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;
  font-size:var(--fs-2xs);color:var(--text-muted);font-family:inherit;
  font-variant-numeric:tabular-nums}
.qseg-key i,.qkey>i{width:8px;height:8px;border-radius:var(--r-xs);flex:none}
/* The one legend entry that is also a destination. It looks like the others
   until you touch it, because it is first a fact about the queue and only
   second a place to go — promoting it to a button would put a second call to
   action on a card that already has one. */
/* Public safety, wherever it jumps the queue.

   Roger's instruction is that these go to the front, so the marker has to read
   at a glance. Indigo deliberately: it is the one tone the status badge set
   does not use, and the flag is a fact about the CUSTOMER rather than about the
   repair — if it shared a colour with a status it would be read as one.

   The icon is not decoration either. Tone alone fails under deuteranopia
   against the amber and red already on this row, so the shield carries the
   meaning and the colour reinforces it. */
.ps-flag{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;
  border-radius:var(--r-full);font-size:var(--fs-2xs);font-weight:600;
  white-space:nowrap;background:var(--tone-indigo-bg);color:var(--tone-indigo-fg);
  box-shadow:inset 0 0 0 1px var(--tone-indigo-bd)}
.ps-flag .ic{width:12px;height:12px}

.qkey-door{border:0;background:none;padding:0;cursor:pointer;border-radius:var(--r-sm)}
.qkey-door b{color:var(--accent);font-weight:600}
.qkey-door:hover{color:var(--text-2)}
.qkey-door:hover b{text-decoration:underline}
.qkey-door:focus-visible{outline:3px solid var(--focus);outline-offset:3px}

/* Number, label and denominator on one line instead of three. The number is
   the anchor and keeps its size; the other two are set against it rather than
   stacked under it. */
.q-head{display:flex;align-items:baseline;gap:10px;width:100%;flex-wrap:wrap}
.q-of{margin-left:auto;font-size:var(--fs-xs);color:var(--text-muted);
  font-variant-numeric:tabular-nums}

/* A card with two destinations cannot itself be a button — a button inside a
   button is invalid HTML and, more practically, a keyboard user tabbing to the
   card then to the link has no way to tell which one the Enter key will take.
   So the headline becomes its own hit area and the card becomes a container. */
.q-card-hit{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  width:100%;padding:0;border:0;background:none;cursor:pointer;font:inherit;
  text-align:left;border-radius:var(--r-sm)}
.q-card-hit:focus-visible{outline:3px solid var(--focus);outline-offset:3px}

/* A second destination out of one card. The population it opens is not
   something a technician can act on, so it reads as a line of prose with a
   link rather than as another button competing with the headline. */
.qdoor{display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;margin-top:12px;padding-top:11px;border-top:1px solid var(--border);
  font-size:var(--fs-xs);color:var(--text-2);background:none;border-left:0;
  border-right:0;border-bottom:0;cursor:pointer;font-family:inherit;text-align:left}
/* The ownership card was given 1.55× the width of its neighbours, on the
   argument that it carries more and matters more. Seeing it built, that reads
   as three cards of uneven size rather than as emphasis — the eye registers
   the mismatch before the meaning, and a row of tiles that are almost but not
   quite equal looks like a layout fault.
   
   Equal thirds. The card earns its emphasis from the bar and the legend it
   alone has, which is a stronger signal than 200px of extra width and does not
   cost the row its rhythm. */
.q-grid>.q-wide{flex:1 1 210px}   /* the same basis as its siblings — a
   different flex-basis with the same grow factor still produces different
   widths, which is how the first attempt at "equal" came out 358 / 448 / 358 */

/* Horizontal magnitude bars. Sorted, labelled, and comparable at a glance —
   which arcs in a doughnut are not, especially at eleven categories. */
/* 11px rather than 7px, so a status row is ~38px instead of 30px.
   
   Two things at once, which is why it is worth the note. The row beside this
   panel ("Quietest tickets") is 44px, so seven 30px rows came to 283px against
   its 337px and the pair read as ragged — Robert, seeing it: "still uneven".
   The fix is not padding the short panel out; it is that 30px was mean for a
   full-width clickable row in the first place. The nav settled on 40px for the
   same reason. */
.sbar{display:flex;align-items:center;gap:10px;width:100%;padding:11px 4px;border:0;
  background:none;cursor:pointer;font:inherit;text-align:left;border-radius:var(--r-sm);}
.sbar:hover{background:var(--surface-2);}
.sbar:focus-visible{outline:3px solid var(--focus);outline-offset:-2px;}
.sbar-l{flex:0 0 42%;font-size:var(--fs-sm);color:var(--text-2);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;}
/* ⚠️ The track is a hairline, not a filled well. Seven bars each with a solid
   8px track put twice the ink on this panel as the data did, and the track
   carries no information the number at the end does not already give. */
.sbar-t{flex:1;height:6px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.sbar-t>i{display:block;height:100%;border-radius:var(--r-full);background:var(--accent-fill);}
.sbar-n{flex:0 0 44px;text-align:right;font-size:var(--fs-sm);font-weight:600;
  font-variant-numeric:tabular-nums;}

.q-row{display:flex;align-items:center;gap:10px;width:100%;min-height:var(--tap);
  padding:8px 4px;border:0;border-bottom:1px solid var(--border);background:none;
  cursor:pointer;font:inherit;text-align:left;}
.q-row:last-of-type{border-bottom:0;}
.q-row:hover{background:var(--surface-2);}
.q-row:focus-visible{outline:3px solid var(--focus);outline-offset:-2px;}
.q-row-id{flex:0 0 96px;font-weight:700;color:var(--accent);font-size:var(--fs-sm);}
.q-row-cust{flex:1;font-size:var(--fs-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.q-row-idle{flex:0 0 46px;text-align:right;font-size:var(--fs-sm);font-weight:600;
  font-variant-numeric:tabular-nums;}
/* A queue row that carries its own action.
   The row stays a link to the ticket and the button sits beside it, so the two
   targets never overlap — a single element that both navigates and acts is how
   you get people opening a ticket when they meant to press the button.
   .q-row itself is a <button>; this variant is a <div> wrapping two, so the
   flex rules are restated rather than inherited. */
.q-row-act{display:flex;align-items:center;gap:10px;width:100%;min-height:var(--tap);
  padding:8px 4px;border-bottom:1px solid var(--border);}
.q-row-act:last-of-type{border-bottom:0;}
.q-row-act:hover{background:var(--surface-2);}
.q-row-hit{display:flex;align-items:center;gap:10px;flex:1;min-width:0;
  border:0;background:none;cursor:pointer;font:inherit;text-align:left;padding:0;}
.q-row-hit:focus-visible{outline:3px solid var(--focus);outline-offset:2px;
  border-radius:var(--r-sm);}

/* Everything genuinely in someone else's hands, in one line.
   It reads as a footnote on purpose: there is nothing to do about any of it,
   and giving it a row each would bury the things there IS something to do
   about. */
.q-wait-line{margin-top:10px;padding-top:10px;border-top:1px solid var(--border);
  font-size:var(--fs-xs);color:var(--text-muted);}
.q-wait-line b{color:var(--text-2);font-weight:600;}

/* An action that only became an action because a deadline passed. Amber rather
   than red: it is late, not broken, and red here would compete with the row
   tints the ageing bands already use. */
.btn-warn{background:var(--warning-bg);border-color:var(--warning);
  color:var(--warning-text);}
.btn-warn:hover{background:var(--tone-gold-bg);}

.q-more{width:100%;min-height:var(--tap);margin-top:8px;border:1px solid var(--border);
  border-radius:var(--r-md);background:none;cursor:pointer;font:inherit;
  font-size:var(--fs-sm);color:var(--text-2);}
.q-more:hover{background:var(--surface-2);}
.q-more:focus-visible{outline:3px solid var(--focus);outline-offset:2px;}
/* 🔴 DECLARED AFTER .q-more, and that is load-bearing.
   This lived earlier in the file at equal specificity, so .q-more simply won
   and the priority footer kept rendering as a full-width bordered box for a
   week — the fix was written, committed, and silently cancelled by the
   cascade. It is a link at the end of a sentence, not a call to action at the
   end of a list. */
.q-more-inline{width:auto;min-height:0;margin-top:0;border:0;border-radius:0;padding:0;
  font-weight:600;color:var(--accent)}
.q-more-inline:hover{background:none;text-decoration:underline}

/* Inline icons sit on the text baseline rather than the box bottom, so a
   label and its icon read as one object. Sized in em by icons.js, so they
   scale with whatever they sit in. */
.ic{vertical-align:-0.135em;flex:0 0 auto;}
button .ic,a .ic{pointer-events:none;}

/* ── Empty states ────────────────────────────────────────────────────────
   DESIGN.md specifies this register ("Section 700 --fs-lg … the empty-state
   line") but the class was never built, so 38 empty states were written as
   ad-hoc inline styles with padding varying from 6px to 40px, and 34 of them
   were a grey noun phrase with no way forward.

   An empty screen is the first thing a new install shows. It should teach
   the interface, not report an absence. */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:6px;padding:48px 24px;text-align:center;}
.empty-state .ic{color:var(--text-muted);opacity:.45;margin-bottom:6px;}
.empty-state-title{font-size:var(--fs-lg);font-weight:700;color:var(--text);}
.empty-state-body{font-size:var(--fs-sm);color:var(--text-2);max-width:46ch;line-height:1.5;}
.empty-state-act{margin-top:14px;}

/* ---------------------------------------------------------------------------
   Status deadlines (Settings) — the aging engine's controls.

   Rows, not a table: each line is one editable rule with its own reason
   underneath, and the reason is the part that stops someone changing 14 to 60
   because a queue looked long. A disabled rule fades rather than disappearing,
   so switching one off never hides that it exists.
--------------------------------------------------------------------------- */
.ar-row{
  display:grid;
  grid-template-columns:1fr auto auto;
  align-items:center;
  gap:14px;
  padding:11px 0;
  border-bottom:1px solid var(--border);
}
.ar-row:last-of-type{border-bottom:0}
.ar-name{
  display:block;
  font-size:var(--fs-sm);
  font-weight:600;
  color:var(--text);
  cursor:pointer;
}
/* The label points at a disabled input when the rule is off — clicking it does
   nothing, so it should not offer. */
.ar-off .ar-name{cursor:default}
.ar-note{
  margin-top:2px;
  font-size:var(--fs-xs);
  line-height:1.45;
  color:var(--text-muted);
  max-width:42ch;
}
.ar-ctl{display:flex;align-items:center;gap:6px}
.ar-days{
  width:68px;
  text-align:right;
  font-variant-numeric:tabular-nums;
}
.ar-unit{font-size:var(--fs-xs);color:var(--text-muted)}
.ar-sw{
  display:flex;
  align-items:center;
  gap:6px;
  font-size:var(--fs-xs);
  color:var(--text-2);
  cursor:pointer;
  user-select:none;
}
.ar-sw input{width:16px;height:16px;cursor:pointer}

/* Off is a state, not an absence. Everything stays legible and the row keeps
   its height so the list does not reflow as rules are toggled.

   Conveyed with the colour ramp rather than opacity. `opacity:.55` measured
   4.00:1 on --text and 2.30:1 on --text-muted against a light card — below the
   4.5:1 floor this system holds itself to — and opacity is the one dimming
   device here whose result cannot be checked against a token. Stepping down
   the ramp instead lands at 7.58:1 / 5.73:1 and still reads as recessed. */
.ar-off .ar-name{color:var(--text-2)}
.ar-off .ar-days{color:var(--text-muted)}

.ar-msg{
  margin-top:12px;
  min-height:1.2em;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}

.ar-terminal{
  margin-top:12px;
  padding:9px 11px;
  border-radius:var(--r-md);
  background:var(--surface-sunken);
  font-size:var(--fs-xs);
  line-height:1.5;
  color:var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Ticket detail — grouped facts.

   Replaces one flat grid of eleven equal cells, where "Status" and "Completed
   date" carried identical weight and the owner was absent entirely. Grouping
   is the hierarchy: a small uppercase heading per group, generous space
   between groups, tight space within one.
--------------------------------------------------------------------------- */
/* More space above a heading than below it, so a group reads as belonging to
   its own title rather than floating between two. */
.tk-group + .tk-group{
  margin-top:26px;
  padding-top:20px;
  border-top:1px solid var(--border);
}
/* A section heading, not another label.
   These were both 11px uppercase muted — 700 against 600 weight was the only
   difference — so "WHERE IT STANDS" and "STATUS" read as the same register and
   the grouping did nothing. A heading needs a different voice from the things
   it groups, or it is just more noise above them. */
.tk-group-h{
  font-size:var(--fs-sm);
  font-weight:650;
  letter-spacing:-.005em;
  color:var(--text);
  margin-bottom:14px;
}
/* Cap the column so three facts cluster together instead of being flung to
   the corners of a 1140px row. auto-fit with a 1fr max stretched every cell to
   a third of the window, which put STATUS and IDLE a hand-span apart. */
.tk-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(150px,220px));
  gap:16px 32px;
}
.tk-dates{
  margin-top:18px;
  padding-top:12px;
  border-top:1px solid var(--border);
  font-size:var(--fs-xs);
  color:var(--text-muted);
  font-variant-numeric:tabular-nums;
}

/* ---------------------------------------------------------------------------
   Ticket detail — the band that stays, and the three panels that swap.

   The page was eleven stacked cards in one column, roughly 3,400px on a seeded
   ticket. Every card on it is wanted by somebody, but not by the same person in
   the same minute, and the one fact needed throughout — which repair is this —
   was the first thing to scroll away. Three tabs, with the identity pinned.

   🔴 #main is the scrollport (`overflow-y:auto`), not the document. That is
   what makes `top:0` here pin against the window chrome. Sticky inside an
   ancestor that does not scroll does nothing at all and reports no error, so
   moving this markup out of .page is a silent way to break it.
--------------------------------------------------------------------------- */
.tk-sticky{
  position:sticky;
  top:0;
  z-index:20;
  /* .page carries var(--cp) of padding. Pulling the band out by that much and
     paying it back as padding lets the opaque background reach both gutters —
     without it, content scrolling underneath shows through a 24px strip down
     each side, which reads as a rendering fault rather than a design. */
  margin:calc(var(--cp) * -1) calc(var(--cp) * -1) 0;
  padding:var(--cp) var(--cp) 0;
  background:var(--bg);
}
/* The band is a header, a meta line and a tab row stacked: at .ph's usual 20px
   it cost 868px-tall windows a card's worth of height for whitespace alone. */
.tk-sticky .ph{margin-bottom:10px}
/* .tabs' own bottom rule becomes the band's edge, so its margin goes: the pane
   below supplies its own top spacing through the first card's margin-top. */
.tk-sticky .tabs{margin-bottom:0}

/* Status and idle, moved out of the "Where it stands" grid so they stay on
   screen. They are read as a sentence — badge, then how long it has sat — so
   they run inline rather than becoming two more labelled cells. */
.tk-meta{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:6px 12px;
  margin-bottom:12px;
  font-size:var(--fs-sm);
  color:var(--text-2);
}/* A count that is also the way to the photos. margin-left:auto parks it at the
   far end so it does not read as part of the status sentence beside it. */
.tk-meta-jump{
  display:inline-flex;
  align-items:center;
  gap:6px;
  min-height:var(--tap-sm);
  padding:0 12px;
  margin-left:auto;
  border:1px solid var(--border);
  border-radius:var(--r-full);
  background:var(--surface);
  color:var(--text-2);
  font-family:inherit;
  font-size:var(--fs-sm);
  font-weight:500;
  cursor:pointer;
}
.tk-meta-jump:hover{background:var(--surface-2);color:var(--text)}

/* All three panels are in the DOM at all times and only one is shown. That is
   deliberate and load-bearing: the async refreshers on this page look up
   assets-list-, att-list-, time-list- and tk-history-body by id straight after
   the render, and getElementById on an unrendered panel returns null, which
   they handle by doing nothing at all. */
.tk-tab-pane{display:none}
.tk-tab-pane.active{display:block}
/* A card brings its own margin-top; the release banner does not, and it is the
   first thing on the Estimate panel. */
.tk-tab-pane > .rel{margin-top:16px}

/* Nobody yet is a real answer, not a missing value — it reads as text rather
   than as an em dash, because an em dash looks like the field failed to load. */
/* "Nobody yet" is the largest single state on the board and was the faintest
   mark on the page — muted AND italic, while the healthy state rendered at
   17.85:1. The salience was backwards: the thing needing action whispered and
   the thing needing nothing shouted. Same ramp step as body text now, no
   italic. Still text rather than an em dash, which reads as a load failure. */
.tk-noowner{color:var(--text-2);font-weight:500}

/* The name of an account whose id resolves to nothing. Distinct class from
   "nobody at all" — they were sharing one, which said the two were the same
   state. They are not. */
.tk-unresolved{color:var(--text-2)}

/* Assigned to someone no longer on the account. Not an error and not normal:
   the ticket has an owner on paper and none in practice. */
.tk-owner-gone{
  display:inline-block;
  margin-left:6px;
  padding:1px 7px;
  border-radius:var(--r-full);
  background:var(--tone-orange-bg);
  color:var(--tone-orange-fg);
  box-shadow:inset 0 0 0 1px var(--tone-orange-bd);
  font-size:var(--fs-2xs);
  font-weight:600;
  white-space:nowrap;
}

.tk-idle{font-variant-numeric:tabular-nums}
.tk-idle.idle-stale{color:var(--danger-text);font-weight:600}
.tk-idle.idle-warn{color:var(--warning-text)}
.tk-idle.idle-fresh,
.tk-idle.idle-none{color:var(--text-2)}

/* The vendor a rule is scoped to. A chip rather than plain text, because
   "Vendor Service" and "Vendor Service · Autel" are two different rules and
   reading them as one is how a 60-day DJI number ended up governing Autel. */
.ar-vendor{
  display:inline-block;
  margin-left:6px;
  padding:1px 7px;
  border-radius:var(--r-full);
  background:var(--tone-orange-bg);
  color:var(--tone-orange-fg);
  box-shadow:inset 0 0 0 1px var(--tone-orange-bd);
  font-size:var(--fs-2xs);
  font-weight:600;
}

/* Where a status deadline came from. Three sources with different authority,
   so they read differently: an SOP is the shop's own standard, measured is
   this system's inference, and a hand-set value outranks both. */
.ar-src{display:flex;align-items:center;gap:7px;margin-top:5px;flex-wrap:wrap}
.ar-chip{
  padding:1px 7px;
  border-radius:var(--r-full);
  font-size:var(--fs-2xs);
  font-weight:600;
  white-space:nowrap;
}
.ar-chip-sop{background:var(--tone-indigo-bg);color:var(--tone-indigo-fg);box-shadow:inset 0 0 0 1px var(--tone-indigo-bd)}
.ar-chip-measured{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
.ar-chip-manual{background:var(--tone-teal-bg);color:var(--tone-teal-fg);box-shadow:inset 0 0 0 1px var(--tone-teal-bd)}
.ar-srcref{font-size:var(--fs-2xs);color:var(--text-muted)}
.ar-legend{
  display:flex;
  align-items:center;
  gap:6px;
  flex-wrap:wrap;
  margin-bottom:14px;
  padding:9px 11px;
  border-radius:var(--r-md);
  background:var(--surface-sunken);
  font-size:var(--fs-2xs);
  line-height:1.7;
  color:var(--text-muted);
}

/* A shared login is not a person. It reads as ownership on every board and
   names nobody accountable, which is why it gets its own marker rather than
   sharing the "not on the account" one — those are different problems. */
/* Three markers, three different fixes, and they must survive both a
   monochrome screen and colour-blindness — the bar this system sets itself.
   Amber and orange measured ΔE 9.3 normally and ΔE 0.0 under deuteranopia, so
   hue alone was doing nothing. Shared login moves to gold, which already means
   "escalated or exceptional" here, and each carries a glyph as a second
   channel. */
.tk-owner-shared,
.tk-owner-gone,
.tk-owner-unknown{
  display:inline-block;
  margin-left:6px;
  padding:1px 8px;
  border-radius:var(--r-full);
  font-size:var(--fs-2xs);
  font-weight:600;
  white-space:nowrap;
}
.tk-owner-shared b,
.tk-owner-gone b,
.tk-owner-unknown b{margin-right:3px;font-weight:700}

.tk-owner-shared{
  background:var(--tone-gold-bg);
  color:var(--tone-gold-fg);
  box-shadow:inset 0 0 0 1px var(--tone-gold-bd);
}
.tk-owner-unknown{
  background:var(--tone-neutral-bg);
  color:var(--tone-neutral-fg);
  box-shadow:inset 0 0 0 1px var(--tone-neutral-bd);
}


/* Let the queue rows actually shrink inside their column. Truncation needs a
   title attribute alongside it — a clipped label with no way to read it in
   full is a worse failure than a wrapped one. */
.q-row-cust{min-width:0}
.q-row .badge{min-width:0;overflow:hidden;text-overflow:ellipsis}
.sbar-l{min-width:0}

/* Five colored left-edge stripes were removed rather than kept: the toasts
   already colour their own text, and the public-safety banner and pinned
   comment already sit on a tinted background, so in every case the stripe
   carried no information the element was not already carrying. The repo row
   became a tinted row, which is subtler and more scannable in a dense table
   than a stripe on one cell. That is the whole test — a decoration that
   duplicates a signal is the tell; a channel that carries one is not.

   Severity needs a second channel: hue alone fails for ~6% of men, where
   --tone-red-fg and --tone-amber-fg collapse toward the same warm brown.
   The idle indicator already honours this with bar length.

   This was a 3px coloured bar down the left edge. The detector did not flag it
   because it was drawn with ::before rather than border-left — but that is a
   technicality, not a defence. A coloured stripe on the left of a card is the
   single most recognisable tell of generated UI whichever property draws it,
   and this system's own craft floor refuses it.

   A glyph carries the same information and is not that pattern. It also
   survives a monochrome screen, which the stripe only did by luck of value. */
.q-card{position:relative}
.q-card[style*="cursor:default"]:not(:has(.q-card-hit))::after{content:none}
.q-card::after{
  content:"›";
  position:absolute;
  right:14px;
  top:14px;
  font-size:var(--fs-lg);
  line-height:1;
  color:var(--text-muted);
  transition:transform .15s,color .15s;
}
.q-card:hover::after{transform:translateX(2px);color:var(--accent)}

/* An attachment whose preview will not load. Shown as a labelled tile, because
   a blank one reads as a file that was never uploaded. */
.att-thumb-failed{
  background:var(--surface-sunken);
  box-shadow:inset 0 0 0 1px var(--border);
  object-fit:none;
  font-size:var(--fs-2xs);
  color:var(--text-muted);
}

/* A comment carries its author's colour as a dot, not as a stripe down the
   side. The colour is real information — who wrote it — but the stripe form is
   the refused pattern, and a dot sits naturally beside the name it belongs to. */
.cmt-row{position:relative}
.cmt-row::before{
  content:"";
  position:absolute;
  left:4px;
  top:14px;
  width:6px;
  height:6px;
  border-radius:var(--r-full);
  background:var(--cmt-author,var(--accent));
}

/* Ticket header actions.
   Back is navigation and sits quiet at the left; the status control is what
   somebody came to change; destructive is icon-only at the end. A red-bordered
   "Delete (admin)" the same size as everything else made the most dangerous
   control the most visible one. */
.btn-quiet{
  background:transparent;
  border:1px solid transparent;
  color:var(--text-2);
  padding:0 10px;
}
.btn-quiet:hover{background:var(--surface-2);color:var(--text)}
.btn-icon{
  padding:0 10px;
  background:transparent;
  border:1px solid var(--border);
  color:var(--text-2);
}
.tk-destructive:hover{
  border-color:var(--danger);
  color:var(--danger-text);
  background:var(--danger-bg);
}

/* ============================================================================
   MONEY: WHAT WAS TAKEN vs WHAT WAS CONFIRMED
   Two sentences that look identical on a screen and are not:
   "this invoice is marked paid" (somebody here typed it) and "Square
   confirmed the money arrived" (an outside system agrees). No processor is
   connected today, so the second is never true, and a screen that shows one
   green PAID badge is asserting something the shop cannot actually know.

   The ledger is a three-row block, tabular so the decimal points stack. The
   confidence line below it is deliberately quiet: nothing is WRONG when a
   payment is unconfirmed, it is simply the limit of what is known, and an
   alarm colour here would cry wolf on every invoice in the system.

   Confirmed and unconfirmed differ in FORM as well as hue — a filled disc
   against a hollow ring — so the distinction survives greyscale and
   deuteranopia. Same dot language as the dashboard severity cues.
   ========================================================================== */
.stl{margin-top:16px;padding:16px;background:var(--surface-2);border-radius:var(--r-md)}
.stl-row{display:flex;justify-content:space-between;gap:16px;margin-bottom:4px;font-size:var(--fs-sm)}
.stl-row>span:first-child{color:var(--text-2)}
.stl-row>span:last-child{font-variant-numeric:tabular-nums}
.stl-row-total{font-weight:700;font-size:var(--fs-lg);margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
.stl-row-total>span:first-child{color:var(--text)}
.stl-due>span:last-child{font-weight:600}
.stl-owing>span:last-child{color:var(--tone-amber-fg)}
.stl-note{font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:normal;font-weight:400}

/* The ledger: one row per payment EVENT. The sums above answer "how much";
   these rows answer "which payments" — the deposit, the split card, the check —
   and carry the reference a reconciliation will need months later. */
.stl-ledger{margin-top:12px;padding-top:10px;border-top:1px dashed var(--border)}
.stl-pay{display:flex;align-items:baseline;gap:10px;padding:4px 0;font-size:var(--fs-xs)}
.stl-pay-date{flex:none;color:var(--text-muted);font-variant-numeric:tabular-nums;min-width:78px}
.stl-pay-what{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.stl-src{flex:none;color:var(--text-muted)}
.stl-src-conf{color:var(--success-text);font-weight:600}
.stl-pay-amt{flex:none;font-variant-numeric:tabular-nums;font-weight:600;min-width:72px;text-align:right}

/* Recording a payment, inline. Amount first because it is the one field that
   cannot be blank; the button states the verb, and the toast states the limit
   of what recording means. */
.stl-record{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border);flex-wrap:wrap}
.stl-rec-amt{width:110px;flex:none}
.stl-rec-method{flex:1;min-width:130px}
.stl-rec-ref{flex:1;min-width:110px}

/* The confidence line. Sits under the rule, reads as a footnote, not a fault. */
.stl-conf{display:flex;align-items:flex-start;gap:8px;margin-top:12px;padding-top:12px;
  border-top:1px solid var(--border);font-size:var(--fs-xs);line-height:1.5}
.stl-dot{flex:none;width:9px;height:9px;border-radius:var(--r-full);margin-top:4px}
/* Filled = an outside system agrees. Hollow = the shop's own word for it. */
.stl-yes .stl-dot{background:var(--tone-green-fg)}
.stl-yes{color:var(--success-text)}
.stl-no  .stl-dot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--text-muted)}
.stl-no{color:var(--text-2)}
.stl-conf b{font-weight:600}
.stl-conf a{color:var(--accent);text-decoration:underline;cursor:pointer}

/* ============================================================================
   CONNECTED SYSTEMS
   What is actually wired up, read from the database rather than hard-coded,
   so "Square is not connected" stops being true the moment somebody connects
   it — no release required. The blocked reason is the point of the row: a
   red cross tells nobody what to do next, a sentence does.
   ========================================================================== */
.conn{display:flex;flex-direction:column;gap:2px}
.conn-row{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;
  padding:10px 4px;border-bottom:1px solid var(--border)}

/* 🔴 The pressable styling lives on its OWN class, not on .conn-row.
   .conn-row is shared layout: the health panel below uses it too, and that
   panel's whole contract is that it reports and never acts — "a diagnostic
   that can change what it is diagnosing cannot be trusted about it". Putting
   cursor:pointer and a hover on the shared class made four read-only health
   rows look pressable. Caught by counting .conn-row in the live DOM and
   finding ten where six connectors exist. */
.conn-open{width:100%;text-align:left;background:none;
  border-left:0;border-right:0;border-top:0;
  font:inherit;color:inherit;cursor:pointer;border-radius:var(--r-sm)}
.conn-open:hover{background:var(--surface-2)}
.conn-open:focus-visible{outline:2px solid var(--focus);outline-offset:-2px}
.conn-row:last-child{border-bottom:0}
.conn-dot{width:9px;height:9px;border-radius:var(--r-full);align-self:center}
.conn-on  .conn-dot{background:var(--tone-green-fg)}
.conn-off .conn-dot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--text-muted)}
/* Configured but unproven: filled, but not the success hue. It is not a fault
   and must not read as one — nobody has checked yet, that is all. */
.conn-idle .conn-dot{background:var(--text-muted)}
.conn-idle .conn-state{color:var(--text-2)}
/* Tried and failed. This one IS a fault and earns the alarm colour. */
.conn-bad .conn-dot{background:var(--danger)}
.conn-bad .conn-state{color:var(--danger-text)}
.conn-name{font-size:var(--fs-sm);font-weight:600}
.conn-kind{font-size:var(--fs-2xs);color:var(--text-muted);text-transform:uppercase;
  letter-spacing:.06em;margin-left:8px;font-weight:500}
.conn-state{font-size:var(--fs-xs);color:var(--text-muted);white-space:nowrap}
.conn-on .conn-state{color:var(--success-text)}
.conn-why{grid-column:2/-1;font-size:var(--fs-xs);color:var(--text-2);line-height:1.5;margin-top:2px}

/* ============================================================================
   IMPROVE — findings and friction
   Every other page answers "what is happening now". This one answers "what
   keeps happening", and the two need to look different or it reads as another
   dashboard nobody opens.

   So: no cards-in-a-grid. A finding is a short piece of writing — a claim, the
   measurement under it, and a route to a decision — and it is laid out as one,
   at reading width, with the metadata demoted to a footer of small-caps keys.
   The reader is deciding what to raise with somebody, not scanning a queue.

   "Where the fix lives" is the one categorical hue on the page, and it earns
   it: two of the four values mean this is not a software problem, which is the
   most useful thing the page says and the easiest thing to skim past.
   ========================================================================== */
.imp-intro{max-width:68ch;font-size:var(--fs-sm);color:var(--text-2);line-height:1.6;margin-bottom:26px}
/* One measure for the whole page. The section rule marks the width of the
   content under it — running it to the viewport while the cards stop at 78ch
   reads as a layout accident rather than a device. */
.imp-sec{margin-bottom:34px;max-width:80ch}
.imp-h{font-size:var(--fs-md);font-weight:700;letter-spacing:-.01em;margin:0 0 4px;
  padding-bottom:8px;border-bottom:1px solid var(--border)}
.imp-sub{max-width:68ch;font-size:var(--fs-xs);color:var(--text-muted);line-height:1.6;margin:10px 0 4px}
.imp-empty{padding:18px 0;font-size:var(--fs-sm);color:var(--text-muted);max-width:68ch;line-height:1.6}

.imp-card{padding:18px 0;border-bottom:1px solid var(--border)}
.imp-card:last-child{border-bottom:0}
.imp-card-off{opacity:.55}
.imp-top{display:flex;align-items:baseline;justify-content:space-between;gap:16px}
.imp-title{font-size:var(--fs-md);font-weight:650;line-height:1.35;margin:0;text-wrap:balance}
.imp-ev{font-size:var(--fs-sm);color:var(--text-2);line-height:1.65;margin:8px 0 0;max-width:68ch}

/* Categorical, not severity. Hue says which world the fix lives in. */
.imp-where{flex:none;font-size:var(--fs-2xs);font-weight:600;letter-spacing:.04em;
  text-transform:uppercase;padding:3px 8px;border-radius:var(--r-full);border:1px solid}
.imp-sw{background:var(--tone-blue-bg);color:var(--tone-blue-fg);border-color:var(--tone-blue-bd)}
.imp-pr{background:var(--tone-amber-bg);color:var(--tone-amber-fg);border-color:var(--tone-amber-bd)}
.imp-bo{background:var(--tone-violet-bg);color:var(--tone-violet-fg);border-color:var(--tone-violet-bd)}
.imp-un{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);border-color:var(--tone-neutral-bd)}

/* Two columns, not one wrapping row. As a single flex row the status control
   landed on the metadata line or below it depending on how long the names
   happened to be, so the same control sat in a different place on every card
   and there was nothing to aim at. */
.imp-foot{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;
  gap:10px 22px;margin-top:14px}
.imp-metas{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 22px}
.imp-meta{font-size:var(--fs-xs);color:var(--text-2);display:flex;align-items:baseline;gap:6px}
.imp-k{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.07em;
  color:var(--text-muted);font-weight:600}
.imp-ref{font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:var(--fs-2xs)}
.imp-status{display:flex;align-items:center;gap:8px;justify-self:end}
.imp-status select.finput{height:32px;width:auto;min-width:118px;padding:0 30px 0 10px;font-size:var(--fs-xs)}

/* ── Friction: counts, not people ────────────────────────────────────────
   A bar and a number. The bar is for scanning the shape of the list, the
   number is the fact. Nothing here identifies anyone — see 18-improve.js. */
.imp-fr{margin-top:14px;display:flex;flex-direction:column}
.imp-fr-row{display:grid;grid-template-columns:150px minmax(0,1fr) 90px 44px;align-items:center;
  gap:14px;padding:9px 0;border-bottom:1px solid var(--border);font-size:var(--fs-xs)}
.imp-fr-row:last-child{border-bottom:0}
.imp-fr-kind{color:var(--text-muted);font-weight:600}
.imp-fr-where{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.imp-fr-det{font-weight:400;color:var(--text-muted)}
.imp-fr-bar{height:4px;border-radius:var(--r-full);background:var(--border);overflow:hidden}
.imp-fr-bar>i{display:block;height:100%;background:var(--text-muted);border-radius:var(--r-xs)}
.imp-fr-n{text-align:right;font-variant-numeric:tabular-nums;font-weight:650}

/* ── Two tabs, because they are two questions ────────────────────────────
   The findings are read by whoever runs the shop; the friction log is read by
   whoever builds the app. Stacked, each audience scrolled through the other's
   screen to reach their own. */
.imp-tabs{margin-bottom:22px}
.imp-tabn{margin-left:7px;font-size:var(--fs-2xs);font-weight:600;color:var(--text-muted);
  font-variant-numeric:tabular-nums}
.imp-fr-intro{margin:0 0 6px}

/* ── Filter by who can decide ────────────────────────────────────────────
   Not an assignee filter. "Who can answer this" is already on every finding,
   and filtering on it turns the page into the agenda for one conversation. */
.imp-tools{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
  gap:12px 24px;margin-bottom:20px}
.imp-who{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.imp-who-k{margin-right:2px}
.imp-who-b{display:inline-flex;align-items:center;gap:7px;min-height:var(--tap-xs);
  padding:0 12px;border-radius:var(--r-full);border:1px solid var(--border);
  background:var(--surface);color:var(--text-2);font-family:inherit;
  font-size:var(--fs-xs);font-weight:600;cursor:pointer;transition:border-color .15s,color .15s}
.imp-who-b:hover{border-color:var(--text-muted);color:var(--text)}
.imp-who-b.on{background:var(--accent-soft);color:var(--accent-ink);border-color:var(--accent)}
.imp-who-n{font-variant-numeric:tabular-nums;font-size:var(--fs-2xs);opacity:.7}

.imp-vt{display:inline-flex;border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden}
.imp-vt-b{min-height:var(--tap-xs);padding:0 14px;border:0;background:var(--surface);
  color:var(--text-2);font-family:inherit;font-size:var(--fs-xs);font-weight:600;cursor:pointer}
.imp-vt-b+.imp-vt-b{border-left:1px solid var(--border)}
.imp-vt-b.on{background:var(--accent-soft);color:var(--accent-ink)}

/* ── The lifecycle as a board ────────────────────────────────────────────
   🔴 The reason the page exists in one layout: as a list, a question that had
   been asked and not answered looked exactly like one nobody had raised. Five
   columns make that difference structural instead of something the reader has
   to hold in their head.

   The board is the one part of this page that is not at reading width — a
   pipeline is read across, so it opts out of the 80ch measure and scrolls
   horizontally rather than crushing five columns into a phone. */
.imp-sec-wide{max-width:none}
.imp-board{display:flex;align-items:flex-start;gap:12px;overflow-x:auto;padding-bottom:10px}
.impb-col{flex:1 1 0;min-width:224px;display:flex;flex-direction:column;
  background:var(--surface-sunken);border:1px solid var(--border);
  border-radius:var(--r-md);padding:10px 10px 12px}
.impb-hdr{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.impb-name{font-size:var(--fs-xs);font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--text-2);margin:0}
.impb-n{font-size:var(--fs-xs);font-weight:700;color:var(--text-muted);
  font-variant-numeric:tabular-nums}
.impb-body{display:flex;flex-direction:column}

/* An empty column keeps its place and says what would sit in it. A lane that
   vanishes when it empties takes the shape of the pipeline with it, and an
   empty "Asked" beside a full "Open" is itself a finding. */
.impb-empty{font-size:var(--fs-2xs);color:var(--text-muted);line-height:1.5;
  margin:10px 0 2px;padding:10px;border:1px dashed var(--border);border-radius:var(--r-sm)}

/* Waiting is the only thing on this page that gets an alarm, and only in the
   one column where waiting is the failure. */
.impb-stall{border-color:var(--tone-amber-bd);background:var(--tone-amber-bg)}
.impb-stall .impb-name,.impb-stall .impb-n{color:var(--tone-amber-fg)}
.impb-alert{display:flex;align-items:flex-start;gap:6px;margin:8px 0 0;
  font-size:var(--fs-2xs);font-weight:600;line-height:1.45;color:var(--warning-text)}
.impb-alert .ic{flex:none;margin-top:1px}

.impb-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);
  padding:10px;margin-top:8px;box-shadow:var(--shadow-sm)}
.impb-title{font-size:var(--fs-sm);font-weight:650;line-height:1.35;margin:7px 0 0;
  text-wrap:balance}
/* Clamped, not truncated mid-thought: the full prose is one click away in the
   list view, which is what that view is for. */
.impb-ev{font-size:var(--fs-2xs);color:var(--text-2);line-height:1.55;margin:6px 0 0;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.impb-needs{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:8px 0 0;
  font-size:var(--fs-2xs);color:var(--text-2);line-height:1.45}
.impb-wait{display:inline-block;margin:8px 0 0;padding:2px 8px;border-radius:var(--r-full);
  border:1px solid var(--border);background:var(--surface-2);color:var(--text-muted);
  font-size:var(--fs-2xs);font-weight:600;font-variant-numeric:tabular-nums}
.impb-wait-over{background:var(--tone-amber-bg);color:var(--tone-amber-fg);
  border-color:var(--tone-amber-bd)}
.impb-move{margin-top:10px}
.impb-move select.finput{height:var(--tap-xs);width:100%;padding:0 28px 0 8px;font-size:var(--fs-2xs)}

@media (max-width:720px){
  .imp-fr-row{grid-template-columns:1fr 44px;gap:4px 14px}
  .imp-fr-kind{grid-column:1/-1}
  .imp-fr-bar{display:none}
  .imp-foot{grid-template-columns:1fr}
  .imp-status{justify-self:start}
  .imp-tools{align-items:flex-start;flex-direction:column}
}


/* ============================================================================
   CAN THIS AIRCRAFT LEAVE THE BUILDING?
   The last question before a handover, so it sits above everything else on the
   repair page rather than being found by scrolling to the invoices section.

   Three states, and the middle one is the reason the component exists: an
   absent invoice is NOT proof nothing was billed — 1,173 invoices carry no
   ticket link — so "unknown" is a real answer with its own treatment, not a
   softened version of "unpaid".

   Silent on an unfinished repair. A banner that appears on every ticket is a
   banner people stop reading.
   ========================================================================== */
/* The border is uniform, not a 3px left rail.
   The state is already carried three times over — a tinted background, the
   dot, and the heading colour — so the rail was a fourth encoding of the same
   fact, and a coloured bar down one edge of a card is the most recognisable
   tell of a machine-made interface. Removing it costs no meaning: the border
   still takes the semantic colour, on all four sides, which reads as a tinted
   panel rather than a decorated one. */
.rel{display:flex;align-items:flex-start;gap:12px;padding:14px 16px;margin-bottom:16px;
  border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface)}
.rel-dot{flex:none;width:10px;height:10px;border-radius:var(--r-full);margin-top:4px}
.rel-body{flex:1;min-width:0}
.rel-head{font-size:var(--fs-md);font-weight:650;line-height:1.35}
.rel-detail{font-size:var(--fs-xs);color:var(--text-2);line-height:1.55;margin-top:4px;max-width:74ch}
.rel-money{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:2px;
  font-size:var(--fs-xs);color:var(--text-2);font-variant-numeric:tabular-nums}

/* Settled: filled disc, the only state that is a green light. */
.rel-ok{border-left-color:var(--success);background:var(--success-bg)}
.rel-ok .rel-dot{background:var(--success)}
.rel-ok .rel-head{color:var(--success-text)}

/* Owing: this one genuinely stops a handover, so it earns the alarm colour. */
.rel-owe{border-left-color:var(--danger);background:var(--danger-bg)}
.rel-owe .rel-dot{background:var(--danger)}
.rel-owe .rel-head{color:var(--danger-text)}

/* Unknown: hollow ring, same shape language as the payment-confidence dot.
   Not a fault and must not read as one — it means go and check. */
.rel-unknown{border-left-color:var(--tone-amber-bd);background:var(--tone-amber-bg)}
.rel-unknown .rel-dot{background:transparent;box-shadow:inset 0 0 0 2px var(--tone-amber-fg)}
.rel-unknown .rel-head{color:var(--tone-amber-fg)}

@media (max-width:700px){ .rel{flex-wrap:wrap} .rel-money{width:100%;align-items:flex-start;flex-direction:row;gap:14px} }

/* ── RECEIVING ────────────────────────────────────────────────────────────
   The check-in screen. The FORM is deliberately narrower than the rest of the
   app: it is filled top to bottom while somebody holds a drone, not scanned
   across, and full width would put the serial box and the save button at
   opposite ends of a 1440px screen.
   The waiting queue above it is not a form, though — it is a list of parcels
   read across, with a reference, an aircraft and two buttons on every row. Held
   to the form's width it truncated every title to "ZZ TEST do not action · f…",
   which is the one field the counter matches against the box in their hands. So
   the column is as wide as the list needs and the form cards keep their own
   cap. */
.rc-wrap{max-width:1040px;display:flex;flex-direction:column;gap:16px}
.rc-scan,
#rc-form,
.rc-done{max-width:760px}
.rc-scan{padding:20px}
.rc-scan-row{display:flex;gap:8px;align-items:center}
/* The serial is the first thing typed and the thing most often mistyped, so
   it gets a larger face and tabular figures — a transposed digit is visible
   in a way it is not at body size. */
.rc-serial-in{font-size:var(--fs-lg);font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace);
  letter-spacing:.02em;font-variant-numeric:tabular-nums}
.rc-hint{font-size:var(--fs-xs);color:var(--text-muted);margin-top:6px;line-height:1.45}

/* A serial that does not look like the serials we hold. It is a remark, not a
   refusal — the aircraft is on the counter either way — so it takes the warning
   tone rather than the danger one, and it sits under the field it is about
   instead of interrupting the save. */
.rc-serial-note{margin-top:8px;display:flex;gap:8px;align-items:flex-start;
  padding:10px 12px;border-radius:var(--r-md);background:var(--warning-bg);
  color:var(--warning-text);font-size:var(--fs-xs);line-height:1.45}
.rc-serial-note svg{width:15px;height:15px;flex:none;margin-top:1px}

/* Photos staged on the check-in form, before the ticket has an id. They reuse
   the ticket page's attachment tiles, with two differences: the tile is not a
   button because there is nothing yet to open, and the remove control is always
   visible — on the ticket page it appears on hover, which a keyboard or a touch
   screen cannot produce, and here it is the only way to undo a wrong file. */
.rc-photo-tile{cursor:default}
.att-del.rc-photo-del{display:flex}

.rc-known{margin-top:14px;padding:12px 14px;border-radius:var(--r-md);font-size:var(--fs-sm)}
.rc-known-wait{color:var(--text-muted);background:var(--surface-2)}
.rc-known-new{color:var(--success-text);background:var(--success-bg);
  display:flex;align-items:center;gap:8px}
.rc-known-new svg{width:15px;height:15px;flex:none}
.rc-known-warn{color:var(--warning-text);background:var(--warning-bg)}
.rc-known-seen{background:var(--accent-soft);border:1px solid var(--accent);padding:14px}
.rc-known-hd{display:flex;align-items:center;gap:8px;font-weight:600;
  color:var(--accent);margin-bottom:8px}
.rc-known-hd svg{width:15px;height:15px;flex:none}
.rc-prior{display:flex;align-items:center;gap:10px;width:100%;padding:7px 4px;
  border:0;border-top:1px solid var(--border);background:none;cursor:pointer;
  font:inherit;text-align:left;font-size:var(--fs-xs)}
.rc-prior:hover{background:var(--surface)}
.rc-prior-id{font-weight:700;color:var(--accent);flex:0 0 84px}
.rc-prior-cust{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rc-prior-date{color:var(--text-muted);flex:0 0 auto;font-variant-numeric:tabular-nums}
.rc-usethis{margin-top:10px}
/* A conflict is not a warning to style quietly. Two customers on one serial
   means somebody is about to put a repair on the wrong account. */
.rc-conflict{margin-top:10px;padding:10px 12px;border-radius:var(--r-md);
  background:var(--danger-bg);color:var(--danger-text);font-size:var(--fs-xs);
  display:flex;gap:8px;align-items:flex-start}
.rc-conflict svg{width:15px;height:15px;flex:none;margin-top:1px}

.rc-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.rc-span{grid-column:1/-1}
@media (max-width:700px){.rc-grid{grid-template-columns:1fr}}
/* Says which fields will refuse to be blank, before somebody finds out by
   pressing the button. */
.rc-req{font-size:var(--fs-2xs);font-weight:500;color:var(--danger-text);
  text-transform:none;letter-spacing:0;margin-left:6px}

.rc-chips{display:flex;flex-wrap:wrap;gap:8px}
.rc-chip{position:relative;display:inline-flex}
.rc-chip input{position:absolute;opacity:0;width:0;height:0}
.rc-chip span{display:inline-block;padding:8px 14px;border:1px solid var(--border);
  border-radius:var(--r-full);font-size:var(--fs-sm);cursor:pointer;
  background:var(--surface);transition:border-color .12s,background .12s;
  min-height:var(--tap);display:inline-flex;align-items:center}
.rc-chip input:checked+span{border-color:var(--accent);background:var(--accent-soft);
  color:var(--accent);font-weight:600}
.rc-chip input:focus-visible+span{outline:3px solid var(--focus);outline-offset:2px}

.rc-chosen{display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:10px 12px;background:var(--accent-soft);border:1px solid var(--accent);
  border-radius:var(--r-md);font-weight:600;color:var(--accent);min-height:var(--tap)}
.rc-clear{border:0;background:none;cursor:pointer;color:var(--accent);
  display:inline-flex;padding:4px;border-radius:var(--r-sm)}
.rc-clear svg{width:14px;height:14px}
.rc-clear:hover{background:var(--surface)}

.rc-hits{position:absolute;top:100%;left:0;right:0;z-index:50;margin-top:4px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);
  box-shadow:var(--shadow-md);max-height:260px;overflow-y:auto}
.rc-hit{display:block;width:100%;padding:9px 12px;border:0;background:none;
  cursor:pointer;font:inherit;text-align:left;font-size:var(--fs-sm);
  border-bottom:1px solid var(--border)}
.rc-hit:hover{background:var(--surface-2)}
.rc-hit-em{display:block;font-size:var(--fs-2xs);color:var(--text-muted)}
.rc-hit-none{color:var(--text-muted);font-size:var(--fs-xs);cursor:default}
/* The matcher's evidence line — "same phone number · same organisation domain".
   Rendered verbatim from the backend so a suggestion can be interrogated; the
   high-confidence variant leans on the accent to be found, not shouted. */
.rc-hit-head{color:var(--text-muted);font-size:var(--fs-2xs);cursor:default;
  text-transform:uppercase;letter-spacing:.05em}
.rc-hit-why{display:block;font-size:var(--fs-2xs);color:var(--text-muted);margin-top:2px}
.rc-hit-why-hi{color:var(--accent)}

/* ── The ticket page's part picker ─────────────────────────────────────────
   Rides the rc-hits pattern. Stock is a state, so it gets a tone: on-hand is
   quiet, none-on-hand warns without forbidding — the bench adds a part to an
   estimate before ordering it all the time. */
.ap-stock{font-size:var(--fs-2xs);color:var(--success-text)}
.ap-stock-none{color:var(--warning-text)}
.ap-chosen-row{display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:9px 12px;margin-bottom:6px;background:var(--surface-2);
  border:1px solid var(--border);border-radius:var(--r-md);font-size:var(--fs-sm)}
.rc-linkbtn{border:0;background:none;padding:0;color:var(--accent);cursor:pointer;
  font:inherit;font-size:var(--fs-xs);text-decoration:underline}

/* ── REPAIRS STARTED ON THE WEBSITE ───────────────────────────────────────
   The queue between a public form and a repair. Every one of these rows is a
   customer who has already told us what is wrong and is waiting to hear back,
   so it sits above the check-in form rather than below it.

   🔴 Styled deliberately close to, but not identical to, a ticket row. Nothing
   here is a repair yet — the aircraft may never arrive — and a queue that looks
   exactly like the board invites somebody to treat it as one. */
.rc-webq-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding-bottom:12px;border-bottom:1px solid var(--border);margin-bottom:4px}
.rc-webq-sub{font-size:var(--fs-xs);color:var(--text-muted)}
.rc-webq-row{display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px;padding:14px 0;border-bottom:1px solid var(--border)}
.rc-webq-row:last-child{border-bottom:0;padding-bottom:0}
.rc-webq-main{min-width:0;flex:1}
/* One line, always. Wrapping put a long aircraft name on its own row and left
   the reference stranded above it, so the queue's rows each started in a
   different place and the eye had nothing to run down. The title truncates
   instead — the full text is two lines below it in the description anyway. */
.rc-webq-l1{display:flex;align-items:baseline;gap:10px;flex-wrap:nowrap;min-width:0}
.rc-webq-l1>.badge{flex:none;align-self:center}
/* The reference the CUSTOMER holds. It is what they read down the phone, so it
   is the largest thing on the row and set in the same face as a serial. */
.rc-webq-ref{font-weight:700;color:var(--accent);font-size:var(--fs-md);
  letter-spacing:.02em;flex:none;white-space:nowrap}
.rc-webq-kit{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  min-width:0;flex:1}
.rc-webq-l2{font-size:var(--fs-sm);color:var(--text-2);margin-top:4px}
.rc-webq-l3{font-size:var(--fs-xs);color:var(--text-muted);margin-top:3px;
  line-height:1.5;overflow-wrap:anywhere}
.rc-webq-track{font-variant-numeric:tabular-nums}
.rc-webq-acts{display:flex;align-items:center;gap:8px;flex:none}
@media (max-width:700px){
  .rc-webq-row{flex-direction:column;gap:10px}
  .rc-webq-acts{width:100%}
}

/* The strip that says this check-in is standing on a submission. It has to be
   impossible to miss: pressing the button accepts a customer's own words about
   who they are, and the undo for that is a merge. */
.rc-webq-banner{display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px;margin-bottom:18px;padding:12px 14px;border-radius:var(--r-md);
  background:var(--accent-soft);border:1px solid var(--accent)}
.rc-webq-banner b{color:var(--accent)}
.rc-webq-banner .rc-hint{margin-top:4px}
.rc-webq-banner-who{font-size:var(--fs-sm);color:var(--text-2)}
@media (max-width:700px){.rc-webq-banner{flex-direction:column;gap:8px}}

.rc-foot{display:flex;align-items:center;justify-content:space-between;gap:16px;
  margin-top:20px;padding-top:18px;border-top:1px solid var(--border)}
.rc-err{color:var(--danger-text);font-size:var(--fs-sm);font-weight:500}

/* ── CAPTURE HEALTH ───────────────────────────────────────────────────────
   Is the shop still recording what it used to record?

   🔴 Sits above the observations on the Improve page, and the ordering is the
   argument: a field that STOPPED being captured is not one finding among
   several, it is the thing that produces findings. Ownership fell from the
   eighties to almost nothing across February 2026 and nothing noticed for five
   months. */
.cap-card{margin-bottom:20px}
.cap-intro{font-size:var(--fs-sm);color:var(--text-2);margin:0 0 16px;max-width:68ch;line-height:1.55}
.cap-row{display:grid;grid-template-columns:1fr;gap:6px;padding:14px 0;
  border-top:1px solid var(--border)}
.cap-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.cap-label{font-size:var(--fs-md);font-weight:600}
.cap-now{font-size:var(--fs-2xl);font-weight:700;line-height:1;
  font-variant-numeric:tabular-nums}
.cap-pc{font-size:var(--fs-sm);font-weight:600;margin-left:1px;opacity:.6}

/* A sparkline of divs. Twelve bars does not need a charting library, and each
   carries its own figures in a tooltip so the shape is never the only way to
   read it. */
/* The baseline matters more than it sounds. A field at 0% draws twelve bars two
   pixels tall, and without a rule under them the row reads as a chart that
   failed to render rather than a measurement that came back zero. Those are
   opposite conclusions: one makes you distrust the panel, the other makes you
   fix the field. */
.cap-spark{display:flex;align-items:flex-end;gap:3px;height:34px;width:100%;
  border-bottom:1px solid var(--border);padding-bottom:1px}
.cap-spark>i{flex:1;min-width:4px;min-height:2px;border-radius:var(--r-xs) var(--r-xs) 0 0;
  display:block;background:var(--text-muted)}
.cap-verdict{font-size:var(--fs-xs);color:var(--text-2);line-height:1.5}
.cap-verdict b{font-variant-numeric:tabular-nums}

/* Four states, and they mean different things:
     bad  a break that is still happening      — a change nobody decided on
     was  a break that recovered               — worth knowing, not urgent
     low  never captured consistently          — a gap somebody chose
     ok   steady                               — nothing to do
   `low` is deliberately neutral rather than red: a field that was never
   started is not a fault, and colouring it as one would bury the one that is. */
.cap-bad .cap-now{color:var(--danger-text)}
.cap-bad .cap-spark>i{background:var(--danger)}
.cap-was .cap-now{color:var(--warning-text)}
.cap-was .cap-spark>i{background:var(--warning)}
.cap-ok  .cap-now{color:var(--success-text)}
.cap-ok  .cap-spark>i{background:var(--success)}
/* `low` gets a real colour, not the border grey the bars default to. At 74% the
   faint version read as a disabled row — the healthiest thing on the panel
   looked switched off, which is exactly backwards. Slate says "measured, and
   nothing is wrong with it" without competing with the red. */
.cap-low .cap-now{color:var(--text-2)}
.cap-low .cap-spark>i{background:var(--text-muted);opacity:.55}
/* A month where the field was never filled. It draws a flat tick on the
   baseline rather than nothing at all, because twelve empty columns read as a
   chart that failed rather than as the measurement they are. Squared off, and
   deliberately after the four state colours so it keeps the border grey in all
   of them: an absence must not look like a very small amount of something. */
.cap-spark>i.cap-nil{height:2px;border-radius:0;background:var(--border);opacity:1}

/* The line under a list saying how much of it you are looking at.
   Two states, and they are deliberately different colours: a plain count is
   information, a truncated count is something the reader has to act on if the
   row they want is not there. */
.list-note{font-size:var(--fs-sm);color:var(--text-muted);margin-top:10px}
.list-note-capped{color:var(--warning-text)}
.list-note b{font-variant-numeric:tabular-nums}

/* The serial group in global search. Marked out from the other group headings
   because it answers a different question: the rest are "what matched", this
   one is "here is the aircraft you are holding". */
.gs-group-serial{color:var(--accent);background:var(--accent-soft)}
.gs-warn{color:var(--danger-text);font-weight:600}

/* The waiting queue. Deliberately NOT the accent used for repairs: nothing in
   this group is a repair yet, and colouring it like one is how somebody
   concludes a ticket exists. Amber is the shop's "needs a person" tone. */
.gs-group-intake{color:var(--warning-text);background:var(--warning-bg)}
.gs-row-intake .gs-id{color:var(--warning-text)}
/* Two lines in one row: what it is, then who sent it. The second line is the
   part that makes a stranger's parcel identifiable. */
.gs-sub{display:block;font-size:var(--fs-2xs);color:var(--text-muted);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gs-cta{display:block;font-size:var(--fs-2xs);font-weight:700;color:var(--warning-text);margin-top:2px}
.gs-empty-sub{margin-top:6px;font-size:var(--fs-xs)}
.gs-empty-sub a{color:var(--accent);cursor:pointer;text-decoration:underline}

/* What is currently being filtered, said on screen rather than left implicit.
   The dashboard's cards navigate into filtered lists, and without this the
   toolbar reads "All Statuses" over a list holding 560 of 724 rows. */
.fchips{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.fchips-l{font-size:var(--fs-xs);color:var(--text-muted);margin-right:2px}
.fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 10px;
  border:1px solid var(--accent);background:var(--accent-soft);color:var(--accent);
  border-radius:var(--r-full);font:inherit;font-size:var(--fs-xs);font-weight:600;
  cursor:pointer;line-height:1.4}
.fchip:hover{background:var(--surface)}
.fchip:focus-visible{outline:3px solid var(--focus);outline-offset:2px}
/* No font-size of its own. This held `font-size:15px` to make a `&times;` text
   glyph look right beside a 12px label — an off-ramp value that existed only to
   compensate for a character. The glyph is now icon('close'), which renders at
   1em and therefore inherits the chip's --fs-xs, which is what the icon system
   is for. Removing the override was part of replacing the glyph and I missed it. */
.fchip-x{line-height:1;opacity:.7}
.fchip:hover .fchip-x{opacity:1}
/* Clear-all is an escape hatch, not a filter, so it does not wear the accent. */
.fchip-all{border-color:var(--border);background:none;color:var(--text-2);font-weight:500}

/* A kanban column that is showing only part of itself. Same rule as the table
   footer: truncation is never silent. */
.kb-more{font-size:var(--fs-2xs);color:var(--warning-text);padding:6px 8px;
  text-align:center;background:var(--warning-bg);border-radius:var(--r-sm);
  margin-bottom:8px}

/* "+N more" beside a cell that is showing one record of several. Quiet, but
   never absent — a lone record displayed where others exist is a figure that
   disagrees with the totals elsewhere on the same page. */
.tk-more-count{font-size:var(--fs-2xs);color:var(--warning-text);font-weight:600;
  cursor:default;white-space:nowrap}

/* ── MAIL SEND POLICY ─────────────────────────────────────────────────────
   Stated above the mail controls, because the guard fails safe and that was
   invisible: a tester who sees nothing arrive cannot tell the guard from a
   broken feature, and the natural repair is to switch the mode to live.
   Live is the only state that gets the alarm colour. Everything else is safe,
   so everything else reads as reassurance rather than warning. */
.mail-pol{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;margin-bottom:12px;
  border:1px solid var(--border);border-radius:var(--r-md);font-size:var(--fs-xs)}
.mail-pol:empty{display:none}
.mail-pol-tag{flex:none;display:inline-flex;align-items:center;gap:5px;font-weight:700}
.mail-pol-txt{color:var(--text-2);line-height:1.5}
.mail-pol-test{background:var(--tone-blue-bg);border-color:var(--tone-blue-bd)}
.mail-pol-test .mail-pol-tag{color:var(--tone-blue-fg)}
.mail-pol-off{background:var(--surface-2)}
.mail-pol-off .mail-pol-tag{color:var(--text-2)}
.mail-pol-live{background:var(--danger-bg);border-color:var(--danger)}
.mail-pol-live .mail-pol-tag{color:var(--danger-text)}

/* ── SETTINGS SUB-NAVIGATION ──────────────────────────────────────────────
   Settings was one scroll of ten cards, so an admin changing a tax rate
   scrolled past four connectors and thirteen deadline rules to reach it. Four
   panes now, one visible at a time, chosen from a list on the left.

   🔴 The hidden panes are `display:none`, NOT removed. `saveSettings()` reads
   all 43 fields by id on every press, so a pane that is not in the DOM makes
   `$e('s-smtp-host')` null and takes the whole save down with it.

   The list sticks because the panes are long — Connections alone is six cards,
   and a sub-navigation you have to scroll back up to reach is a sub-navigation
   people stop using. */
.spn{display:flex;align-items:flex-start;gap:28px}
.spn-side{position:sticky;top:0;flex:0 0 216px;align-self:flex-start;padding-top:2px}
.spn-body{flex:1;min-width:0;padding-bottom:8px}
.spn-nav{display:flex;flex-direction:column;gap:2px}

/* 44px is the floor for anything a person commits through; this is navigation,
   and it takes two lines of text, so it sizes to its content above that floor
   rather than being pinned to a row height that would clip the hint. */
.spn-item{display:flex;align-items:flex-start;gap:10px;width:100%;min-height:var(--tap);
  padding:9px 11px;border:1px solid transparent;border-radius:var(--r-md);
  background:none;font-family:inherit;text-align:left;cursor:pointer;
  color:var(--text-2);transition:background .12s,color .12s,border-color .12s}
.spn-item:hover{background:var(--surface-2);color:var(--text)}
.spn-item.active{background:var(--accent-soft);border-color:var(--accent);color:var(--accent-ink)}
.spn-ic{flex:none;display:flex;align-items:center;height:18px;font-size:var(--fs-lg);opacity:.85}
.spn-item.active .spn-ic{opacity:1}
.spn-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.spn-label{font-size:var(--fs-md);font-weight:600;line-height:1.25}

/* The second line is a hint until something is wrong, and then it is the
   sentence saying what. One line, two jobs, because a tab that grows a third
   line when a backup fails moves everything under it. */
.spn-hint{font-size:var(--fs-2xs);line-height:1.35;color:var(--text-muted)}
.spn-item.active .spn-hint{color:var(--text-2)}

/* 🔴 The health card used to be the first thing on this page, and the reason
   was sound: whoever opens Settings on a machine that is not working needs it
   before anything else. Filing it behind a tab would have thrown that away, so
   the tab carries the alarm instead — visible from whichever pane is open. */
.spn-flagged .spn-hint,.spn-flagged.active .spn-hint{color:var(--danger-text)}
.spn-flag{flex:none;display:flex;align-items:center;height:18px;color:var(--danger-text)}

.spn-pane{display:none}
.spn-pane.active{display:block}

/* Sits at the bottom of the pane rather than the window: it is the end of a
   form, and a floating bar over a page this short would be furniture. */
.spn-save{display:flex;align-items:center;gap:12px;max-width:720px;
  padding-top:16px;border-top:1px solid var(--border)}
.spn-save-note{font-size:var(--fs-xs);color:var(--text-muted)}

/* Below the point where 216px of list plus a 720px card still fit, the list
   becomes a row above the panes rather than squeezing both. */
@media (max-width:1100px){
  .spn{flex-direction:column;gap:16px}
  .spn-side{position:static;flex:none;width:100%}
  .spn-nav{flex-direction:row;flex-wrap:wrap}
  .spn-item{width:auto;flex:1 1 200px}
}


/* ── Receiving: what has just been checked in ───────────────────────────────
   Sits between the scan box and the form. Deliberately quiet — it is a
   reference, not an alert, and the eye belongs on the empty serial field. */
.rc-done{
  border:1px solid var(--border); border-radius:var(--r-md);
  background:var(--surface-2); padding:10px 12px; margin-bottom:14px;
}
.rc-done-hd{
  display:flex; align-items:center; gap:6px;
  font-size:var(--fs-xs); color:var(--text-muted);
  text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px;
}
/* The row and its optional warranty action share a line. The row keeps all the
   flexible width so the aircraft name still truncates rather than shoving the
   button off the edge. */
.rc-done-line{ display:flex; align-items:center; gap:4px; }
.rc-done-line > .rc-done-row{ flex:1 1 auto; min-width:0; }
.rc-done-row{
  display:grid; grid-template-columns:auto 1fr auto; gap:10px; align-items:baseline;
  width:100%; text-align:left; padding:6px 8px; border:0; border-radius:var(--r-sm);
  background:transparent; color:inherit; cursor:pointer; font:inherit;
}
/* Deliberately quiet. Checking warranty is worth offering at check-in and is
   never the reason somebody came to this screen — the ticket number is. It sits
   at the muted weight of the clock beside it, not at the accent weight of the
   ticket id. */
.rc-done-dji{
  display:inline-flex; align-items:center; gap:4px; flex:0 0 auto;
  padding:5px 8px; border:1px solid var(--border); border-radius:var(--r-sm);
  background:transparent; color:var(--text-muted);
  font:inherit; font-size:var(--fs-xs); cursor:pointer; white-space:nowrap;
}
.rc-done-dji:hover{ color:var(--text); border-color:var(--accent); }
.rc-done-dji:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
.rc-done-row:hover{ background:var(--surface-3, var(--surface)); }
.rc-done-row:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
.rc-done-id{ font-family:ui-monospace,Menlo,monospace; font-weight:600; color:var(--accent); }
.rc-done-what{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The clock is the point of the row, but it must not shout — nothing here is
   late yet, and 48 hours is a long time. */
.rc-done-clock{ font-size:var(--fs-xs); color:var(--text-muted); white-space:nowrap; }


/* ── DJI device lookup, embedded ────────────────────────────────────────────
   DJI's own page inside the app. The frame is the tall element on purpose: the
   answer is over there, and the recorder underneath must not compete with it. */
.dji-wrap{ display:flex; flex-direction:column; gap:12px; }/* An answer months old is not evidence about today. Amber, not red — it is
   stale rather than wrong, and nothing is broken. */
.tk-warranty-stale{ color:var(--tone-amber-fg); }

/* ===== STAGING BANNER =====
   Shown on a practice copy, on every screen including sign-in, and it cannot be
   dismissed. Deliberately informational rather than alarming: this is the
   CORRECT state for a copy Roger or Cody is trying, it will be on screen for
   weeks, and a red alarm bar is a thing people stop seeing by day three.
   Amber reads as "note the context" rather than "something is broken". */
.deploy-banner{
  position:fixed; top:0; left:0; right:0; z-index:200;
  display:flex; align-items:center; justify-content:center; gap:6px;
  padding:7px 16px; text-align:center;
  background:var(--warning-bg); color:var(--warning-text);
  border-bottom:1px solid var(--border);
  font-size:var(--fs-sm); line-height:1.4;
}
.deploy-banner strong{ font-weight:700; }
.deploy-banner-note{ opacity:.75; }
/* Everything shifts down rather than sitting underneath it — a banner that
   covers the first row of a table is how people learn to ignore banners. */
body.has-deploy-banner{ padding-top:32px; }

/* ===== DATABASE PICKER (Admin Panel → DB Connection) =====
   Replaces "paste a connection string" as the primary action. The live database
   is marked in the danger colour and the practice copies in the muted one, so
   the row somebody must not casually open is the one that reads as heavier. */
.ws-row{
  display:flex; align-items:center; gap:12px;
  padding:10px 12px; border:1px solid var(--border); border-radius:var(--r-md);
  margin-bottom:8px; background:var(--surface);
}
.ws-row.ws-current{ border-color:var(--accent); background:var(--surface-2); }
.ws-main{ flex:1 1 auto; min-width:0; }
.ws-name{ font-weight:600; display:flex; align-items:center; gap:8px; }
.ws-facts{ font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px; }
.ws-badge{
  font-size:var(--fs-2xs); font-weight:700; letter-spacing:.06em;
  padding:2px 6px; border-radius:var(--r-sm); text-transform:uppercase;
}
.ws-live{ background:var(--danger-bg,var(--warning-bg)); color:var(--danger-text); }
.ws-practice{ background:var(--surface-3,var(--surface-2)); color:var(--text-muted); }
.ws-bad{ background:var(--surface-2); color:var(--text-muted); }
.ws-you{ font-size:var(--fs-2xs); color:var(--accent); font-weight:600; }
.ws-make{
  margin-top:16px; padding:14px; border:1px dashed var(--border);
  border-radius:var(--r-md); background:var(--surface-2);
}
.ws-make-hd{ font-size:var(--fs-sm); font-weight:600; margin-bottom:10px; }

/* ── A FIGURE INSIDE A SENTENCE ──────────────────────────────────
   Prose at reading size with the numbers carried by weight and colour rather
   than by a box. Born on the Command page and named `cmd-` for it; the
   inventory count result reached for the same idiom, which is what makes these
   shared rather than page-specific.

   ⚠️ They stay here even though the Command page no longer uses them. Deleting
   a class because the file that introduced it was rewritten is how a stylesheet
   quietly breaks a screen nobody was looking at — `05-inventory.js` emits every
   one of these. */
.cmd-say{margin:0;font-size:var(--fs-xl);line-height:1.5;color:var(--text-2);
  letter-spacing:-.01em;max-width:52ch;text-wrap:pretty}
.cmd-say-sm{font-size:var(--fs-lg);max-width:60ch}
/* Full text colour against recessed prose is the whole emphasis mechanism;
   tabular numerals so two amounts on adjacent lines line up. */
.cmd-n{color:var(--text);font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap}
.cmd-n-owed{color:var(--warning-text)}
.cmd-n-late{color:var(--danger-text)}
.cmd-note{margin:10px 0 0;font-size:var(--fs-sm);color:var(--text-muted);
  line-height:1.6;max-width:60ch}

/* ── COMMAND ───────────────────────────────────────────────────────────────
   🔴 An executive screen, and the whole point is that it does NOT look like
   the dashboard two rows above it in the sidebar.

   The dashboard is an operator board: dense, queue-shaped, oldest-first, read
   standing up with a drone in one hand. This is read sitting down, once a
   week, by three people who will disagree about what it says. So the two
   screens are deliberately opposite in every dimension a reader notices before
   they read a word:

     dashboard              here
     ───────────────────────────────────────────────────────
     many small tiles       three figures and a sentence
     counts                 counts WITH a comparison
     "what is stacked"      "is it better or worse than last time"
     no verdict             a verdict on every row
     tight rhythm           wide rhythm, ~64px between sections

   If a future change makes this look like a denser version of the dashboard,
   the change is wrong even if every number on it is right — two screens that
   look alike get read as one screen, and the one people stop opening is this
   one, because the other one is needed to do the job.

   🔴 Two measures on one page, deliberately. The prose holds a reading measure
   because that is what prose needs; the scorecard runs to the full column
   because a table is not prose. The asymmetry is the composition — a narrow
   statement over a wide instrument — rather than a layout that failed to fill
   its container. */
.ex-page{max-width:1120px;padding:40px 32px 72px}
.ex-sec{margin-top:64px}
.ex-h{font-size:var(--fs-xs);font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--text-muted);margin:0 0 6px}

/* The title is a question, not a noun. "Command Center" tells a reader what
   the software calls the screen; a question tells them what it is for, and it
   is the only line on the page that never changes with the data. */
.ex-q{font-size:var(--fs-3xl);font-weight:700;letter-spacing:-.02em;
  line-height:1.2;color:var(--text);margin:0;max-width:24ch}

/* ── The two controls, and there are exactly two ─────────────────────────────
   A period and a comparison basis. Anything else on this row is a setting
   somebody has to understand before the page says anything, and a screen you
   configure before it speaks is a screen nobody opens twice. */
.ex-ctl{display:flex;flex-wrap:wrap;gap:8px 40px;margin-top:20px;align-items:baseline}
.ex-ctl-g{display:flex;align-items:baseline;gap:10px}
.ex-ctl-l{font-size:var(--fs-2xs);font-weight:700;letter-spacing:.1em;
  text-transform:uppercase;color:var(--text-muted)}
.ex-b{border:0;background:none;padding:3px 0;font:inherit;font-size:var(--fs-sm);
  font-weight:500;color:var(--text-muted);cursor:pointer;
  border-bottom:2px solid transparent}
.ex-b:hover{color:var(--text-2)}
.ex-b.on{color:var(--accent);font-weight:700;border-bottom-color:var(--accent)}
.ex-b:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
.ex-sep{color:var(--border);font-size:var(--fs-sm)}

/* Two clocks, not one. "As of now" is the wrong answer when the newest payment
   in the ledger is six weeks old, and the gap between the two is the single
   fact that stops somebody reading an empty money figure as a bad month. */
.ex-asof{margin:18px 0 0;font-size:var(--fs-xs);color:var(--text-muted);
  line-height:1.6;max-width:78ch}
.ex-asof b{color:var(--text-2);font-weight:600}

/* WHICH POPULATION. Stated on the screen, in one line, above everything it
   governs — because a reader looking at a repair figure and an equipment
   figure WILL add them together unless told why not to, and the sum they reach
   is a number about equipment wearing a repair shop's label. */
.ex-pop{margin:14px 0 0;padding:12px 16px;border-radius:var(--r-md);
  background:var(--surface-sunken);font-size:var(--fs-sm);color:var(--text-2);
  line-height:1.6;max-width:88ch}
.ex-pop b{color:var(--text);font-weight:600}

/* ── Tier 1: the sentence, then three figures ────────────────────────────────
   The prose comes FIRST and the figures repeat it. That order is deliberate: a
   reader who stops after the first paragraph has the finding, and a reader who
   only scans the numbers has the same finding in a different form. Neither is
   the "real" version. */
.ex-lede{margin:0;font-size:var(--fs-xl);line-height:1.6;color:var(--text-2);
  max-width:72ch}
.ex-lede+.ex-lede{margin-top:10px}
.ex-n{color:var(--text);font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap}
.ex-n-bad{color:var(--danger-text)}
.ex-n-warn{color:var(--warning-text)}
.ex-n-good{color:var(--success-text)}

.ex-heads{display:grid;grid-template-columns:repeat(3,1fr);gap:32px 48px;
  margin-top:36px;padding-top:28px;border-top:1px solid var(--border)}
@media (max-width:900px){.ex-heads{grid-template-columns:1fr;gap:28px}}
.ex-m-l{display:block;font-size:var(--fs-xs);font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--text-muted)}
/* Tabular figures so three numbers of different widths still line up down the
   row, and tight tracking so a six-digit money figure does not read as wider
   than a two-digit day count. */
.ex-m-v{display:block;margin-top:10px;font-size:var(--fs-3xl);font-weight:700;
  letter-spacing:-.02em;color:var(--text);font-variant-numeric:tabular-nums;
  line-height:1.1}
.ex-m-u{font-size:var(--fs-lg);font-weight:600;color:var(--text-2);margin-left:5px;
  letter-spacing:0}
/* The empty case is a sentence at the same weight as the figure it replaces,
   never a zero and never a blank. A zero here is a claim about the period; the
   sentence is a claim about the record, and only the second one is true. */
.ex-m-none{display:block;margin-top:10px;font-size:var(--fs-lg);font-weight:600;
  color:var(--text-2);line-height:1.35;max-width:26ch}
.ex-m-d{display:block;margin-top:8px;font-size:var(--fs-sm);font-weight:600;
  font-variant-numeric:tabular-nums;color:var(--text-muted)}
.ex-m-d-bad{color:var(--danger-text)}
.ex-m-d-good{color:var(--success-text)}
/* The qualifier under the label, not a tooltip on it. What a number counts is
   part of the number. */
.ex-m-q{display:block;margin-top:8px;font-size:var(--fs-xs);color:var(--text-muted);
  line-height:1.6;max-width:34ch}

/* ── Tier 1.5: the one chart ─────────────────────────────────────────────────
   Hand-drawn SVG rather than the vendored Chart.js. A twelve-point line with a
   ghost behind it needs no axis engine, no tooltip layer and no 200KB, and
   every colour on it resolves through a token so it follows the theme — which
   a canvas chart cannot do without being redrawn. */
.ex-chart{margin-top:18px}
.ex-chart svg{display:block;width:100%;height:auto}
.ex-grid{stroke:var(--border);stroke-width:1}
.ex-gridtext{fill:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
.ex-axis{fill:var(--text-muted);font-size:11px}
.ex-axis-y{fill:var(--text-2);font-size:11px;font-weight:600}
/* The ghost is the SAME line one year earlier. Three channels separate it from
   the live series — dash, weight and hue — because a pale line and a bright
   line are ONE channel, and roughly one in sixteen readers is being asked to
   separate them by lightness alone.

   ⚠️ It carries no `opacity`. An earlier draft set .55, which measured 2.4:1
   against the light ground — under the 3:1 floor for a graphic, on the series
   the whole chart exists to compare against. Full-strength muted grey against a
   2.5px accent line is already unmistakably the secondary one. */
.ex-ghost{fill:none;stroke:var(--text-muted);stroke-width:1.5;stroke-dasharray:4 4}
.ex-line{fill:none;stroke:var(--accent);stroke-width:2.5;stroke-linejoin:round;
  stroke-linecap:round}
.ex-dot{fill:var(--accent)}
/* The selected window. The tint alone is nearly invisible on the light ground —
   --accent-soft is two steps from --bg there — so the region is marked by an
   edge as well as a fill, and the edge is what actually reads. */
.ex-band{fill:var(--accent-soft)}
.ex-bandedge{stroke:var(--accent);stroke-width:1.5;stroke-dasharray:3 3}
.ex-gap{fill:var(--text-muted);font-size:var(--fs-2xs)}
.ex-key{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:10px;
  font-size:var(--fs-xs);color:var(--text-muted)}
.ex-key i{display:inline-block;width:18px;height:0;vertical-align:middle;
  margin-right:7px;border-top:2px solid var(--accent)}
.ex-key .ex-key-g i{border-top:2px dashed var(--text-muted)}
/* The band's swatch needs an outline, not a fill. --accent-soft against --bg is
   two steps apart on the light ground, so a filled 18px chip reads as blank
   paper — the same reason the band on the chart itself gained an edge. */
.ex-key .ex-key-w i{height:12px;border:1px dashed var(--accent);
  background:var(--accent-soft)}

/* ── Tier 2: the scorecard ───────────────────────────────────────────────────
   A table, because this is genuinely tabular: the same five questions asked of
   seventeen different parts of the shop. Grouped by stage so a reader can stop
   at the group they own, and failing rows rise inside each group so the group
   heading is never hiding its worst row three lines down.

   🔴 Sorted inside groups, never across them. A global sort buries a stage
   whose every row is fine — and "nothing is wrong at intake" is a thing the
   reader needs to see stated, not infer from an absence. */
.ex-tbl{width:100%;border-collapse:collapse;margin-top:14px;font-size:var(--fs-sm)}
.ex-tbl th{text-align:left;padding:0 14px 10px 0;font-size:var(--fs-2xs);
  font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--text-muted);border-bottom:1px solid var(--border);white-space:nowrap}
.ex-tbl th.ex-r,.ex-tbl td.ex-r{text-align:right;padding-right:0;padding-left:14px}
.ex-tbl td{padding:14px 14px 14px 0;vertical-align:top;
  border-bottom:1px solid var(--border)}
.ex-grp td{padding:30px 0 8px;border-bottom:0;font-size:var(--fs-xs);font-weight:700;
  letter-spacing:.12em;text-transform:uppercase;color:var(--text-2)}
.ex-grp-n{font-weight:500;letter-spacing:0;text-transform:none;
  color:var(--text-muted);margin-left:12px}
.ex-metric{display:block;font-size:var(--fs-md);font-weight:600;color:var(--text)}
.ex-qual{display:block;margin-top:4px;font-size:var(--fs-xs);color:var(--text-muted);
  line-height:1.6;max-width:52ch}
.ex-val{font-size:var(--fs-lg);font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap;color:var(--text)}
.ex-of{font-size:var(--fs-xs);font-weight:500;color:var(--text-muted);
  font-variant-numeric:tabular-nums}
.ex-tgt{font-size:var(--fs-xs);color:var(--text-2);line-height:1.5;max-width:22ch;
  display:block}
.ex-tgt-s{display:block;margin-top:3px;color:var(--text-muted)}
/* Colour PLUS words. The verdict is legible with the colour removed, which is
   the requirement — roughly one in sixteen men cannot separate this palette's
   red from its amber, and a verdict column carried by hue alone is a verdict
   column those readers do not have. */
.ex-v{font-size:var(--fs-sm);font-weight:700;white-space:nowrap}
.ex-v-off{color:var(--danger-text)}
.ex-v-watch{color:var(--warning-text)}
.ex-v-ok{color:var(--success-text)}
.ex-v-none{color:var(--text-muted);font-weight:600}
.ex-chg{font-size:var(--fs-sm);font-weight:600;font-variant-numeric:tabular-nums;
  white-space:nowrap;color:var(--text-2)}
.ex-chg-bad{color:var(--danger-text)}
.ex-chg-good{color:var(--success-text)}
/* A standing row has no prior-period value and cannot be given one — see the
   note under the table. The dash is the true answer, and the word beside it
   stops it reading as a rendering failure. */
.ex-chg-none{color:var(--text-muted);font-weight:500;font-size:var(--fs-xs)}
.ex-open{border:0;background:none;padding:0;font:inherit;font-size:var(--fs-xs);
  color:var(--accent);cursor:pointer;margin-top:6px;display:inline-block}
.ex-open:hover{text-decoration:underline}
.ex-open:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
@media (max-width:900px){
  .ex-tbl th.ex-hide,.ex-tbl td.ex-hide{display:none}
  .ex-qual{max-width:none}
}

.ex-note{margin:16px 0 0;font-size:var(--fs-xs);color:var(--text-muted);
  line-height:1.7;max-width:82ch}
.ex-note b{color:var(--text-2);font-weight:600}
.ex-empty{margin:12px 0 0;font-size:var(--fs-md);color:var(--text-2);
  line-height:1.6;max-width:62ch}
.ex-src{margin:56px 0 0;padding-top:16px;border-top:1px solid var(--border);
  font-size:var(--fs-xs);color:var(--text-muted);line-height:1.7;max-width:82ch}

/* ── INVENTORY ─────────────────────────────────────────────────────────────
   🔴 Stock state is a filter and a marker, never a row wash.

   This table used to paint every out-of-stock row with a danger background.
   2,073 of 3,157 parts sit at zero, so two thirds of the screen was red — the
   colour marked the ordinary case, which is the same as marking nothing, and
   it made the row a technician actually wants (a part they can use) the
   hardest one to pick out. */
.inv-chips{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
.inv-chip{display:inline-flex;align-items:baseline;gap:5px;padding:6px 12px;
  border:1px solid var(--border);border-radius:var(--r-full);background:var(--surface);
  font:inherit;font-size:var(--fs-xs);color:var(--text-2);cursor:pointer;
  min-height:var(--tap-xs);transition:border-color .12s,background .12s}
.inv-chip b{font-weight:700;color:var(--text);font-variant-numeric:tabular-nums}
.inv-chip:hover{background:var(--surface-2)}
.inv-chip:focus-visible{outline:2px solid var(--focus);outline-offset:1px}
.inv-chip.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.inv-chip.on b{color:var(--accent)}
/* The tone is on the COUNT, not on the chip's whole surface — these are four
   facts about one catalogue, not four alert levels. */
.inv-chip-ok b{color:var(--success-text)}
.inv-chip-warn b{color:var(--warning-text)}
.inv-chip-bad b{color:var(--danger-text)}
.inv-chip.on.inv-chip-ok b,.inv-chip.on.inv-chip-warn b,.inv-chip.on.inv-chip-bad b{color:var(--accent)}

.inv-tbl td{vertical-align:top}
/* Part names arrive from the supplier catalogue as unbroken strings —
   "640-Acetate Cloth Tape/Model_C1/0.06*8mm*20m" — with no space to wrap at.
   Without this they push the cell wider than the column it was given. */
.inv-tbl td{overflow-wrap:anywhere}
/* Truncate rather than wrap, and never at a price. The full value is on the
   cell's title and in the edit form. */
.inv-tbl td.mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.inv-tbl .badge{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.inv-name{font-weight:500;line-height:1.35;overflow-wrap:anywhere}
/* What it fits, under what it is. Beside the name it competed for the width
   that pushed the quantity column off the screen. */
.inv-sub{font-size:var(--fs-2xs);color:var(--text-muted);margin-top:2px;
  overflow-wrap:anywhere}
.inv-loc{margin-left:8px;padding:0 6px;border-radius:var(--r-sm);
  background:var(--surface-2);color:var(--text-2)}

/* The number, and its state, in one cell. */
.inv-qty{display:flex;align-items:center;gap:6px}
.inv-qty .qty-in{width:60px}
.inv-tag{font-size:var(--fs-2xs);font-weight:700;padding:1px 6px;
  border-radius:var(--r-full);letter-spacing:.02em}
.inv-tag-out{background:var(--tone-red-bg);color:var(--tone-red-fg)}
.inv-tag-low{background:var(--tone-amber-bg);color:var(--tone-amber-fg)}

.empty-sm{padding:22px 16px;text-align:center;color:var(--text-muted);
  font-size:var(--fs-sm)}

/* ── PURCHASING ────────────────────────────────────────────────────────────
   The same correction the parts table needed, for the same reason: the
   finished case dominated the list and the work nobody had done was invisible
   inside it. 222 of 297 orders sit at "received" and never close, so a
   date-ordered list of everything buried the twelve a supplier still owes.

   The chips reuse .inv-chip rather than growing a second vocabulary — one
   idiom for "counted groups you can filter by", learned once. */
.po-bar{display:flex;gap:8px;flex-wrap:wrap;padding:12px 14px;
  border-bottom:1px solid var(--border)}
.po-note{margin:0;padding:10px 14px;font-size:var(--fs-xs);color:var(--text-2);
  line-height:1.55;border-bottom:1px solid var(--border)}
/* The rule spans the panel; the sentence stops at a readable measure. Putting
   max-width on the paragraph itself cut the border off mid-panel and read as a
   layout fault rather than as a line length. */
.po-note>span{display:block;max-width:78ch}
.po-note b{color:var(--text);font-variant-numeric:tabular-nums}
.po-note-late b{color:var(--warning-text)}
/* Lateness sits on the date it is about. A whole-row treatment would say
   something is wrong with the order, and nothing is — a supplier is late. */
.po-late{color:var(--warning-text);font-variant-numeric:tabular-nums}
.po-late-tag{margin-left:7px;padding:1px 6px;border-radius:var(--r-full);
  background:var(--tone-amber-bg);color:var(--tone-amber-fg);
  font-size:var(--fs-2xs);font-weight:700}

/* ── DASHBOARD, DE-BOXED ───────────────────────────────────────────────────
   🔴 Scoped to #page-dashboard on purpose. `.card`, `.sc` and `.q-card` are
   used on fourteen screens; this changes how they read on ONE, the landing
   page, without touching the rest of the app the night before it is shown.

   What was wrong: six bordered, shadowed panels stacked down the page, with the
   three headline counts as bordered tiles inside the first one. Every element
   had a box, so no box meant anything, and the page read as a dashboard
   template rather than as this shop's morning. The information was right — the
   three queues are the correct three — but the container did all the talking.

   What replaces it: rules and space. The counts become measurements sitting on
   the page rather than tiles sitting in slots; the panels become sections under
   a hairline. Same layout, same click targets, same scanning order — a
   technician standing at the bench reads it exactly as before, which is why
   this is a refinement and not a redesign.

   The register stays different from Command deliberately. That page is a
   briefing to be read; this is a board to be scanned, and turning the bench's
   screen into prose would be the same mistake as turning the manager's into
   tiles. */
/* 🔴 A grid, not a wrapping flex row.
   The de-boxed version first kept `.q-grid`'s flex-wrap and separated the four
   measurements with a left rule on every item after the first. Wrapped into two
   rows of two, that put a stray vertical rule at the START of the second row —
   a divider dividing nothing, which is worse than no divider at all.
   Two explicit columns, space between them, and a rule only between the rows. */
#page-dashboard .q-grid{display:grid;grid-template-columns:1fr 1fr;
  column-gap:44px;margin-bottom:30px;
  border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
#page-dashboard .q-card{border:0;border-radius:0;background:none;
  padding:18px 0;min-width:0}
#page-dashboard .q-grid>*:nth-child(n+3){border-top:1px solid var(--border)}
#page-dashboard .q-card:hover{background:none}
/* Hover moves the figure rather than the ground. With no box to tint, a
   background change would paint a rectangle that is not otherwise there. */
#page-dashboard .q-card:hover .q-n{color:var(--accent)}
@media (max-width:900px){
  #page-dashboard .q-grid{grid-template-columns:1fr}
  #page-dashboard .q-grid>*+*{border-top:1px solid var(--border)}
}
/* The figure carries the emphasis now that the box does not. */
#page-dashboard .q-n{font-size:var(--fs-3xl)}

#page-dashboard .sc,
#page-dashboard .card{background:none;border:0;box-shadow:none;border-radius:0;
  padding-left:0;padding-right:0}
#page-dashboard .sc{margin-bottom:30px}
/* A section is named by its heading and separated by a rule, which is all a
   heading needs. */
#page-dashboard .sc-hdr{border-bottom:1px solid var(--border);padding-left:0;padding-right:0}
#page-dashboard .sc-title{font-size:var(--fs-md);font-weight:600;letter-spacing:0;text-transform:none;color:var(--text)}
/* Tables sit flush to the column, so the first cell aligns with the heading
   above it rather than being indented by a container that is no longer there. */
#page-dashboard .sc table td:first-child,
#page-dashboard .sc table th:first-child{padding-left:0}
#page-dashboard .sc table td:last-child,
#page-dashboard .sc table th:last-child{padding-right:0}

/* ── STOCK TAKE ────────────────────────────────────────────────────────────
   The count sheet is the parts table, not a separate screen. Somebody walking
   a shelf has already narrowed to what they are looking at — a category, a
   search, the low-stock tab — and moving them to a different list would ask
   them to reproduce that narrowing in a second place. */
.st-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:10px;
  padding:12px 14px;border:1px solid var(--accent);border-radius:var(--r-md);
  background:var(--accent-soft)}
.st-bar-say{font-size:var(--fs-sm);color:var(--text-2)}
.st-bar-say b{color:var(--accent);font-weight:700;font-variant-numeric:tabular-nums}
.st-note{flex:1;min-width:200px;margin-left:auto}
.st-scope{margin:0 0 14px;font-size:var(--fs-xs);color:var(--text-muted);
  line-height:1.55;max-width:74ch}

/* The variance sits UNDER the box, not beside it. Beside it, the pair needed a
   column wide enough for both and pushed past the table's right edge — and the
   thing that got clipped was the variance, which is the entire reason to run a
   count. */
.st-cell{display:block}
.st-in{width:84px;padding:5px 8px;font-variant-numeric:tabular-nums}
.st-cell .st-var{display:block;margin-top:3px;padding-left:2px}
/* The variance, beside the number that produced it. Empty until something is
   typed, so an untouched row stays quiet rather than claiming a zero. */
.st-var{font-size:var(--fs-2xs);font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap}
.st-var-ok{color:var(--text-muted);font-weight:500}
.st-var-diff{color:var(--warning-text)}

/* ── Estimates ────────────────────────────────────────────────────────────
   The scope line is the page's one money figure, and it is deliberately
   welded to the filter above it: it sums the rows that are actually on
   screen and then names the population it just summed. The shop's rule
   about money is to ask what produced a sum before quoting it — here the
   answer is printed beside the number, so it cannot be quoted without it.

   Chips reuse .inv-chip. One idiom for "counted groups you can filter by",
   learned once, as in Purchasing. */
.est-scope{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  padding:0 0 14px;margin-bottom:2px;border-bottom:1px solid var(--border)}
.est-scope-money{font-size:var(--fs-2xl);font-weight:700;color:var(--text);
  font-variant-numeric:tabular-nums;letter-spacing:-0.01em}
.est-scope-say{font-size:var(--fs-sm);color:var(--text-2)}
.est-scope-say b{color:var(--text);font-weight:600;font-variant-numeric:tabular-nums}
/* Wide enough for its own placeholder. At 240px the hint read "Estimate #,
   customer, ticket or not" — a truncation that looks like a typo rather than
   like a narrow box, and one only a screenshot catches. */
.est-search{margin-left:auto;min-width:300px;padding:6px 10px;font-size:var(--fs-sm);
  border:1px solid var(--border);border-radius:var(--r-sm);
  background:var(--surface);color:var(--text)}
.est-search:focus-visible{outline:2px solid var(--focus);outline-offset:1px}

/* Right-aligned and tabular so the column reads as money down the page. */
.est-num{text-align:right;font-variant-numeric:tabular-nums}

.est-ver{font-size:var(--fs-2xs);color:var(--text-muted)}

/* "no invoice" states a fact about a record. Estimate, then work, then
   invoice is the intended sequence, so this is a marker, not a warning —
   which is why it is neutral rather than red. */
.est-flag{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:var(--r-full);
  font-size:var(--fs-2xs);font-weight:600;letter-spacing:.02em;
  background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}

/* Shown instead of an empty item table — see the comment in 12-estimates.js. */
.est-nolines{padding:14px 16px;border:1px dashed var(--border);border-radius:var(--r-md);
  font-size:var(--fs-sm);color:var(--text-2);line-height:1.6;max-width:78ch}
.est-nolines b{color:var(--text);font-weight:600}

/* Same statement as .est-nolines, inside a table cell rather than beside one. */
.inv-nolines{padding:14px 16px;font-size:var(--fs-sm);color:var(--text-2);
  line-height:1.6;text-align:left}
.inv-nolines b{color:var(--text);font-weight:600}

/* A record with no name says so, rather than printing the import's placeholder
   as though it were one. See custName() in 01-core.js. */
.name-absent{color:var(--text-muted);font-weight:400;font-style:italic}

/* ── One connector, opened ─────────────────────────────────────────────────
   The panel says three things in order: what this connection IS, what was
   last actually observed, and what can be done about it. A status with no
   route to a fresh answer is the thing this whole panel exists to stop. */
.cx-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:4px}
.cx-kind{font-size:var(--fs-2xs);color:var(--text-muted);text-transform:uppercase;
  letter-spacing:.06em;font-weight:600}
.cx-state{font-size:var(--fs-sm);font-weight:600}
.cx-state.on{color:var(--success-text)}
.cx-state.bad{color:var(--danger-text)}
.cx-state.idle{color:var(--text-2)}
.cx-say{font-size:var(--fs-sm);color:var(--text-2);line-height:1.6;max-width:70ch;margin:10px 0 0}
.cx-facts{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;margin:16px 0 0;
  font-size:var(--fs-sm)}
.cx-facts dt{color:var(--text-muted)}
.cx-facts dd{margin:0;color:var(--text);font-variant-numeric:tabular-nums}
/* The probe's own answer, kept apart from the stored status so nobody has to
   guess which of the two they are reading. */
.cx-probe{margin:16px 0 0;padding:12px 14px;border-radius:var(--r-md);
  background:var(--surface-2);font-size:var(--fs-sm);line-height:1.6;max-width:70ch}
.cx-probe.on{border:1px solid color-mix(in srgb, var(--success-text) 30%, transparent)}
.cx-probe.bad{border:1px solid color-mix(in srgb, var(--danger) 30%, transparent)}

/* The connector's own setup, inside its panel. Separated from the status above
   by a rule rather than a card, because it is the same subject continued —
   what this connection is, then what it needs to work. */
.cx-setup{margin-top:20px;padding-top:16px;border-top:1px solid var(--border)}
.cx-setup-h{margin:0 0 10px;font-size:var(--fs-xs);font-weight:700;letter-spacing:.1em;
  text-transform:uppercase;color:var(--text-muted)}

/* ShipStation's carriers, inside its panel. The postage column is the reason
   this table exists — it is what decides whether a label can be bought. */
.cx-carriers{width:100%;border-collapse:collapse;font-size:var(--fs-sm);margin-top:14px}
.cx-carriers th{text-align:left;padding:4px 8px;font-size:var(--fs-xs);
  color:var(--text-muted);font-weight:500;border-bottom:1px solid var(--border)}
.cx-carriers td{padding:6px 8px;border-bottom:1px solid var(--rule,var(--border))}
.cx-carriers tr:last-child td{border-bottom:0}

/* The portal's real database reach, read from information_schema rather than
   described in prose. Amber marks a table it can write, because "can read your
   invoices" and "can change your estimates" are different sentences. */
.pt-grants{margin:14px 0 4px}
.pt-grant-list{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
.pt-grant{font-family:var(--ff-mono);font-size:var(--fs-2xs);padding:3px 8px;
  border-radius:var(--r-full);background:var(--surface-2);color:var(--text-2);
  border:1px solid var(--border)}
.pt-grant-w{background:var(--tone-amber-bg);color:var(--tone-amber-fg);
  border-color:var(--tone-amber-bd)}
.pt-note{font-size:var(--fs-xs);color:var(--text-muted);line-height:1.55;max-width:70ch}

/* ── Fleet portal accounts, on the Employees page ──────────────────────────
   Same question as Staff — who can sign in — asked about the other
   population, so it shares that page rather than taking a sidebar row the
   height budget does not have. */
.emp-tabs{display:flex;gap:4px;margin-bottom:18px;border-bottom:1px solid var(--border)}
.fleet-lede{font-size:var(--fs-sm);color:var(--text-2);line-height:1.65;
  max-width:74ch;margin-bottom:18px}
.fleet-h{font-size:var(--fs-2xs);font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-muted);margin:20px 0 8px}
.fleet-row{display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:11px 2px;border-bottom:1px solid var(--border)}
.fleet-row:last-child{border-bottom:0}
.fleet-meta{display:block;font-size:var(--fs-xs);color:var(--text-muted);margin-top:2px}
/* Not a fault: a login with nothing linked works and shows an empty list,
   which is the correct default. Muted, never red. */
.fleet-none{color:var(--text-muted);font-style:italic;font-size:var(--fs-sm)}
.fleet-search{margin-bottom:4px}
.fleet-found{border-top:1px solid var(--border);margin-top:10px}

/* ---- Money · Today. Three facts, then two lists the desk works down. ---- */
.mt-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px}
.mt-sub{font-size:var(--fs-xs);color:var(--text-muted);margin-top:6px;line-height:1.45}
.mt-row{display:grid;grid-template-columns:minmax(96px,auto) 1fr auto;gap:14px;align-items:center;padding:10px 6px;border-top:1px solid var(--border);cursor:pointer;font-size:var(--fs-sm);border-radius:var(--r-sm)}
.mt-row:hover{background:var(--surface-2)}
.mt-row .mt-amt{font-variant-numeric:tabular-nums;font-weight:600;text-align:right;white-space:nowrap}
.mt-row .mt-meta{font-size:var(--fs-xs);color:var(--text-muted);margin-top:2px}
.mt-row .mt-inv{color:var(--primary);cursor:pointer}
.mt-row .mt-inv:hover{text-decoration:underline}
.mt-note{font-size:var(--fs-xs);color:var(--text-muted);line-height:1.55;margin-top:18px;max-width:72ch}

/* The counter strip on a repair that is Ready for Pickup: its buttons sit at the end. */
.tk-next-act{margin-left:auto;display:flex;gap:8px;flex-shrink:0}
.tk-next-ok{border-color:var(--tone-green-bd);background:var(--tone-green-bg)}
.tk-next-ok .tk-next-t,.tk-next-ok .tk-next-s{color:var(--tone-green-fg)}
.tk-take{margin-left:8px;vertical-align:middle}

/* Two readings of one list (Customers: People | Companies). */
.view-chips{display:flex;gap:4px;margin-right:12px;flex-shrink:0}

/* A fold is already a card. What it holds is content, not another card:
   the double frame ("cards inside cards") was one of the tells the 2026-09-04
   direction named. The body keeps its own markup and loses the second box. */
.tk-fold-b > .card{background:transparent;border:0;box-shadow:none;padding:0 0 4px;margin-top:0}

/* Command: where repairs came from. A quiet bar list; the unasked bucket recedes. */
.ex-bars{display:flex;flex-direction:column;gap:8px;max-width:640px;margin-top:8px}
.ex-bar-row{display:grid;grid-template-columns:180px 1fr 90px;gap:12px;align-items:center;font-size:var(--fs-sm)}
.ex-bar-t{height:8px;border-radius:var(--r-full);background:var(--surface-2);overflow:hidden}
.ex-bar-t i{display:block;height:100%;background:var(--accent-fill);border-radius:var(--r-full)}
.ex-bar-muted .ex-bar-t i{background:var(--text-muted);opacity:.45}
.ex-bar-muted .ex-bar-l{color:var(--text-muted)}
.ex-bar-n{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
.ex-bar-n small{color:var(--text-muted);font-weight:500;margin-left:4px}


/* ═════════════════════════════════════════════════════════════════════════
   SECOND PASS — the world of the 4 September mockup, 2026-09-06.

   Framed cards with a soft lift, a sidebar on the page's own surface, ids
   and ages in the mono face, a status as a dot and a word instead of a pill,
   and counts as small pills beside a heading. One layer over the component
   rules above, so the anatomy of every screen stays exactly where it is and
   only its paint changes. Where a rule here repeats a selector from above,
   this one wins by position, on purpose.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── Sidebar: a column of the page, not a panel beside it ─────────────── */
#sidebar{border-right:1px solid var(--sidebar-divider)}
.sb-brand{border-bottom:0;padding:18px 16px 12px}
.sb-nav{padding:2px 10px 8px}
.nav-item{border-left:0;border-radius:var(--r-md);padding:8px 10px;min-height:38px;margin:1px 0;
  color:var(--sidebar-text);font-size:var(--fs-sm)}
.nav-item:hover{background:var(--sidebar-hover);color:var(--sidebar-text-hover)}
.nav-item.active{background:var(--sidebar-active);color:var(--sidebar-active-text);font-weight:600}
.nav-item svg{opacity:.7}
.nav-item.active svg{opacity:1;color:var(--accent)}
.nav-item:focus-visible{outline-offset:-2px}
.sb-footer{border-top:1px solid var(--sidebar-divider)}

/* ── Header: on the surface, separated by a rule alone ────────────────── */
#hdr{box-shadow:none}

/* ── Cards: one radius, one lift ──────────────────────────────────────── */
.card{border-radius:var(--r-lg);box-shadow:var(--shadow-md)}
.sc-title{font-size:var(--fs-md);font-weight:600}
/* A count beside a heading: mono, blue on soft blue, never a second heading. */
.cnt{display:inline-block;font-family:var(--ff-mono);font-weight:500;font-size:var(--fs-xs);
  color:var(--accent);background:var(--accent-soft);padding:1px 7px;border-radius:var(--r-md);
  margin-left:8px;vertical-align:1px;font-variant-numeric:tabular-nums;line-height:1.5}
/* The quiet qualifier at the right of a card heading. */
.more{margin-left:auto;font-size:var(--fs-xs);font-weight:500;color:var(--text-muted)}

/* ── Statuses: a dot and a word ───────────────────────────────────────────
   The pill was a box competing with every button on the row. The dot carries
   the role — blue for ours, hollow for moving, amber for theirs, green for
   settled, red for broken — and the word stays in the running text colour,
   so a column of statuses reads as a column of words with a lamp each. */
.badge.role-ours,.badge.role-moving,.badge.role-theirs,.badge.role-settled,.badge.role-broken{
  background:none;box-shadow:none;padding:0;border-radius:0;gap:7px;font-weight:500;
  color:var(--text-2);font-size:var(--fs-xs)}
.badge.role-ours::before,.badge.role-moving::before,.badge.role-theirs::before,
.badge.role-settled::before,.badge.role-broken::before{
  content:"";width:8px;height:8px;border-radius:50%;flex:none;box-sizing:border-box}
.badge.role-ours::before{background:var(--accent-fill);box-shadow:0 0 0 3px var(--accent-soft)}
.badge.role-moving::before{border:1.5px solid var(--text-muted)}
.badge.role-theirs::before{background:var(--warning);box-shadow:0 0 0 3px var(--warning-bg)}
.badge.role-settled{color:var(--success-text)}
.badge.role-settled::before{background:var(--success);box-shadow:0 0 0 3px var(--success-bg)}
.badge.role-broken{color:var(--danger-text);font-weight:600}
.badge.role-broken::before{background:var(--danger);box-shadow:0 0 0 3px var(--danger-bg)}

/* ── Rows: the aircraft first, the name in weight, the id and age in mono ── */
.thumb{width:36px;height:36px;border-radius:var(--r-md);background:var(--surface-2);display:inline-flex;
  align-items:center;justify-content:center;color:var(--text-2);flex:none}
.thumb svg{width:22px;height:22px}
.q-row-id{font-family:var(--ff-mono);font-weight:500;color:var(--text-muted);font-size:var(--fs-xs);
  flex:0 0 auto;min-width:84px;white-space:nowrap}
/* The side column is 360px: the aircraft thumb gives way to the reference. */
.home-side .thumb{display:none}
.home-side .q-row-id{min-width:0}
.q-row-cust{font-weight:600}
.q-row-cust .td-muted{font-weight:400}
.q-row-idle,.q-row-idle-lead,.sbar-n,.q-of{font-family:var(--ff-mono);font-weight:500}
.q-row,.q-row-act{gap:12px}
.q-row:hover .q-row-id,.q-row-act:hover .q-row-id{color:var(--accent)}

/* ── Home, framed again ───────────────────────────────────────────────────
   The flat Home of 4 September was the right anatomy on the wrong paint: four
   figures separated by rules read as a spreadsheet. Cards with a lift, one
   per figure, are what the mockup drew and what a manager glances at. */
#page-dashboard .card{background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow-md);
  border-radius:var(--r-lg);padding:0 16px 6px}
#page-dashboard .sc-hdr{padding:14px 0 10px;border-bottom:0}
#page-dashboard .q-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;
  border:0;margin-bottom:20px}
#page-dashboard .q-grid>*:nth-child(n+3){border-top:1px solid var(--border)}
#page-dashboard .q-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);
  box-shadow:var(--shadow-md);padding:14px 16px 12px;min-width:0}
#page-dashboard .q-card:hover{background:var(--surface-2)}
#page-dashboard .q-n{font-size:var(--fs-3xl);font-weight:700}
#page-dashboard .q-l{font-size:var(--fs-xs);color:var(--text-2)}
@media (max-width:1180px){#page-dashboard .q-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:760px){#page-dashboard .q-grid{grid-template-columns:1fr}}
/* The bench's Home: what is mine on the left, what arrived on the right. */
.home-grid{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:20px;align-items:start}
.home-grid .card{margin-bottom:16px}
@media (max-width:1100px){.home-grid{grid-template-columns:1fr}}
.home-hello h2{font-size:var(--fs-2xl);font-weight:700;letter-spacing:-.02em}
.home-hello p{color:var(--text-2)}
.sbar-t{height:8px}

/* ── Labels speak in sentences, not in small capitals ─────────────────────
   The mockup drops the eyebrow: a section is named by a heading at text
   size, and a table column by a plain word. Tracking and uppercase were
   doing the work that weight and position do better. */
.stitle{font-size:var(--fs-sm);font-weight:600;text-transform:none;letter-spacing:0;color:var(--text)}
.ex-h,.ex-ctl-l,.ex-m-l,.ex-grp td{text-transform:none;letter-spacing:0;font-weight:600;font-size:var(--fs-sm)}
.ex-ctl-l{color:var(--text-2)}
.ex-tbl th{text-transform:none;letter-spacing:0;font-size:var(--fs-xs);font-weight:600}
.ex-grp-n{font-weight:400}
/* Ticket numbers are references, and a reference is set in the mono face. */
.tk-id-cell span{font-family:var(--ff-mono);font-weight:500;font-size:var(--fs-xs)}
/* ── The repair's headline: aircraft, number, model ───────────────────── */
.tk-ident{display:flex;align-items:center;gap:14px;min-width:0}
.thumb-lg{width:52px;height:52px;border-radius:var(--r-lg)}
.thumb-lg svg{width:30px;height:30px}
.ph h2{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:var(--fs-2xl);font-weight:700;letter-spacing:-.015em}
.tk-id{font-family:var(--ff-mono);font-weight:500;font-size:var(--fs-lg);color:var(--accent);letter-spacing:0}
.tk-craft{font-weight:700}
/* The priority stripe is gone from the list and the board. The flag sits on
   half of all open work (Home says so, by share), so a red edge on every
   other row marked nothing and coloured everything. The flag still sorts
   first and still shows on the repair itself. */
.tk-prio-row td:first-child,.kb-prio{box-shadow:none}
/* A destructive control on every card was the loudest thing on the page. It
   stays a plain secondary button until the pointer reaches it. */
.btn-danger-quiet:hover,.btn-danger-quiet:focus-visible{border-color:var(--danger);color:var(--danger-text)}
/* Order and stage chips speak the same dot-and-word as repair statuses:
   open is hollow, active is amber (with a supplier), done is green, quiet is
   a muted dot. Word colour stays the running text colour. */
.badge.chip-open,.badge.chip-active,.badge.chip-done,.badge.chip-quiet{
  background:none;box-shadow:none;padding:0;border-radius:0;gap:7px;font-weight:500;color:var(--text-2);font-size:var(--fs-xs)}
.badge.chip-open::before,.badge.chip-active::before,.badge.chip-done::before,.badge.chip-quiet::before{
  content:"";width:8px;height:8px;border-radius:50%;flex:none;box-sizing:border-box}
.badge.chip-open::before{border:1.5px solid var(--text-muted)}
.badge.chip-active::before{background:var(--warning);box-shadow:0 0 0 3px var(--warning-bg)}
.badge.chip-done{color:var(--success-text)}
.badge.chip-done::before{background:var(--success);box-shadow:0 0 0 3px var(--success-bg)}
.badge.chip-quiet::before{background:var(--text-muted)}
/* Board cards carry their number the way every list does. */
.kb-id{font-family:var(--ff-mono);font-weight:500;color:var(--text-muted);font-size:var(--fs-xs)}
.kb-card{border-radius:var(--r-lg)}
/* ── The repair page: a card per question ────────────────────────────── */
.tk-two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px}
@media (max-width:1180px){.tk-two{grid-template-columns:1fr}}
.tk-gcard{padding:18px 20px}
.tk-gcard .tk-group{margin:0;padding:0;border:0}
.tk-gcard .tk-grid{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
.tk-dates{margin-top:0;padding-top:0;border-top:0}
.tk-gcard .tk-grid > *{min-width:0;overflow-wrap:anywhere}
/* "Look up" is one word to the eye, so the button never breaks it. */
.rc-scan-row .btn{white-space:nowrap;flex:none}
/* The sign-in and setup scenes sit on the page's own ground with a soft
   wash of the brand blue in one corner, instead of a navy gradient that
   belonged to the old sidebar. */
.login-overlay,.setup-overlay{background:radial-gradient(1100px 620px at 12% -8%,var(--accent-soft),transparent 62%),var(--bg)}
.login-card{border:1px solid var(--border)}
/* Labels on the portal and connector panels speak in sentences too. */
.pt-k{text-transform:none;letter-spacing:0;font-size:var(--fs-xs);font-weight:600;color:var(--text-2)}
/* A category name is short and read whole; the column grows to fit it. */
.inv-tbl .badge{max-width:none}
/* Document numbers — invoices, estimates — are references like ticket
   numbers, and read the same way: mono, quiet, lit on hover. */
.doc-id{font-family:var(--ff-mono);font-weight:500;color:var(--accent);font-size:var(--fs-xs)}
/* The theme control grew a drawn sun or moon beside its word, so it is a
   pill now rather than a circle that fit one word. */
.dm-btn{width:auto;padding:0 12px 0 10px;gap:6px;font-size:var(--fs-sm);font-weight:500;color:var(--text-2)}
.dm-btn svg{width:16px;height:16px}
/* A repair number longer than its column (the mirror's -RS twins) is cut
   with an ellipsis rather than run over the next cell; the full number is
   on the cell's title. */
.tk-id-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The kicker: the storefront's signature, a 7px square of the accent before
   a small mono label. The app carries it on exactly two screens — sign-in and
   the Updates card, its front-door moments — and on no section heading, which
   is where it would become a tell (../ODR-KB/3-reference/design-tokens.md). */
.kicker{display:flex;align-items:center;gap:.5rem;font-family:var(--ff-mono);font-size:.6875rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px}
.kicker::before{content:"";width:7px;height:7px;border-radius:1px;background:var(--accent);flex:none}
.login-brand .kicker{margin-bottom:2px}
/* A ghost button is text with a hit area: no fill until hovered. It had no
   rule at all, so the sign-in card's invitation control drew in the
   browser's default grey. */
.btn-ghost{background:transparent;color:var(--text-2);border:1px solid transparent}
.btn-ghost:hover{background:var(--surface-2);color:var(--text)}

/* The customer's own reference, beside the internal one on a repair's identity
   line. Quieter than `.tk-id` on purpose: the bench works in TK numbers all
   day, and this is the string they read out when somebody phones. Mono because
   it gets read character by character down a telephone, and the tabular figures
   stop a 9 and a 4 trading places at a glance. */
.tk-ref{
  font-family:var(--ff-mono);
  font-variant-numeric:tabular-nums;
  font-weight:500;
  font-size:var(--fs-sm);
  color:var(--text-2);
  background:var(--surface-2);
  border:1px solid var(--border);
  border-radius:var(--r-sm);
  padding:1px 7px;
  margin-left:10px;
  vertical-align:2px;
  white-space:nowrap;
}

/* ── FIRST RUN ──────────────────────────────────────────────────────────────
   The one-line hint above the Home cards.

   🔴 These classes were listed in this file's contents at the top and never
   actually written, so the strip rendered with no styling at all: three block
   elements stacked, the sample serial crushed against its dismiss button, and
   a `<b>` doing all the visual work. A class named in a table of contents
   reads exactly like a class that exists.

   It is a strip, not a card. It sits above the real content, says one thing,
   carries a control that does that thing, and leaves for good when used. So it
   is quieter than a card and shorter than a banner. */
.frun{
  display:flex;
  align-items:center;
  gap:14px;
  flex-wrap:wrap;
  padding:12px 14px;
  margin-bottom:16px;
  background:var(--surface-sunken);
  border:1px solid var(--border);
  border-radius:var(--r-lg);
}
.frun-body{
  flex:1 1 260px;      /* takes the room, wraps before it squeezes the button */
  min-width:0;
  font-size:var(--fs-sm);
  line-height:1.5;
  color:var(--text-2);
}
.frun-body b{ color:var(--text); font-weight:650; }

/* The sample serial reads as an identifier, so it is set like every other id
   in the app: mono, tabular, and not wrapped mid-string down a phone. */
.frun .btn{
  flex:0 0 auto;
  font-family:var(--ff-mono);
  font-variant-numeric:tabular-nums;
  letter-spacing:.01em;
  white-space:nowrap;
}

/* 🔴 Its own control, not a `.btn`. Rendered as one it inherited the button
   padding and border and became a second, competing action beside "Try" —
   which is how the pair ended up looking jammed together. Dismissing is the
   quieter of the two things you can do here, and it should look it. */
.frun-x{
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:28px;
  height:28px;
  margin-left:-4px;
  padding:0;
  border:0;
  border-radius:var(--r-md);
  background:transparent;
  color:var(--text-muted);
  cursor:pointer;
}
.frun-x:hover{ background:var(--surface-2); color:var(--text); }
.frun-x:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.frun-x svg{ width:16px; height:16px; }

/* On a narrow window the sentence takes the first row and the two controls sit
   together on the second, rather than the serial being squeezed to nothing. */
@media (max-width:640px){
  .frun{ gap:10px; }
  .frun-body{ flex:1 1 100%; }
}

/* ── Two things a busy header was breaking ──────────────────────────────────

   🔴 THE PILL WAS CLIPPING MID-WORD. `.inv-tbl .badge{max-width:none}` earlier
   in this layer overrode the ellipsis rule set for it, so a long category met
   the cell's `overflow:hidden` and was cut with no ellipsis: "Cameras & Gim".
   A hard cut reads as a data error — somebody goes looking for a truncated
   record — where an ellipsis reads as what it is, a column too narrow. Same
   number of pixels, completely different message.

   Restored as an ellipsis, and the title carries the full text so it is
   readable on hover rather than merely honest about being cut. */
.inv-tbl .badge{
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  vertical-align:bottom;
}

/* 🔴 THE GLOBAL SEARCH WAS BEING SQUEEZED OUT BY ITS OWN PAGE'S BUTTONS.
   On Parts — three header actions wide — it shrank to "Search customers, repa",
   which is a search box that no longer says what it searches. It was `flex:1`
   against buttons that would not give, so it lost every argument.

   The floor is the placeholder, roughly: below this the control stops
   explaining itself and becomes a mystery box. The page's own actions shrink
   first because they are labelled and stay recognisable at any width, and
   because search is the one control on every screen. */
/* Three things want the same row, so the order they give way in is stated
   rather than left to whichever has the longest text.

   🔴 The tabs never shrink. They are navigation, and a clipped "Purchase
   order" is a control somebody cannot identify — which is exactly what the
   first version of this rule caused by letting the search grow into them.
   Search gives way second, because a narrowed search box is still obviously a
   search box. The page's own buttons give way first: they are labelled, and
   they are the only ones a person can also reach another way. */
.hdr-id{ min-width:0; flex-shrink:0; }
.hdr-tabs{ flex:none; white-space:nowrap; }
.hdr-search{ min-width:min(240px, 26vw); flex-shrink:1; }
.hdr-actions{ min-width:0; flex-shrink:2; }
.hdr-actions .btn{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* ── People cards line up ───────────────────────────────────────────────────
   🔴 The markup already asked for this and could not have it. The action row
   carries `margin-top:auto`, which pins it to the bottom of a flex column —
   but `.emp-card` was not a flex column, so the rule did nothing and the
   buttons floated wherever the content above them ended. One person with a job
   title and everybody else without pushed a single card's buttons out of line
   with the rest of the grid, which reads as a broken layout rather than as a
   fuller record.

   Grid items stretch by default, so the cards were already the same height.
   Only the inside needed to know it. */
.emp-card{
  display:flex;
  flex-direction:column;
}
/* Keeps the gap where a job title would go, so a card with a role and one
   without are the same shape rather than one being visibly shorter. */
/* The role line is omitted rather than emptied now, and the contact lines with
   it — a card carrying no email and no phone shows neither instead of two
   stacked dashes. Heights stay level because the card is a column and the
   buttons are pinned to its bottom, so the grid does the aligning. */
.emp-contact{font-size:var(--fs-sm);color:var(--text-2)}
.emp-contact+.emp-contact{margin-top:2px}
.emp-contact:last-of-type{margin-bottom:12px}

/* ══════════════════════════════════════════════════════════════════════════
   THIRD PASS — the surfaces nobody draws
   ══════════════════════════════════════════════════════════════════════════

   🔴 Everything below is a part of the interface the browser supplies by
   default, in a palette belonging to no design system: the selection
   highlight, the caret, the scrollbars, the shape of a focus ring. They are
   the cheapest signal that a screen was built rather than assembled, and they
   are the first thing that gives away an app that is otherwise carefully
   made. An operator meets all four of them within a minute of opening this.

   Refinement only. Nothing here changes a layout, a colour meaning, or a
   control's behaviour. */

/* ── Selection ─────────────────────────────────────────────────────────────
   The default is the operating system's blue, which is neither of this app's
   blues and lands hardest on the mono ids and serials — the exact strings
   somebody selects to copy. Tinted from the accent, kept light enough that
   selected text stays readable rather than reversing out. */
::selection{ background:var(--accent-soft); color:var(--text); }
::-moz-selection{ background:var(--accent-soft); color:var(--text); }

/* ── The caret ─────────────────────────────────────────────────────────────
   Black on a dark field is nearly invisible, which is why a technician
   sometimes cannot tell whether a search box has focus. */
input,textarea,[contenteditable]{ caret-color:var(--accent); }

/* ── Scrollbars ────────────────────────────────────────────────────────────
   ⚠️ The default WebKit scrollbar is a light grey trough. On this app's dark
   ground it is a bright stripe down the side of every long table — the single
   most visible unstyled surface in the product.
   Drawn as the content's own edge rather than as a control: no trough, a
   thumb the weight of a border, and a hover state so it is discoverable
   without ever being loud. Wide enough to grab at 10px; Apple's own is 7. */
*{ scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
::-webkit-scrollbar{ width:10px; height:10px; }
::-webkit-scrollbar-track{ background:transparent; }
::-webkit-scrollbar-thumb{
  background:var(--border);
  border-radius:var(--r-full);
  /* The inset keeps the thumb off the very edge, so it reads as floating on
     the content rather than as a fixture bolted to the window. */
  border:2px solid transparent;
  background-clip:content-box;
}
::-webkit-scrollbar-thumb:hover{ background:var(--text-muted); background-clip:content-box; }
::-webkit-scrollbar-corner{ background:transparent; }

/* ── Headings break where a person would break them ────────────────────────
   `balance` stops a two-line heading leaving one orphaned word on the second
   line. It costs nothing and is the difference between a heading that was set
   and one that was poured. Capped at four lines by the spec, which is why it
   is safe to apply broadly. */
h1,h2,h3,.tk-card-h,.modal-title,.ex-h{ text-wrap:balance; }
/* Body copy wants the opposite: `pretty` only guards the last line, so long
   hint text stops ending on a single word. */
.fhint,.rc-hint,.td-muted,.empty p{ text-wrap:pretty; }

/* ── Numerals ──────────────────────────────────────────────────────────────
   The display numbers are already tabular. What they were missing is the
   optical correction every large-set number needs: at 28px the default
   tracking that suits 14px body text reads loose and slightly cheap. */
.q-n,.ex-n,.money-big{ letter-spacing:-.022em; font-variant-numeric:tabular-nums; }

/* ── Underlines ────────────────────────────────────────────────────────────
   A link underline sitting on the baseline cuts the descenders of p, g and y.
   Two pixels down and skipping the ink is what a typesetter would do. */
a{ text-underline-offset:.18em; text-decoration-thickness:.07em; }
a:not([class]){ text-decoration-skip-ink:auto; }

/* ── The one authored moment ────────────────────────────────────────────────

   🔴 The app had six animations and all six were utility: a toast sliding in,
   a shake on a bad password, three spinners. Nothing was authored for the
   thing an operator does forty times a day, which is move between areas.

   This is that moment and it is deliberately the only one. Not a stagger down
   a table — four hundred rows arriving in sequence is a screen a person waits
   for, and this app's own principle is speed back out of the screen. Not a
   fade, either: a page that fades in is a page that was briefly not there.

   The content settles. Six pixels, 260ms, exponential ease-out — the curve
   things follow when they are decelerating rather than being placed. Fast
   enough to read as responsiveness rather than as decoration, and it lands
   before a person's eye has finished travelling from the sidebar.

   ⚠️ The resting state is the visible one. `.page.active` is `display:block`
   with no transform of its own, so if the animation never runs — reduced
   motion, an old webview, a stylesheet that failed — the page is simply
   there. The animation refines an already-correct default rather than
   revealing content that was hidden waiting for it. */
@keyframes page-settle{
  from{ transform:translate3d(0,6px,0); opacity:.85 }
  to  { transform:none;                 opacity:1   }
}
.page.active{
  animation:page-settle 260ms cubic-bezier(.16,1,.3,1) both;
}
/* 🔴 No `will-change`. It was here with a comment claiming the layer was
   "promoted only while it moves", and the rule did the opposite: `will-change`
   on `.page.active` holds a compositor layer for the whole session, on a page
   that carries a 400-row table. The browser already promotes for the duration
   of an animation; the hint bought nothing and kept the cost.
   Measured on the repairs board with 400 rows on screen: 55fps through the
   settle, which is smooth at this duration. */
/* The sidebar and header do not participate. They are the fixed frame the
   content moves inside, and animating the frame with its contents is what
   makes an interface feel like a slideshow. */
@media (prefers-reduced-motion:reduce){
  .page.active{ animation:none }
}

/* ── The pair of cards on a repair sizes to what is in them ─────────────────
   🔴 A CSS grid stretches its items to the tallest row by default, so the
   "Where it stands" card — often just an owner and a button — was drawn the
   full height of "Condition on arrival" beside it, with 200px of nothing
   under three words. Two cards of visibly different content forced into the
   same rectangle read as a layout that failed, not as a pair.

   `start` lets each end where its content does. The pair still aligns at the
   top, which is the alignment a reader actually uses. */
.tk-two{ align-items:start; }

/* ── The two Home panels balance ────────────────────────────────────────────
   🔴 Both hold eight rows, and the left one's rows are shorter — so the panel
   that lists every open status ended a hundred pixels above its neighbour,
   with a hole under the last bar. Two panels drawn as a matched pair have to
   agree, and the honest way to make them agree is to give the tighter one the
   room it should have had, not to trim the other.

   11px to 15px of vertical padding. That is a bigger target for a technician
   pointing at a shop-floor screen, more air between eight rows that are read
   as a list rather than a table, and it closes the gap. Three arguments, one
   change; the spacing was the weak part regardless of the alignment. */
#page-dashboard .sbar{ padding-top:15px; padding-bottom:15px; }

/* The manual sweep sits under the postage table it affects. */
.cx-sweep{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:14px;
  padding-top:14px;border-top:1px solid var(--border)}
.cx-sweep .td-muted{flex:1 1 320px;min-width:0;line-height:1.5}

/* ── The four-card strips give way before they get cramped ──────────────────

   🔴 CORRECTION, same night: I first wrote that these had "no breakpoint
   anywhere". `.q-grid` already had one at 1180px, higher up this file, which I
   missed. `.stat-grid` genuinely had none. What this rule does is tighten
   1180 to 1330 and bring `.stat-grid` along, which the arithmetic below still
   supports — at 1180 a card is 217px, well under the floor.

   This Mac is 1440 and hides all of it; the shop's machines are not this Mac.

   Measured against the real layout — 204px sidebar, 24px page padding, 14px
   gaps — each card gets:

     1440px window → 286px
     1366px        → 268px
     1280px        → 246px   a common laptop, and already too narrow
     1024px        → 182px

   250px is the floor: below it "worst is 203 days past its deadline" stops
   fitting on two lines under a 28px numeral, and the card that is supposed to
   be read in one glance needs three.

   Two columns rather than three at the first step, because four cards in
   three columns leaves one alone on a second row — which reads as a mistake
   rather than as a wrap. */
@media (max-width:1330px){
  #page-dashboard .q-grid,
  .stat-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:700px){
  #page-dashboard .q-grid,
  .stat-grid{ grid-template-columns:1fr; }
}

/* The three-column grids, same argument, one step later because three columns
   of 1fr survive a narrower window than four do. `.ex-heads` is Command's
   summary row and `.mt-grid` is the money strip; both were fixed at every
   width. */
@media (max-width:1100px){
  .ex-heads,
  .mt-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:700px){
  .ex-heads,
  .mt-grid{ grid-template-columns:1fr; }
}

/* Two lists side by side, each ending where its own content ends.
   Money puts "Waiting on a link" beside "Collect on pickup", and the first is
   empty most days while the second carries forty rows — stretched to match, the
   empty card became five hundred pixels of nothing, which reads as a panel that
   failed to load rather than as a queue with nobody in it. Scoped to the page
   because a stretched pair is right where two cards are meant to be read as one
   block; here they are two independent answers. */
#page-money .grid-2{ align-items:start; }

/* A badge asked for a tone by name and got nothing.
   `<span class="badge tone-violet">Test — not a real repair</span>` and
   `<span class="badge tone-red">Public safety</span>` both rendered as plain
   grey text on the check-in queue, because the ten tones existed as tokens and
   as status classes but never as a `tone-*` class a badge could ask for. The
   two that matter most are exactly the two that must never be missed: a test
   record a technician might pick up, and a public-safety unit that jumps the
   queue. Named after the token so a new badge picks a tone rather than a hex,
   which is the rule the tokens were written for. */
.badge.tone-neutral{background:var(--tone-neutral-bg);color:var(--tone-neutral-fg);box-shadow:inset 0 0 0 1px var(--tone-neutral-bd)}
.badge.tone-blue{background:var(--tone-blue-bg);color:var(--tone-blue-fg);box-shadow:inset 0 0 0 1px var(--tone-blue-bd)}
.badge.tone-indigo{background:var(--tone-indigo-bg);color:var(--tone-indigo-fg);box-shadow:inset 0 0 0 1px var(--tone-indigo-bd)}
.badge.tone-violet{background:var(--tone-violet-bg);color:var(--tone-violet-fg);box-shadow:inset 0 0 0 1px var(--tone-violet-bd)}
.badge.tone-teal{background:var(--tone-teal-bg);color:var(--tone-teal-fg);box-shadow:inset 0 0 0 1px var(--tone-teal-bd)}
.badge.tone-green{background:var(--tone-green-bg);color:var(--tone-green-fg);box-shadow:inset 0 0 0 1px var(--tone-green-bd)}
.badge.tone-amber{background:var(--tone-amber-bg);color:var(--tone-amber-fg);box-shadow:inset 0 0 0 1px var(--tone-amber-bd)}
.badge.tone-gold{background:var(--tone-gold-bg);color:var(--tone-gold-fg);box-shadow:inset 0 0 0 1px var(--tone-gold-bd)}
.badge.tone-orange{background:var(--tone-orange-bg);color:var(--tone-orange-fg);box-shadow:inset 0 0 0 1px var(--tone-orange-bd)}
.badge.tone-red{background:var(--tone-red-bg);color:var(--tone-red-fg);box-shadow:inset 0 0 0 1px var(--tone-red-bd)}

/* The header must never run off its own right edge.
   Parts puts three actions up there — "PO from below-reorder (344)", "Count
   stock", "+ Add Part" — and beside the global search, the bell, the theme
   toggle and the avatar they added up to more than 1440px. Nothing wrapped;
   the row simply overflowed, and the thing that fell off the edge was the
   avatar, the one control that is in the same place on every screen.
   Flex only shrinks what it is allowed to shrink, and `min-width:0` is that
   permission. The search gives way first because a narrower search box is
   still a search box, then the buttons truncate to an ellipsis — a clipped
   label is recoverable, a control off-screen is not. */
#hdr>.hdr-id{flex:0 1 auto;min-width:0}
#hdr>.hdr-search{flex:1 1 auto;min-width:0}
#hdr>.hdr-right{flex:0 1 auto;min-width:0}
.hdr-right>.hdr-actions{min-width:0;flex:0 1 auto}
/* Everything after the page's own actions is fixed furniture and keeps its
   size: the bell, the theme toggle and the avatar are the same three controls
   on every screen, and a squashed avatar reads as a rendering fault. */
.hdr-right>.icon-btn,
.hdr-right>.dm-btn,
.hdr-right>.user-av,
.hdr-right>.timer-chip{flex:none}

/* The vendor's case number once it HAS one. A reference a person reads down a
   telephone is data, not a link — underlining it the way the "record it"
   invitation is underlined made it look clickable in the wrong way, and a long
   value plus its batch wrapped mid-number onto a second line. Quiet, aligned
   left with the label above it, and it still opens the editor when pressed. */
.tk-vcase{display:flex;flex-direction:column;align-items:flex-start;gap:1px;
  background:none;border:0;padding:0;margin:0;cursor:pointer;
  font:inherit;color:inherit;text-align:left;max-width:100%}
.tk-vcase:hover .fld-mono{text-decoration:underline;text-underline-offset:3px}
.tk-vcase .fld-mono{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.tk-vcase-b{font-size:var(--fs-xs)}

/* The battery answer on a repair. "They were not sure" is the one that should
   catch an eye: it is the answer a yes/no question would have recorded as "no
   battery is coming", and it is the box that gets packed wrong. Amber rather
   than red — nothing is broken, somebody just has to ask. */
.tk-batt-warn{color:var(--warning-text);font-weight:500}
.tk-batt-where{color:var(--text-2)}

/* The battery line on a waiting submission. Amber because it changes what the
   person at the counter does next — a parcel with a loose cell is packed and
   labelled differently — and not red, because nothing is wrong. */
.rc-batt{color:var(--warning-text)}
