/* ==========================================================================
   eCrew Sync ÔÇö design system ┬À v1.0 "Aurora Glass"
   Offline-first roster manager for Air Alg├®rie crew.
   Structure:
     01 Tokens & themes      08 Stat tiles
     02 Reset & base         09 Calendar
     03 Utilities           10 Week / day / list views
     04 Hero & app bar      11 Day cards
     05 Buttons             12 Dashboard
     06 Tabs                13 Trips
     07 Layout & pills      14 Stats pane & charts
                            15 Route map
                            16 Modals & forms
                            17 Search & command palette
                            18 Toasts
                            19 Loading / empty / errors
                            20 Footer & kbd
                            21 Ops checks
                            22 Motion, responsive, print
   ========================================================================== */

/* ============================ 01 ┬À TOKENS ================================ */

:root {
    /* Print body size (L64). Declared here so it is a token like the rest. */
    --fs-print: 11pt;
    /* brand ÔÇö
       --accent is a *user choice*, so it must never carry meaning on its own:
       status, danger and error roles use the fixed tokens below.

       --accent-soft resolves through --accent-soft-user. It used to be written
       here AND as an inline style on <html> by applySettings(), and an inline
       style outranks every rule in this file -- so the light theme's value was
       permanently dead and light mode always showed the dark tint. See the
       light block below, which is only alive because of this indirection.

       --accent-soft-user is set only when the crew member has actually picked a
       palette. Leaving it unset for the brand default is the point: the default
       carries a dark-theme tint, so writing it unconditionally would keep the
       bug while looking like a fix. */
    --accent: #2563eb;
    --accent-soft: var(--accent-soft-user, #1e3a8a);

    /* fixed semantic roles ÔÇö independent of the chosen accent
       These are used as *text* colours, so they are ink, not fills. Measured on
       the surfaces below: #b3231f gave 2.78:1 on --surface and #a8231d gave
       2.56:1 -- both below even the 3:1 that large text is allowed, and they
       carried every "Delete", every sign-in failure and every "months missing"
       warning. #e5534b measures 4.96 / 5.37 / 5.13 on --surface / --bg /
       --surface-2, and #d97706 measures 5.77 for the warning.

       Those were the old coral values. The Aero-Tech ramp declared further down
       re-declares --danger, --error and --warning with measured inks, and it is
       the one that is read: a later declaration of the same token inside the
       same block wins, so declaring them twice would leave the file carrying two
       answers and only one of them true. The history stays because it is the
       reason the ink was re-derived. */

    /* neutrals */
    /* neutrals — Deep Cockpit Slate
       Four levels and no more, because a fifth is how a palette stops being a
       palette. Every one of these is verified against every other, not against
       the page alone: --text-faint measures 4.94:1 on --input, the lightest
       surface in the app, so no label falls under AA by sitting on the wrong one.

         --bg        the page
         --surface   cards, level 1
         --surface-2 nested cards, level 2
         --input     anything you type into or press, level 2

       Pure black (#000000) is banned. It is not "darker", it is a different
       material, and it makes every elevation step read as a hole rather than a
       surface. */
    --bg: #090d16;
    --surface: #0f172a;
    --surface-2: #1e293b;
    --card: #0f172a;
    --input: #1e293b;
    --card-border: rgba(255, 255, 255, 0.08);
    --card-border-2: rgba(255, 255, 255, 0.16);
    --text: #f8fafc;
    --text-soft: #94a3b8;
    /* 5.4:1 on white ÔÇö the small uppercase meta labels rely on this */
    /* Was #5c6b82: 3.68:1 on the page background, below the 4.5:1 that WCAG AA
       asks of ordinary text. It is the greeting's colour, and small text at
       11px, so it is read rather than scanned. #727f94 keeps the cool cast and
       lands at 4.9:1 on the page and 4.5:1 on a card. */
    /* The greeting, and every small uppercase meta label, reads in this. The old
       #727f94 was measured against the old near-black page and cleared 4.9:1
       there; the whole ramp moved with it, so it was measured again rather than
       assumed. #8a97ab clears 4.94:1 on --input, the lightest surface here. */
    /* Aero-Tech: #7c8aa0 was the specified value and measures 4.18:1 on
       --surface-2, the lightest surface here -- under AA for a label that is read
       rather than scanned. #8290a5 is the same hue lightened 4.8 %, and clears
       4.52:1. The deviation is measured, not a preference. */
    --text-faint: #8290a5;

    /* Duty status roles (mirrors TYPE_SET in app.js). Each role is a *pair*: the
       vivid value paints the chip's fill, its dot and the chart stroke, and the
       -ink value is what carries text. Every ink below clears 4.5:1 measured on
       --input, the worst surface a badge can land on, so a role cannot fail AA by
       being placed somewhere else in the app.

       --*-soft is the 10%-opacity wash the design calls for, written as a
       color-mix against --card rather than a fixed rgba. A fixed rgba is a fixed
       blend with whatever happens to be behind it, so the same badge goes muddy
       on one surface and clean on another; a mix against the surface keeps every
       wash at the same proportion of its own background. */
    /* Aero-Tech roles. The five accents are the specified ones; the inks are
       derived from them and measured, because an accent cannot double as text.
       Measured worst-case on --input (the lightest surface here), so a role
       cannot fail AA by being placed somewhere else:

         --flight      #0284c7 as text ->  3.57:1  FAIL   -> ink #38bdf8  6.83:1
         --hotel       #10b981 as text ->  5.77:1  pass   -> kept as its own ink
         --standby     #f59e0b as text ->  6.81:1  pass   -> kept as its own ink
         --training    #8b5cf6 as text ->  3.45:1  FAIL   -> ink #a17bf8  4.67:1
         --danger      #ef4444 as text ->  3.89:1  FAIL   -> ink #f26262  4.67:1

       The two inks derived by lightening (#a17bf8 +19 %, #f26262 +16 %) and
       --flight-ink were given in the brief; the other two are the specified
       accents unchanged, because they already clear the threshold as text. */
    --flight: #0284c7;   --flight-ink: #38bdf8;
    --flight-soft: color-mix(in srgb, #0284c7 12%, var(--card));
    --flight-line: color-mix(in srgb, #0284c7 38%, transparent);
    --sky: #0284c7;      --sky-ink: #38bdf8;
    --sky-soft: color-mix(in srgb, #0284c7 12%, var(--card));
    --hotel: #10b981;    --hotel-ink: #10b981;
    --hotel-soft: color-mix(in srgb, #10b981 12%, var(--card));
    --hotel-line: color-mix(in srgb, #10b981 38%, transparent);
    --green: #10b981;    --green-ink: #10b981;
    --green-soft: color-mix(in srgb, #10b981 12%, var(--card));
    --standby: #f59e0b;  --standby-ink: #f59e0b;
    --standby-soft: color-mix(in srgb, #f59e0b 12%, var(--card));
    --standby-line: color-mix(in srgb, #f59e0b 38%, transparent);
    --amber: #f59e0b;    --amber-ink: #f59e0b;
    --amber-soft: color-mix(in srgb, #f59e0b 12%, var(--card));
    --training: #8b5cf6; --training-ink: #a17bf8;
    --training-soft: color-mix(in srgb, #8b5cf6 12%, var(--card));
    --training-line: color-mix(in srgb, #8b5cf6 38%, transparent);
    --purple: #8b5cf6;   --purple-soft: color-mix(in srgb, #8b5cf6 12%, var(--card));
    --reserve: #0284c7;  --reserve-ink: #38bdf8;
    --reserve-soft: color-mix(in srgb, #0284c7 12%, var(--card));
    --reserve-line: color-mix(in srgb, #0284c7 38%, transparent);
    /* Off is the absence of work, so it is the quietest role on the ramp. At 7%
       it is a whisper beside a flight at 10% -- which is the point. The month grid
       was spending 15 of its 31 chips on days that held no work at all. */
    --off-ink: #cbd5e1;
    --off-soft: color-mix(in srgb, #cbd5e1 7%, var(--card));
    --off-line: color-mix(in srgb, #cbd5e1 22%, transparent);
    --gray: #64748b;     --gray-ink: #cbd5e1;
    --gray-soft: color-mix(in srgb, #94a3b8 8%, var(--card));
    /* Red is reserved for infractions, conflicts and missing data. The vivid
       #ef4444 stays available as a *fill* -- a dot, a chip, a chart stroke --
       and --danger carries the ink, because #ef4444 as text measures 3.89:1 on
       the lightest surface. Both are declared: an accent that exists only as an
       ink has nothing left to paint with. */
    --danger: #f26262;
    --danger-fill: #ef4444;
    --danger-soft: color-mix(in srgb, #ef4444 12%, var(--card));
    --error: #f26262;
    --warning: #f59e0b;
    --accent-ink: #f26262;

    /* Flat surface ramp for the page wash. It moved with everything else, so a
       gradient built from these tokens still lands on the cockpit slate rather
       than on the retired near-black palette. */
    --bg-grad-1: #090d16;
    --bg-grad-2: #0f172a;

    /* elevation
       Shadows on a dark surface cannot be the usual black drop: black is already
       the ground, so a black shadow is invisible and the card reads as painted-on.
       These are two-part -- a tight contact shadow to seat the card on the page,
       and a wide soft one for the lift -- and they are tinted with the app's own
       navy rather than neutral black so they read as depth instead of dirt. */
    --shadow-xs: 0 1px 2px rgba(2, 6, 16, 0.40);
    --shadow-sm: 0 1px 2px rgba(2, 6, 16, 0.44), 0 2px 6px rgba(2, 6, 16, 0.30);
    --shadow-md: 0 4px 12px -2px rgba(2, 6, 16, 0.48), 0 2px 6px rgba(2, 6, 16, 0.30);
    --shadow-lg: 0 16px 40px -12px rgba(2, 6, 16, 0.60), 0 6px 14px rgba(2, 6, 16, 0.34);
    --shadow-xl: 0 36px 72px -20px rgba(2, 6, 16, 0.70), 0 10px 24px rgba(2, 6, 16, 0.40);
    /* The ring on a dark ground has to be brighter than the classic 20% wash: at
       that strength over #0b0f17 it was invisible on a focus, which is the one
       moment a keyboard user is guaranteed to be looking. */
    --ring: 0 0 0 3px color-mix(in srgb, var(--sky) 55%, transparent);
    --ring-soft: 0 0 0 3px color-mix(in srgb, var(--sky) 34%, transparent);

    /* radii */
    --r-xs: 4px;
    --r-sm: 6px;
    --r-md: 8px;
    --r-lg: 12px;
    --r-xl: 16px;
    --r-full: 999px;   /* pills and round chips; 99px and 999px both meant this */

    /* --- The scales -------------------------------------------------------
       There were twenty-seven distinct font sizes, twenty-eight radii and
       twenty-seven spacing values in this file, including eight half-pixels.
       Every one had been chosen per component, so nothing lined up and nothing
       could be changed once for everything. These are the steps everything now
       uses.

       Type is integer-only on purpose: half-pixels round differently depending
       on the zoom level and the device pixel ratio, which is why they creep in
       in the first place.

       Spacing is 2px and 4px steps. Radii stop at five values plus the pill. */
    --fs-3xs: 9px;
    --fs-2xs: 10px;
    --fs-xs: 11px;
    --fs-sm: 12px;
    --fs-base: 13px;   /* 14 only under [data-density="comfortable"]; the default density is compact */
    --fs-md: 15px;
    --fs-lg: 17px;
    --fs-xl: 20px;
    --fs-2xl: 24px;
    --fs-3xl: 30px;
    /* Display type, for the focus sheet: the flight number and the route, which
       are the two things a crew member reads in a corridor. They are fluid because
       a fixed px cannot serve a phone in landscape and a laptop -- that part is
       deliberate -- but they used to be written inline as four separate
       `clamp(...)` calls, which left them outside this scale entirely and so
       unchangeable as a group. Naming them is what was missing.

       The two `short` pair respond to height rather than width, for a landscape
       phone where width is generous and height is not. */
    --fs-display: clamp(38px, 11vw, 62px);
    --fs-display-route: clamp(19px, 5.4vw, 27px);
    --fs-display-short: clamp(30px, 7vh, 42px);
    --fs-display-route-short: clamp(16px, 3.6vh, 22px);

    --space-0: 0;
    /* S1: a one-device-pixel rule for the year picker's coverage strip. The
       scale's smallest step is 2px, which is too wide for a hairline between
       twelve cells. Declared here so it is altered in one place like the rest. */
    --hairline: 1px;
    --space-1: 2px;
    --space-2: 4px;
    --space-3: 8px;
    --space-4: 12px;
    --space-5: 16px;
    --space-6: 20px;
    --space-7: 24px;
    --space-8: 32px;
    --space-9: 40px;
    --space-10: 48px;
    --space-11: 56px;

    /* The app bar's height, in one place. `.hero` and `.tabs { top }` both
       hard-coded 52px, so any change to one silently desynchronised the sticky
       offset from the real header height. */
    --appbar-h: 52px;

    /* density — the knob the cockpit layout scales from. --cell-h stood here too,
       and this comment called the block the *single* knob while nothing read it:
       .cal-cell hard-codes min-height: 100px, so the month grid never scaled
       from it either. Removed with the rest of the dead tokens (audit L59), which
       leaves every name below actually read. */
    --row-h: 30px;
    --gap: var(--space-3);
    --pad: var(--space-4);
    --fs-meta: var(--fs-xs);     /* kept: widely referenced, now a step */
    --fs-label: var(--fs-2xs);

    /* Glass. Dark values live here because the root *is* the dark theme -- the
       light block below overrides them. They used to be white, which meant dark
       mode put a bright band across the top of the page: the header, the tab bar
       and every surface using --glass rendered as translucent white over a
       near-black page, and controls whose borders were rgba(255,255,255,0.08)
       became white-on-white outlines. Built from --surface instead, so it sits
       with the rest of the dark surfaces. */
    --glass: rgba(19, 27, 42, 0.72);
    --glass-brd: rgba(255, 255, 255, 0.10);
    --glass-tint: rgba(255, 255, 255, 0.06);
    --glass-brd-dark: rgba(255, 255, 255, 0.16);

    /* type
       Inter and Plus Jakarta are named first because the brief asks for them and
       because they are what the layout was tuned against -- but this app is
       offline-first with no build step and no webfont fetch, so neither is ever
       actually downloaded. What follows is a stack that lands on a close
       substitute rather than on Times, and `--mono` matters more than --font
       here: see --tnum below. */
    --font: Inter, "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont,
            "Segoe UI Variable Display", "Segoe UI", Roboto, "Helvetica Neue",
            Arial, "Noto Sans", sans-serif;
    --mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", Consolas,
            "Liberation Mono", monospace;
    /* Times, IATA codes and counters are read as *columns*, and a proportional
       digit makes "11:30Z" and "11:35Z" different widths, so a column of them
       shimmers as it updates. tabular-nums fixes the advance width; it is applied
       to every .mono below rather than left to each component to remember. */
    --tnum: tabular-nums;

    /* motion */
    --dur: 0.22s;
    --dur-fast: 0.14s;
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);

    /* decoration */
    --plane-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z'/%3E%3C/svg%3E");
    --shell: 1440px;
}

[data-theme="dark"] {
    color-scheme: dark;
}
[data-theme="light"] {
    color-scheme: light;
    --bg: #fafafa;
    --surface: #ffffff;
    --surface-2: #f4f4f5;
    --card: #ffffff;
    --card-border: rgba(9, 9, 11, 0.10);
    --card-border-2: rgba(9, 9, 11, 0.16);
    --text: #18181b;
    --text-soft: #52525b;
    --text-faint: #6b6b73;   /* was #71717a: 4.40:1 on --surface-2, just under the 4.5
                           that 10-13px labels need. 5.28 / 5.06 / 4.81 now. */

    --accent-soft: var(--accent-soft-user, #fee2e2);
    --danger: #b91c1c;
    --danger-soft: #fee2e2;
    --error: #b91c1c;
    /* The vivid #ef4444 stays the fill in this theme too; --danger is its ink. */
    /* #a16207 measured 4.48:1 on --surface-2, just under the 4.5 that body text
       needs. #92400e gives 6.10 on all three light surfaces. */
    --warning: #92400e;

    /* Aero-Tech light theme. Same five accents, re-measured against white
       surfaces: an accent that clears 4.5:1 on #090d16 has no reason to clear
       it on #ffffff, so each ink is darkened until it does. The fills stay the
       specified accents; the inks are what carries the text. */
    --flight: #0284c7;        --flight-ink: #075985;
    --flight-soft: #e0f2fe;   --flight-line: #7dd3fc;
    --sky: #0284c7;           --sky-ink: #075985;         --sky-soft: #e0f2fe;
    --hotel: #10b981;         --hotel-ink: #047857;
    --hotel-soft: #ecfdf5;    --hotel-line: #6ee7b7;
    --green: #10b981;         --green-ink: #047857;       --green-soft: #ecfdf5;
    --standby: #f59e0b;       --standby-ink: #92400e;
    --standby-soft: #fffbeb;  --standby-line: #fcd34d;
    --amber: #f59e0b;         --amber-ink: #92400e;       --amber-soft: #fffbeb;
    --training: #8b5cf6;      --training-ink: #6d28d9;
    --training-soft: #f5f3ff; --training-line: #c4b5fd;
    --purple: #8b5cf6;      --purple-soft: #f5f3ff;
    --reserve: #0284c7;       --reserve-ink: #075985;
    --reserve-soft: #f0f9ff;  --reserve-line: #7dd3fc;
           --off-ink: #52525b;
    --off-soft: #f4f4f5;      --off-line: #e4e4e7;
    --gray: #71717a;          --gray-ink: #52525b;        --gray-soft: #f4f4f5;
    --accent-ink: #b91c1c;
    --danger-fill: #ef4444;

    --shadow-xs: 0 1px 2px rgba(9, 9, 11, 0.05);
    --shadow-sm: 0 1px 2px rgba(9, 9, 11, 0.06);
    --shadow-md: 0 4px 12px -2px rgba(9, 9, 11, 0.10);
    --shadow-lg: 0 12px 32px -8px rgba(9, 9, 11, 0.14);
    --shadow-xl: 0 24px 56px -12px rgba(9, 9, 11, 0.18);

    --glass: rgba(255, 255, 255, 0.88);
    --glass-brd: rgba(9, 9, 11, 0.10);
    --glass-tint: rgba(9, 9, 11, 0.02);
    /* Was missing here, so it fell through to the dark value -- a white border
       at 16% on a near-white button, which is no border at all. Buttons built on
       it have been outline-less in the light theme. */
    --glass-brd-dark: rgba(9, 9, 11, 0.16);

    --bg-grad-1: #ffffff;
    --bg-grad-2: #f4f4f5;
}

/* Density: two real geometries, not two labels on one. Compact réduit de
   25 à 35 % les paddings, gaps et hauteurs ; comfortable respire (marges
   amples, en-têtes plus grands). Mesurable : la hauteur du document diffère
   significativement entre les deux modes (sonde Playwright). */
html[data-density="compact"] {
    --row-h: 26px;
    --gap: var(--space-2);
    --pad: var(--space-3);
    --fs-base: 13px;
    --fs-meta: var(--fs-xs);
    --fs-label: var(--fs-2xs);
}
html[data-density="compact"] .cal-cell { min-height: 72px; padding: var(--space-1); gap: 2px; }
html[data-density="compact"] .rot { padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2); }
html[data-density="compact"] .rot-head { padding-bottom: var(--space-2); margin-bottom: var(--space-2); gap: var(--space-2); }
html[data-density="compact"] .mtr { padding: var(--space-1) var(--space-2); }
html[data-density="compact"] .mix-row { padding-top: var(--space-1); padding-bottom: var(--space-1); }
html[data-density="compact"] .dash-card { padding: var(--space-3); }
html[data-density="compact"] .kpi { padding: var(--space-2); gap: var(--space-2) var(--space-3); }
html[data-density="compact"] .dv-list,
html[data-density="compact"] .days { gap: var(--space-2); }
html[data-density="compact"] .bento { gap: var(--space-2); margin-bottom: var(--space-2); }
html[data-density="compact"] .dash-grid { gap: var(--space-2); }
html[data-density="compact"] .strip-times { padding-top: var(--space-2); }
@media (max-width: 620px) {
    html[data-density="compact"] .cal-cell { min-height: 48px; }
}
/* Comfortable density overrides. Everything downstream reads these four
   tokens, so this is the only place a density change is needed.

   --fs-base: 14px is the one size in the file that is not a step on the type
   scale, and deliberately so: this is a density knob for the whole interface,
   not a component choosing its own size, and a density that nobody can see is
   not a density. It sits between --fs-base and --fs-md on purpose. */
[data-density="comfortable"] {
    --row-h: 36px;
    --gap: var(--space-5);
    --pad: var(--space-6);
    /* Deliberately raw, and the only exception here: 14px sits between --fs-base
       and --fs-md on purpose, which is what the comment above this block says. */
    --fs-base: 14px;
    --fs-meta: var(--fs-sm);
    --fs-label: var(--fs-xs);
}
html[data-density="comfortable"] .cal-cell { min-height: 118px; padding: var(--space-3); gap: var(--space-2); }
html[data-density="comfortable"] .rot { padding: var(--space-5) var(--space-6); margin-bottom: var(--space-5); }
html[data-density="comfortable"] .rot-head { padding-bottom: var(--space-4); margin-bottom: var(--space-4); }
html[data-density="comfortable"] .mtr { padding: var(--space-4) var(--space-4); }
html[data-density="comfortable"] .dash-card { padding: var(--space-6); }
html[data-density="comfortable"] .kpi { padding: var(--space-5); }
html[data-density="comfortable"] .dv-list,
html[data-density="comfortable"] .days { gap: var(--space-5); }
html[data-density="comfortable"] .bento { gap: var(--space-5); margin-bottom: var(--space-5); }
html[data-density="comfortable"] .month-label { font-size: 22px; }
html[data-density="comfortable"] .dash-title { font-size: var(--fs-sm); }
@media (max-width: 620px) {
    html[data-density="comfortable"] .cal-cell { min-height: 84px; }
}

/* ========================= 02 ┬À RESET & BASE ============================== */

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

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scrollbar-color: var(--text-faint) transparent;
}

body {
    font-family: var(--font);
    font-size: var(--fs-base);
    color: var(--text);
    /* Flat neutral ground: no aurora, no radial washes, no grain veil. */
    background: var(--bg);
    min-height: 100dvh;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    font-variant-numeric: var(--tnum);
    transition: background 0.35s ease, color 0.35s ease;
    overflow-x: hidden;
}

/* The film-grain veil is retired: it added a full-viewport overlay layer
   over an operational UI. */
body::after { display: none; }

img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { -webkit-tap-highlight-color: transparent; }
h1, h2, h3, h4 { font-weight: 800; letter-spacing: -0.01em; line-height: 1.2; }
b, strong { font-weight: 700; }
code { font-family: var(--mono); }

::selection {
    background: color-mix(in srgb, var(--accent) 22%, transparent);
    color: var(--text);
}

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--text-faint) 45%, transparent);
    border: 3px solid transparent;
    border-radius: var(--r-xl);
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background-color: var(--text-faint); }

:focus-visible {
    /* The shadow stays -- it is the soft halo the design wants -- but on its own
       it was 1.25:1 against --surface, where WCAG 2.4.11 and 1.4.11 ask for 3:1.
       A box-shadow is also not painted where an ancestor clips with
       overflow: hidden, and this file clips on .modal, .dash-card,
       .calendar-view, .day-card and .stat-card, so a focused control inside any
       of them showed nothing at all. Forced-colours mode drops box-shadows
       entirely, which left no focus indicator whatsoever on Windows.
       The outline is what makes the indicator real; --accent-ink measures
       6.65:1 dark and 6.47:1 light. */
    outline: 2px solid var(--accent-ink);
    outline-offset: 2px;
    box-shadow: var(--ring-soft);
    border-radius: var(--r-sm);
}

/* =========================== 03 ┬À UTILITIES =============================== */

.hidden { display: none !important; }

.skip-link {
    position: absolute;
    left: 50%;
    top: -60px;
    transform: translateX(-50%);
    z-index: 200;
    background: var(--card);
    color: var(--text);
    border: 1px solid var(--card-border-2);
    padding: var(--space-3) var(--space-5);
    border-radius: 0 0 var(--r-md) var(--r-md);
    font-weight: 700;
    font-size: var(--fs-base);
    text-decoration: none;
    box-shadow: var(--shadow-lg);
    transition: top var(--dur) var(--ease);
}
.skip-link:focus { top: 0; }

.spacer { flex: 1; }

/* ===================== 04 - HERO & APP BAR ============================== */
.hero {
    position: sticky;
    top: 0;
    z-index: 60;
    /* min-height, not height: on a phone the action row wraps to a second line
       and the bar has to grow with it. A fixed height plus vertical padding meant
       the bar painted past its own box and over the tab bar (z-index 60 against
       the tabs' 50), which is a bug that only shows up on the smallest screen. */
    min-height: var(--appbar-h);
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: 0 var(--space-5);
    /* Real glassmorphism, and it needs the blur *and* a saturate: a backdrop that
       is only desaturated reads as "faded", not as "behind glass". The tint is
       now built from the card surface rather than the old near-black, so the bar
       belongs to this palette instead of sitting on it like a grey film. */
    background: var(--glass);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border-bottom: 1px solid var(--card-border);
    box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.03) inset, var(--shadow-sm);
}

.hero-top {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex: 1 1 auto;
    min-width: 0;
}

.brand { display: flex; align-items: center; gap: var(--space-3); min-width: 0; flex: none; }

.brand-mark {
    position: relative;
    width: 30px;
    height: 30px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--r-md);
    background: linear-gradient(160deg,
                color-mix(in srgb, var(--sky) 26%, var(--card)),
                color-mix(in srgb, var(--sky) 8%, var(--card)));
    border: 1px solid color-mix(in srgb, var(--sky) 30%, transparent);
    color: var(--sky-ink);
    box-shadow: 0 2px 10px -4px color-mix(in srgb, var(--sky) 60%, transparent);
}
.brand-mark::after { display: none; }
.brand-mark svg { position: relative; z-index: 1; width: 16px; height: 16px; }

.brand-text { min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); }
.brand-text h1 {
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.2;
    background: none;
    -webkit-background-clip: initial;
    background-clip: initial;
    color: var(--text);
}
.brand-text span {
    display: block;
    font-size: var(--fs-meta);
    font-weight: 500;
    letter-spacing: 0.2px;
    text-transform: none;
    color: var(--text-faint);
    margin-top: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- Double clock ---------------------------------------------------------
   Zulu first in the DOM for reading order but local first on screen: the local
   clock is the one you check against a hotel shuttle, and Zulu is the one you
   quote. The light is the "these are live" tell.

   `.mono` on the <b> is load-bearing, not decoration: without tabular figures
   the seconds column changes width every tick and the whole pair shimmers once a
   second, which is exactly the frequency at which a human notices movement. */
.clock-pair {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    flex: none;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-lg);
    background: color-mix(in srgb, var(--input) 70%, transparent);
    border: 1px solid var(--card-border);
    position: relative;
}

.clock-live {
    width: 6px;
    height: 6px;
    flex: none;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 22%, transparent),
                0 0 8px color-mix(in srgb, var(--green) 70%, transparent);
    /* A slow breathe rather than a blink. A blink at 1Hz is a "something is
       wrong" signal in every interface that has ever used one; a 3.2s breathe
       reads as alive and stays under the threshold of annoyance. It stops under
       reduced motion -- see the single block at the end of this file. */
    animation: clock-breathe 3.2s var(--ease) infinite;
}

@keyframes clock-breathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

.clock-cell { display: flex; flex-direction: column; align-items: center; line-height: 1.15; }
.clock-cell b {
    font-size: var(--fs-md);
    font-weight: 700;
    letter-spacing: 0.2px;
    color: var(--text);
    font-variant-numeric: var(--tnum);
}
.clock-cell small {
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.7px;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-top: var(--space-1);
}
/* Zulu is the company's reference and the local clock is yours: local is the
   primary reading, so it gets the brighter ink and the Zulu pair recedes. */
.clock-cell.is-local b { color: var(--text); }
.clock-cell.is-utc b { color: var(--text-soft); font-size: var(--fs-base); }

.clock-sep {
    width: 1px;
    align-self: stretch;
    background: var(--card-border);
}

.hero-icons { display: flex; gap: var(--space-2); flex: none; align-items: center; }

/* Audit 2026-10-14 A11Y-04 : cibles tactiles 44 px (HIG cockpit) sans changer
   le visuel. WCAG 2.2 SC 2.5.8 (24 px) passe déjà ; `::after` étend la zone de
   pointage au-delà de la boîte peinte (30 px -> 44 px pour `.lang-btn`,
   40 px -> 54 px pour les `.icon-btn` d'en-tête). `position: relative` ne
   déplace rien (aucun enfant positionné) et `inset` n'est pas une propriété
   suivie par test_visual_tokens.py. */
.lang-btn,
.hero-icons .icon-btn {
    position: relative;
}
.lang-btn::after,
.hero-icons .icon-btn::after {
    content: '';
    position: absolute;
    inset: -7px;
}

.lang-btn {
    height: 30px;
    min-width: 38px;
    flex: none;
    padding: 0 var(--space-3);
    border-radius: var(--r-sm);
    cursor: pointer;
    color: var(--text-soft);
    /* Audit 2026-10-14 A11Y-04 : zone tactile 44 px sans changer le visuel
       (voir le bloc après .hero-icons : `::after` étend la cible, pas la
       boîte peinte). */
    font-size: var(--fs-meta);
    font-weight: 700;
    letter-spacing: 0.6px;
    background: transparent;
    border: 1px solid var(--card-border);
    transition: background var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}
.lang-btn:hover { background: var(--input); color: var(--text); border-color: var(--card-border-2); }
.lang-btn:active { transform: scale(0.96); }

/* Greeting demoted to a quiet inline note. */
.hero-sub {
    position: relative;
    z-index: 2;
    max-width: none;
    margin: 0;
    padding: 0 var(--space-3);
    border-left: 1px solid var(--card-border);
    font-size: var(--fs-meta);
    font-weight: 500;
    color: var(--text-faint);
    text-shadow: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hero-actions {
    position: relative;
    z-index: 2;
    max-width: none;
    margin: 0 0 0 auto;
    display: flex;
    gap: var(--space-2);
    flex-wrap: nowrap;
    flex: none;
}

/* --- Tab bar --------------------------------------------------------------
   The cursor is a sibling of the buttons, absolutely positioned and moved by
   transform. It is deliberately *not* a background on .tab-btn: a background can
   only be present or absent, so it cannot travel, and the brief asks for a moving
   active cursor rather than a lit tab.

   Width is set inline by moveTabCursor() from the measured button, and the
   transform is transitioned, so switching tabs slides it. The transition is
   suppressed on the very first placement (see .is-unplaced) to kill the fly-in. */
.tabs {
    position: sticky;
    top: var(--appbar-h);
    z-index: 50;
    display: flex;
    gap: var(--space-1);
    align-items: center;
    padding: 0 var(--space-5);
    background: var(--glass);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border-bottom: 1px solid var(--card-border);
    overflow-x: auto;
    scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab-cursor {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 0;
    border-radius: var(--r-full);
    background: linear-gradient(90deg,
                color-mix(in srgb, var(--sky) 20%, transparent),
                var(--sky),
                color-mix(in srgb, var(--sky) 20%, transparent));
    box-shadow: 0 0 12px color-mix(in srgb, var(--sky) 65%, transparent);
    transform: translateX(0);
    transition: transform var(--dur) var(--ease-out),
                width var(--dur) var(--ease-out),
                opacity var(--dur-fast) var(--ease);
    pointer-events: none;
    opacity: 0;
}
/* Placed but invisible until the first measurement, so it never animates in
   from x=0 on load. JS drops this class immediately after positioning. */
.tab-cursor.is-unplaced { opacity: 0; }
.tab-cursor.is-ready { opacity: 1; }

/* ============================ 05 ┬À BUTTONS ================================ */

.btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    border: none;
    cursor: pointer;
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: 0.1px;
    padding: var(--space-3) var(--space-5);
    border-radius: var(--r-lg);
    white-space: nowrap;
    isolation: isolate;
    transition:
        transform var(--dur-fast) var(--ease),
        box-shadow var(--dur) var(--ease),
        background var(--dur) var(--ease),
        border-color var(--dur) var(--ease),
        filter var(--dur) var(--ease);
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0) scale(0.97); }
.btn svg { width: 16px; height: 16px; flex: none; }
.btn:disabled { opacity: 0.55; cursor: progress; transform: none; }

.btn-light {
    background: linear-gradient(180deg, #ffffff, #eef3fb);
    color: var(--bg-grad-1);
    box-shadow: 0 8px 22px -8px rgba(0, 0, 0, 0.65), inset 0 1px 0 #fff;
}
.btn-light:hover { box-shadow: 0 14px 30px -10px rgba(0, 0, 0, 0.7), inset 0 1px 0 #fff; }

.btn-ghost {
    background: var(--glass-tint);
    color: #fff;
    border: 1px solid var(--glass-brd-dark);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
}
.btn-ghost:hover {
    background: rgba(255, 255, 255, 0.18);
    border-color: rgba(255, 255, 255, 0.4);
}

.btn-ghost-dark {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--card-border-2);
}
.btn-ghost-dark:hover { background: var(--gray-soft); }

.btn-accent {
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 82%, #ffffff) 0%,
                var(--accent) 55%,
                color-mix(in srgb, var(--accent) 88%, #000000) 100%);
    color: #fff;
    box-shadow:
        0 8px 20px -8px color-mix(in srgb, var(--accent) 70%, transparent),
        inset 0 1px 0 rgba(255, 255, 255, 0.28);
}
.btn-accent:hover {
    box-shadow:
        0 14px 30px -10px color-mix(in srgb, var(--accent) 80%, transparent),
        inset 0 1px 0 rgba(255, 255, 255, 0.32);
}

.btn-danger {
    background: transparent;
    color: var(--danger);
    border: 1px solid color-mix(in srgb, var(--danger) 55%, transparent);
}
.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }

.btn-soft {
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--card-border);
    box-shadow: var(--shadow-xs);
}
.btn-soft:hover {
    background: var(--card);
    border-color: var(--card-border-2);
    box-shadow: var(--shadow-sm);
}

.icon-btn {
    width: 40px;
    height: 40px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--r-lg);
    cursor: pointer;
    color: #fff;
    background: var(--glass-tint);
    border: 1px solid var(--glass-brd-dark);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    transition: background var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
}
.icon-btn:hover {
    background: rgba(255, 255, 255, 0.2);
    border-color: rgba(255, 255, 255, 0.42);
    transform: translateY(-2px);
    box-shadow: 0 8px 18px -8px rgba(0, 0, 0, 0.6);
}
.icon-btn:active { transform: scale(0.94); }
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn.round { border-radius: 50%; }

/* variant used inside light surfaces (month navigation) */
.month-nav .icon-btn {
    color: var(--text);
    background: var(--card);
    border-color: var(--card-border);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: var(--shadow-sm);
}
.month-nav .icon-btn:hover {
    color: var(--accent-ink);
    background: var(--card);
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    box-shadow: var(--shadow-md);
}

.icon-sun { display: none; }
[data-theme="dark"] .icon-moon { display: none; }
[data-theme="dark"] .icon-sun { display: block; }

#dark-toggle svg { transition: transform 0.4s var(--ease-spring), opacity var(--dur) var(--ease); }
#dark-toggle:hover svg { transform: rotate(35deg) scale(1.08); }

/* ============================== 06 - TABS ================================= */
.tab-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    font-size: var(--fs-base);
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text-faint);
    padding: 0 var(--space-4);
    height: 34px;
    border-radius: var(--r-sm);
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--dur) var(--ease),
                background var(--dur) var(--ease);
}
.tab-btn svg { width: 16px; height: 16px; opacity: 0.7; transition: opacity var(--dur) var(--ease); }
.tab-btn:hover { color: var(--text); background: color-mix(in srgb, var(--input) 60%, transparent); }
.tab-btn:hover svg { opacity: 1; }
.tab-btn:active { transform: translateY(1px); }

/* Active: brighter ink and a full-strength icon. No fill, no border, no
   gradient -- the cursor owns the "you are here" signal, and a second one
   competes with it instead of reinforcing it. */
.tab-btn.active { color: var(--text); }
.tab-btn.active svg { opacity: 1; color: var(--sky-ink); }

/* ======================= 07 ┬À LAYOUT & PILLS ============================= */

main {
    position: relative;
    z-index: 2;
    max-width: var(--shell);
    margin: 0 auto;
    padding: var(--gap) var(--space-6) var(--space-9);
}

.pane { display: none; }
/* No entrance animation: it delays the data the user came to read. */
.pane.active { display: block; }

@keyframes rise {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

/* --- status pills ------------------------------------------------------- */

.status-pill-row {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
    align-items: center;
    font-size: var(--fs-sm);
    color: var(--text-soft);
    margin-bottom: var(--space-5);
}

.status-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    background: var(--card);
    border: 1px solid var(--card-border);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-xl);
    box-shadow: var(--shadow-sm);
    font-weight: 600;
    text-transform: capitalize;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
.status-pill.muted { color: var(--text-faint); font-weight: 500; text-transform: none; }

.status-pill .dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 22%, transparent);
    animation: breathe 2.6s var(--ease) infinite;
}
.status-pill.error { color: var(--error); }
/* The dot carries the vivid #ef4444; the text beside it carries --danger, which
   is the same hue lightened until it clears 4.5:1. A status point is a fill and
   is never read as small text, so it is allowed the stronger colour. */
.status-pill.error .dot { background: var(--danger-fill); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger-fill) 22%, transparent); }
.status-pill.offline .dot { background: var(--amber); box-shadow: 0 0 0 3px color-mix(in srgb, var(--amber) 24%, transparent); }

/* --- month navigation --------------------------------------------------- */

.month-nav {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin: var(--space-3) 0 var(--space-4);
    justify-content: center;
    position: relative;
}
.month-info { min-width: 190px; text-align: center; }
/* The Trips pairing count sits inside the month navigation rather than on a row
   of its own below it. `.month-nav` centres its children, so the pill is taken
   out of that flow and pinned to the right edge with an absolute offset -- the
   same trick `.month-info`'s `position: relative` allows, and no extra wrapper.
   It is a count of the month being shown, so it belongs at the month's eye level
   and not a band lower, where it read as an unrelated status. */
.trip-count-pill {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
}
@media (max-width: 620px) {
    /* Too narrow for a third column beside a 190px title; it goes under it. */
    .trip-count-pill {
        position: static;
        transform: none;
        align-self: center;
    }
}
.month-label {
    font-size: var(--fs-xl);
    font-weight: 800;
    letter-spacing: 0.1px;
}
.today-link {
    display: inline-block;
    margin-top: var(--space-1);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.3px;
    text-transform: uppercase;
    color: var(--sky);
    cursor: pointer;
    text-decoration: none;
    padding: var(--space-0) var(--space-3);
    border-radius: var(--r-xl);
    transition: background var(--dur-fast) var(--ease);
    /* M-21. This element was an `<a>` and is now a `<button>`, because an anchor
       without `href` is not focusable and cannot be activated from the keyboard, so
       "jump to today" was listed in the shortcuts table and unreachable by anyone
       not using a mouse (index.html:313).

       A button brings the UA's own chrome with it -- a background, a border, and a
       default font that ignores the sizes above -- so those are reset here. Kept on
       the same rule rather than in a shared `button {}` reset because this is the
       only element in the app styled as a link rather than as a control, and a
       blanket reset would touch every other button in the interface. `font-family:
       inherit` is the one that would otherwise be silently wrong, since the rules
       above set size and weight but never the family. */
    background: none;
    border: 0;
    font-family: inherit;
    line-height: inherit;
}
.today-link:hover { background: var(--sky-soft); text-decoration: none; }
.today-link:focus-visible {
    /* A focus ring, because the whole point of the element change was that this can
       now be reached by keyboard. `:focus-visible` so a mouse click does not paint
       one, which is what that pseudo-class is for. */
    outline: 2px solid var(--sky);
    outline-offset: 2px;
}

.month-nav input[type="month"],
.month-nav input[type="date"] {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

/* --- jump-to-month picker -------------------------------------------------
   Sits beside the view switch rather than inside `.month-nav`, because the
   arrows and the title already fill that row and a third control there would
   crowd it. */
.view-switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.month-pick {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-xs);
    font-weight: 700;
}
.month-pick-label {
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    white-space: nowrap;
}
.month-pick-select {
    font: inherit;
    font-weight: 700;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-2) var(--space-3);
    min-height: 32px;
    cursor: pointer;
}
.month-pick-select:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.month-pick-select:disabled { opacity: 0.55; cursor: default; }

/* --- segmented control -------------------------------------------------- */

.view-switch {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-2);
    margin: var(--space-3) 0 var(--space-4);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-xl);
    box-shadow: var(--shadow-sm);
    max-width: 460px;
    margin-inline: auto;
}

.seg {
    flex: 1;
    background: transparent;
    border: 1px solid transparent;
    font-size: var(--fs-sm);
    font-weight: 700;
    letter-spacing: 0.2px;
    color: var(--text-soft);
    padding: var(--space-3) var(--space-2);
    border-radius: var(--r-lg);
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--dur) var(--ease),
                background var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease),
                border-color var(--dur) var(--ease);
}
.seg:hover { color: var(--text); background: var(--gray-soft); }

.seg[aria-pressed="true"] {
    color: #fff;
    background: linear-gradient(180deg, var(--bg-grad-2), var(--bg-grad-1));
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    box-shadow: 0 8px 18px -8px rgba(7, 20, 38, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
[data-theme="dark"] .seg[aria-pressed="true"] {
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 90%, #ffffff),
                color-mix(in srgb, var(--accent) 80%, #000000));
    box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--accent) 80%, transparent),
                inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

.view-switch.small { max-width: 290px; margin: 0; }

/* --- filter chips ------------------------------------------------------- */

.chips {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin: 0 0 var(--space-5);
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    border: 1px solid var(--card-border);
    background: var(--card);
    color: var(--text-soft);
    font-size: var(--fs-sm);
    font-weight: 700;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-xl);
    cursor: pointer;
    box-shadow: var(--shadow-xs);
    transition: background var(--dur) var(--ease),
                color var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                transform var(--dur-fast) var(--ease),
                box-shadow var(--dur) var(--ease);
}
.chip:hover {
    transform: translateY(-2px);
    color: var(--text);
    box-shadow: var(--shadow-md);
}
.chip:active { transform: scale(0.96); }
.chip .dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 18%, transparent);
}

.chip[aria-pressed="true"] {
    color: #fff;
    background: linear-gradient(180deg, var(--bg-grad-2), var(--bg-grad-1));
    border-color: transparent;
    box-shadow: 0 8px 18px -8px rgba(7, 20, 38, 0.8);
}
[data-theme="dark"] .chip[aria-pressed="true"] {
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 90%, #ffffff),
                color-mix(in srgb, var(--accent) 78%, #000000));
    box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--accent) 80%, transparent);
}
.chip[aria-pressed="true"] .dot { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); }

/* =========================== 08 ┬À STAT TILES ============================= */

.stats,
.stats-cards {
    display: grid;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
.stats { grid-template-columns: repeat(6, 1fr); }
.stats-cards { grid-template-columns: repeat(5, 1fr); }

.stat-card {
    --tile: var(--sky);
    position: relative;
    overflow: hidden;
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-lg);
    padding: var(--space-5) var(--space-3) var(--space-4);
    text-align: center;
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur) var(--ease-spring), box-shadow var(--dur) var(--ease);
}
.stat-card::before {
    content: "";
    position: absolute;
    top: 0;
    left: 14%;
    right: 14%;
    height: 3px;
    border-radius: 0 0 var(--r-xs) var(--r-xs);
    background: linear-gradient(90deg, transparent, var(--tile), transparent);
    box-shadow: 0 0 10px -1px var(--tile);
}
.stat-card::after {
    content: "";
    position: absolute;
    inset: -40% -20% auto;
    height: 130px;
    background: radial-gradient(60% 100% at 50% 0%, color-mix(in srgb, var(--tile) 16%, transparent), transparent 70%);
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
}
.stat-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
}
.stat-card:hover::after { opacity: 1; }

.stat-num {
    position: relative;
    font-size: var(--fs-2xl);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
    background: linear-gradient(180deg, var(--text) 30%, color-mix(in srgb, var(--tile) 78%, var(--text)) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.stat-label {
    position: relative;
    margin-top: var(--space-2);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.7px;
    color: var(--text-faint);
}

/* tile accents follow the KPI order used by app.js */
.stat-card:nth-child(1) { --tile: var(--accent); }
.stat-card:nth-child(2) { --tile: var(--sky); }
.stat-card:nth-child(3) { --tile: var(--green); }
.stat-card:nth-child(4) { --tile: var(--amber); }
.stat-card:nth-child(5) { --tile: var(--purple); }
.stat-card:nth-child(6) { --tile: var(--sky); }

/* ============================ 09 ┬À CALENDAR =============================== */

#cal-content { display: block; }
#cal-content.clear { display: block; }

.calendar-view,
.week-view {
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-xl);
    box-shadow: var(--shadow-md);
    overflow: hidden;
    margin-bottom: var(--space-5);
}

/* minmax(0, 1fr), not 1fr.

   `1fr` is `minmax(auto, 1fr)`, and `auto` is the track's *minimum*, so the
   track could not shrink below the widest thing inside a cell. `.cal-evt` is
   `white-space: nowrap` with `text-overflow: ellipsis`, which only works if the
   cell is *allowed* to be narrower than the chip -- so the chip's min-content
   was setting the track width instead of the column.

   Measured at a 390px viewport: the grid laid out at 609px inside a 366px
   `.calendar-view`, and that container is `overflow: hidden`. The month ran off
   the right edge with the whole SUN column unreachable, and the tracks were not
   even equal -- one came out at 22px next to five at ~98px. `minmax(0, 1fr)`
   makes the seven columns equal and lets `.cal-evt`'s own ellipsis do the
   truncating, which is what it was written to do. */
.cal-grid,
.cal-dow,
.week-cols { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }

.cal-dow {
    background: linear-gradient(180deg, var(--surface-2), var(--gray-soft));
    border-bottom: 1px solid var(--card-border);
    position: relative;
}
.cal-dow span {
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.9px;
    color: var(--text-faint);
    text-align: center;
    padding: var(--space-3) 0;
}

.cal-cell {
    position: relative;
    /* Audit 2026-10-14 UI-05 : hauteur fluide au lieu de 100 px figés -- la
       grille mois respire sur petit écran (64 px à 390 px de large) sans
       bouger sur bureau (plafond 100 px, inchangé à 1280). */
    min-height: clamp(64px, 12vw, 100px);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    cursor: pointer;
    border-right: 1px solid var(--card-border);
    border-bottom: 1px solid var(--card-border);
    background: var(--card);
    transition: background var(--dur-fast) var(--ease);
}
.cal-cell:nth-child(7n) { border-right: none; }
.cal-grid > .cal-cell:nth-last-child(-n + 7) { border-bottom: none; }

.cal-cell:hover { background: var(--surface-2); }
/* A crossing chip paints into the next cell, and a child's z-index does not rise
   above a SIBLING cell once that cell makes a stacking context -- so the whole
   departure CELL is raised. `.cal-lay` is what it was painting over. */
.cal-cell:has(.cal-cross) { z-index: 2; } .cal-cell:has(.cal-lane) { z-index: 1; }
.cal-cell:hover::before {
    content: "";
    position: absolute;
    inset: 0;
    border: 2px solid color-mix(in srgb, var(--accent) 40%, transparent);
    border-radius: var(--r-xs);
    pointer-events: none;
    z-index: 2;
}

.cal-cell.out {
    background: color-mix(in srgb, var(--gray-soft) 55%, var(--card));
}
.cal-cell.out .cal-num { color: var(--text-faint); font-weight: 500; opacity: 0.55; }
/* weekend is marked by a tinted band + heavier weight, not hue alone,
   so it survives greyscale, colour-blindness and accent changes */
.cal-cell.weekend { background: color-mix(in srgb, var(--text-faint) 7%, var(--card)); }
.cal-cell.weekend .cal-num { color: var(--text); font-weight: 800; }
.cal-cell.weekend::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: color-mix(in srgb, var(--text-faint) 40%, transparent);
    pointer-events: none;
}

.cal-top { display: flex; align-items: center; gap: var(--space-2); }
.cal-num {
    font-size: var(--fs-sm);
    font-weight: 700;
    color: var(--text-soft);
}

.cal-cell.today {
    background:
        radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%),
        var(--card);
}
.cal-cell.today .cal-num {
    display: inline-grid;
    place-items: center;
    min-width: 24px;
    height: 24px;
    line-height: 1;
    font-size: var(--fs-sm);
    color: #fff;
    background: linear-gradient(160deg,
                color-mix(in srgb, var(--accent) 78%, #ffffff), var(--accent));
    border-radius: var(--r-md);
    box-shadow: 0 4px 12px -3px color-mix(in srgb, var(--accent) 75%, transparent);
}

.cal-events { display: flex; flex-direction: column; gap: var(--space-1); overflow: hidden; }
/* Below 900px a 101px cell cannot hold the number AND the cities on L1. */
@media (max-width: 900px) {
    .cal-evt-l1 b, .cal-evt-l1 .cal-evt-flt { display: none; }
    .cal-evt .cal-route, .cal-lane .cal-route { font-size: var(--fs-2xs); }
}

/* A day off, as a bar at the foot of the cell rather than a pill.
   This is the single biggest readability change in the calendar: measured on
   October 2026, 15 of the 31 chips in the month were "OFF", so half the ink was
   marking days with no work in them. The bar is deliberately the quietest mark
   in the cell -- 2px, at 22% -- so the flights are what the eye lands on. It is
   aria-hidden because the "Days off" stat above the grid already states the count
   in words, and a screen reader should not hear "off" sixteen times. */
.cal-offbar {
    margin-top: auto;
    height: 2px;
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--off-ink) 22%, transparent);
    flex: none;
}

/* colour + background are set inline by app.js; we own the shape */
/* MOCKUP B15 — two lines, one grammar for every duty. L1: flight number, then the
   cities of the whole route. L2: departure at the LEFT end, arrival at the RIGHT.
   A sector landing the next day is not a different chip but the same chip made
   wider than its cell, so L2's right end falls in the following day -- and it
   measures the same height as every other chip, which is the whole point. */
.cal-evt, .cal-lane {
    /* `--ink`/`--soft` live on the CHIP: on a modifier-only branch they made
       `background: <gradient>, var(--soft)` invalid and the chip lost its
       background without a word. */
    --ink: var(--flight-ink, var(--sky));
    --soft: color-mix(in srgb, var(--ink) 15%, var(--card));
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--fs-2xs);
    font-weight: 700;
    line-height: 1.35;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--r-sm);
    border-left: 2.5px solid var(--ink);
    background: linear-gradient(180deg, color-mix(in srgb, #ffffff 6%, transparent), transparent 55%), var(--soft);
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    min-width: 0;
    max-width: 100%;
    cursor: pointer;
    transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}
/* A duty REPORTED today whose sector leaves later. Amber, dashed, no aircraft
   icon: there is no flight on this day, there is a start. As a chip it asserted
   a departure that has not happened -- 2 September showed a 05:55 departure on
   a day whose last event was a debrief the evening before. */
.cal-starts { display: flex; flex-direction: column; gap: var(--space-1); }
.cal-start {
    display: block; font: var(--fs-2xs)/1.35 var(--mono); color: var(--warning);
    background: color-mix(in srgb, var(--warning) 8%, var(--card)); border: 1px dashed color-mix(in srgb, var(--warning) 38%, transparent);
    border-radius: var(--r-xs); padding: var(--space-1) var(--space-2); }
.cal-start-ar { font-weight: 700; margin-right: var(--space-1); }
.cal-start small { display: block; color: var(--text-faint); font-size: var(--fs-3xs); }

/* The route is the one token the ellipsis may eat: the number is what a crew
   member quotes, so it sits outside the overflowing box. */
.cal-evt .chip-txt, .cal-lane .chip-txt {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* ONE blue for every flight: out of base was a second literal blue. Now a GLYPH. */
.cal-evt.t-flight { --flight-ink: var(--sky); }
.cal-obmark { font-style: normal; color: var(--text-soft); flex: none; }
/* min-height, not height: pins the row against a font fallback, still lets it grow. */
.cal-evt-l1, .cal-evt-l2 { display: flex; align-items: baseline; gap: var(--space-1); min-width: 0; }
.cal-evt-l1 { min-height: 18px; }
.cal-evt-l1 b, .cal-evt-l1 .cal-evt-flt { font-family: var(--mono); color: var(--text); flex: none; }
/* The CITIES. They were 10px mono in a 101px column, and they are what says where
   the day goes -- so they get the chip's own size, which is the last thing that
   should need a magnifier. */
.cal-evt .cal-route { font-family: var(--mono); font-size: var(--fs-xs); font-weight: 600; color: var(--ink); }
.cal-tarr { margin-left: auto; color: var(--text-soft); font-weight: 600; flex: none; }
.cal-tarr em { font-style: normal; font-size: var(--fs-3xs); color: var(--text-faint); margin-left: var(--space-1); }
/* The narrow-width marker: above 620px the rectangle and `+1j` already say it, and
   a third statement made the 11th of October read `17:09 +1j 06:22 +1j`. */
.cal-xmark { display: none; font-style: normal; color: var(--sky-ink); font-weight: 700; }
/* A crossing that could not reach -- Sunday into the Monday below, or the last day of
   the month -- keeps its arrival in its own cell, so `+1j` is all that is left saying it
   lands tomorrow. Below 620px that marker was already the fallback. */
.cal-evt.cal-cross-inline .cal-xmark { display: inline; }
/* How far into the NEXT cell the chip reaches, as a fraction of that cell.
   Written out rather than guessed: percentages resolve against the CONTENT box,
   so `100% + 74%` overshoots into a second full column and `100% + 50%`
   undershoots, drawing the arrival on the wrong day. */
/* The chip's own `max-width: 100%` is right for a chip, fatal for this one.
   No `z-index` here: the CELL is what gets raised (`.cal-cell:has(.cal-cross)`),
   because a child's z-index cannot rise above a sibling cell that makes a
   stacking context. Both were here once and neither did anything. */
.cal-evt.cal-cross {
    max-width: none;
    --reach: 0.74;
    width: calc((1 + var(--reach)) * 100% + var(--space-2) * (1 + 2 * var(--reach)));
}
/* The reserved lane: it shares the chip's rules, so it has the chip's box. */
.cal-lane { visibility: hidden; }
/* The station-local departure sits under the route rather than trailing it after
   a middot: `ALG→ORN 11:30` makes the reader work out that the last token is a
   time, and at cell width the pair stops fitting. */
.cal-evt-time { color: var(--text-soft); font-variant-numeric: var(--tnum); flex: none; }
.cal-evt:hover {
    transform: translateX(2px) scale(1.02);
    filter: saturate(1.15) brightness(1.02);
    box-shadow: 0 4px 10px -4px rgba(11, 25, 45, 0.4);
}
.cal-evt .flag { width: 5px; height: 5px; flex: none; border-radius: 50%; box-shadow: 0 0 5px currentColor; }

.cal-more { font-size: var(--fs-2xs); font-weight: 700; color: var(--text-faint); padding-left: var(--space-2); }

/* ===================== 10 ┬À WEEK / DAY / LIST VIEWS ====================== */

.wk-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-0);
    padding: var(--space-3) var(--space-1);
    background: linear-gradient(180deg, var(--surface-2), var(--gray-soft));
    border-bottom: 1px solid var(--card-border);
}
.wk-head .dow {
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.7px;
    color: var(--text-faint);
}
.wk-head .dnum { font-size: var(--fs-lg); font-weight: 800; }
/* weekend header carries the same non-hue cue as the month grid */
.wk-head.weekend { background: linear-gradient(180deg,
            color-mix(in srgb, var(--text-faint) 10%, var(--surface-2)), var(--gray-soft)); }
.wk-head.weekend .dow { color: var(--text-soft); }

/* weekend day card: dashed rail + tinted ground, readable without colour */
.day-card.weekend { background: color-mix(in srgb, var(--text-faint) 5%, var(--card)); }
.day-card.weekend .day-rail { border-right-style: dashed; }
.wk-head.today { background: linear-gradient(180deg, var(--accent-soft), var(--gray-soft)); }
/* Video audit §4: week pills use the full column width on two lines --
   flight + route, then hours -- instead of truncating to "AH462 1...". */
.week-cols .cal-evt.wk-evt {
    flex-direction: column;
    align-items: stretch;
    gap: var(--hairline);
    width: 100%;
    padding: var(--space-1) var(--space-2);
    line-height: 1.25;
}
.week-cols .wk-line1 {
    display: flex;
    align-items: baseline;
    gap: var(--space-1);
    min-width: 0;
    overflow: hidden;
}
.week-cols .wk-flt { font-weight: 800; flex: none; }
.week-cols .wk-route {
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
.week-cols .wk-time {
    font-size: var(--fs-3xs);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.wk-head.today .dnum {
    display: inline-grid;
    place-items: center;
    min-width: 28px;
    height: 28px;
    color: #fff;
    background: linear-gradient(160deg,
                color-mix(in srgb, var(--accent) 78%, #ffffff), var(--accent));
    border-radius: var(--r-md);
    box-shadow: 0 4px 12px -3px color-mix(in srgb, var(--accent) 75%, transparent);
}

.wk-col {
    min-width: 0;
    min-height: 128px;
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    border-right: 1px solid var(--card-border);
    transition: background var(--dur-fast) var(--ease);
}
.wk-col:last-child { border-right: none; }
.wk-col:hover { background: var(--surface-2); }
.wk-col .empty-tip {
    font-size: var(--fs-xs);
    color: var(--text-faint);
    opacity: 0.5;
    padding: var(--space-1);
}

.day-view { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-5); }

.dv-datehead {
    position: relative;
    overflow: hidden;
    text-align: center;
    font-size: var(--fs-base);
    font-weight: 800;
    letter-spacing: 0.2px;
    color: var(--text);
    padding: var(--space-4);
    background:
        radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--sky) 14%, transparent), transparent 62%),
        var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-sm);
}
.dv-list,
.days { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-5); }

/* ========================== 11 ┬À DAY CARDS =============================== */

/* density is data-driven: [data-density="compact"] rescales the metrics that
   decide how much of a month fits on a phone. Padding/radius are halved,
   the rail narrows, and type loses one step ÔÇö never below legibility. */
.day-card {
    --dc-pad-y: 15px;
    --dc-pad-x: 16px;
    --dc-gap: 15px;
    --dc-radius: var(--r-xl);
    --dc-rail: 50px;
    position: relative;
    display: flex;
    gap: var(--dc-gap);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--dc-radius);
    padding: var(--dc-pad-y) var(--dc-pad-x);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    animation: rise 0.3s var(--ease-out) both;
    transition: transform var(--dur) var(--ease-spring),
                box-shadow var(--dur) var(--ease),
                border-color var(--dur) var(--ease);
}
[data-density="compact"] .day-card {
    --dc-pad-y: 10px;
    --dc-pad-x: 12px;
    --dc-gap: 11px;
    --dc-radius: var(--r-md);
    --dc-rail: 42px;
}
.day-card::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 4px;
    background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 35%, var(--sky)));
    opacity: 0.85;
}
.day-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-lg);
    border-color: var(--card-border-2);
}

.day-rail {
    flex: none;
    min-width: var(--dc-rail, 50px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-0);
    padding-right: var(--space-4);
    border-right: 1px solid var(--card-border);
    position: relative;
}
.day-num { font-size: var(--fs-2xl); font-weight: 800; line-height: 1; letter-spacing: -0.03em; }
.day-dow {
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-faint);
}
[data-density="compact"] .day-num { font-size: var(--fs-xl); }
[data-density="compact"] .day-rail { padding-right: var(--space-3); }
[data-density="compact"] .duty { margin-bottom: var(--space-2); gap: var(--space-3); }
[data-density="compact"] .duty-badge { padding: var(--space-1) var(--space-3); font-size: var(--fs-3xs); }
[data-density="compact"] .flight-num { font-size: var(--fs-md); }
[data-density="compact"] .meta-val { font-size: var(--fs-sm); }
[data-density="compact"] .days,
[data-density="compact"] .dv-list { gap: var(--space-3); }
[data-density="compact"] .duty-meta { gap: var(--space-2) var(--space-5); }

.day-body { flex: 1; min-width: 0; }
.duty { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); flex-wrap: wrap; }

.duty-badge {
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.8px;
    text-transform: uppercase;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-sm);
    border: 1px solid transparent;
    white-space: nowrap;
}

.status-flag {
    font-size: var(--fs-3xs);
    font-weight: 800;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-sm);
}

.flag-new { background: var(--green-soft); color: var(--green-ink); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--green) 30%, transparent); }
.flag-changed { background: var(--amber-soft); color: var(--amber-ink); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--amber) 34%, transparent); }

/* Deadhead: a repositioning leg, so it must not read as a passenger duty.
   Sky tint + a dashed edge, distinct from the NEW/CHANGED flags. */
.flag-deadhead {
    background: var(--sky-soft);
    color: var(--sky-ink);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sky) 34%, transparent);
}

/* Delay: signed, so colour is a redundant cue, not the only one */
.flag-delay {
    background: var(--amber-soft);
    color: var(--amber-ink);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--amber) 34%, transparent);
    font-variant-numeric: var(--tnum);
}
.flag-delay.late-major {
    background: var(--danger-soft);
    color: var(--danger);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 38%, transparent);
}

/* operated flight number, shown next to the roster one when they differ */
.op-flt {
    display: inline-block;
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.4px;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-sm);
    color: var(--text-soft);
    background: var(--gray-soft);
    box-shadow: inset 0 0 0 1px var(--card-border-2);
    white-space: nowrap;
}
.detail-hero .op-flt { font-size: var(--fs-sm); padding: var(--space-2) var(--space-3); }
.duty-flag { font-size: var(--fs-3xs); font-weight: 800; letter-spacing: 0.5px;
    text-transform: uppercase; padding: var(--space-1) var(--space-3); border-radius: var(--r-sm); white-space: nowrap; }

.badge-flight {
    background: var(--flight-soft);
    color: var(--flight-ink);
    box-shadow: inset 0 0 0 1px var(--flight-line);
}
.badge-hotel {
    background: var(--hotel-soft);
    color: var(--hotel-ink);
    box-shadow: inset 0 0 0 1px var(--hotel-line);
}
.badge-off {
    background: var(--off-soft);
    color: var(--off-ink);
    box-shadow: inset 0 0 0 1px var(--off-line);
}
.badge-standby {
    background: var(--standby-soft);
    color: var(--standby-ink);
    box-shadow: inset 0 0 0 1px var(--standby-line);
}
.badge-training {
    background: var(--training-soft);
    color: var(--training-ink);
    box-shadow: inset 0 0 0 1px var(--training-line);
}
.badge-reserve {
    background: var(--reserve-soft);
    color: var(--reserve-ink);
    box-shadow: inset 0 0 0 1px var(--reserve-line);
}
.badge-other {
    background: var(--off-soft);
    color: var(--off-ink);
    box-shadow: inset 0 0 0 1px var(--off-line);
}
/* Deadhead: the dashed border is the signal, so it reads as "not a revenue
   leg" without spending a colour on it. */
.badge-deadhead {
    background: var(--off-soft);
    color: var(--off-ink);
    border: 1px dashed var(--text-faint);
    box-shadow: none;
}

.duty-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-6);
}
.meta-item { display: flex; gap: var(--space-3); align-items: baseline; }
.meta-key {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.7px;
    color: var(--text-faint);
}
.meta-val { font-size: var(--fs-base); font-weight: 600; color: var(--text-soft); }

.day-actions { display: flex; gap: var(--space-2); align-self: center; }
/* Le carré 33px est la taille des boutons icônes (détail/édition/suppression,
   qui ne portent qu'un svg). #day-add est un .btn texte dans la même rangée :
   écrasé dans le carré, son libellé débordait à gauche du tiroir. */
.day-actions .mini:not(.btn) {
    width: 33px;
    height: 33px;
    display: grid;
    place-items: center;
    border-radius: var(--r-md);
    cursor: pointer;
    color: var(--text-soft);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
    transition: transform var(--dur-fast) var(--ease-spring),
                color var(--dur-fast) var(--ease),
                background var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}
.day-actions .mini:hover {
    transform: scale(1.09);
    color: var(--accent-ink);
    background: var(--card);
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    box-shadow: var(--shadow-sm);
}
.day-actions .mini.danger:hover {
    color: var(--accent-ink);
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    background: var(--accent-soft);
}
.day-actions .mini svg { width: 16px; height: 16px; }

/* =========================== 12 ┬À DASHBOARD ============================== */

/* "Saved locally, not yet on disk" (QA 360 C1). Deliberately NOT a toast: a
   toast is right for "it happened", this is "it did not, and here is the
   difference between what you see and what is in the file" -- so it survives
   until dealt with, above every pane, since the edit could be from any of them.
   Warning ink rather than danger: nothing was lost, and a red banner for a
   refused write teaches people to ignore red banners. */
#save-banner:not(.hidden) { margin-bottom: var(--space-4); }
.save-banner {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: var(--space-4) var(--space-5);
    border-radius: var(--r-lg);
    background:
        radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--warning) 12%, transparent), transparent 62%),
        var(--card);
    border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    box-shadow: var(--shadow-sm);
    animation: rise 0.3s var(--ease-out) both;
}
.save-banner .dicon { flex: none; display: grid; place-items: center; color: var(--warning); }
.save-banner .dicon svg { width: 22px; height: 22px; }
.save-banner-text { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.save-banner-text b { font-size: var(--fs-base); color: var(--text); }
.save-banner-detail {
    font-size: var(--fs-sm, 0.8125rem);
    color: var(--text-soft);
    line-height: 1.5;
    /* The server's own error text is an English string; it can be long and it
       can contain anything it liked to put in `error`. `esc()` already ran. */
    overflow-wrap: anywhere;
}
.save-banner-retry { flex: none; }
[data-density="compact"] .save-banner { padding: var(--space-3) var(--space-4); }
@media (max-width: 620px) {
    .save-banner { padding: var(--space-4); }
    .save-banner-retry { flex: 1 1 auto; }
}

/* Erased state: the dashboard explains it and offers the roster back. */
.wiped-banner {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: var(--space-4) var(--space-5);
    margin-bottom: var(--space-4);
    border-radius: var(--r-lg);
    background:
        radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--danger) 10%, transparent), transparent 62%),
        var(--card);
    border: 1px solid color-mix(in srgb, var(--danger) 34%, transparent);
    box-shadow: var(--shadow-sm);
    animation: rise 0.3s var(--ease-out) both;
}
.wiped-text { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.wiped-text b { font-size: var(--fs-base); color: var(--danger); }
.wiped-text span { font-size: var(--fs-base); color: var(--text-soft); line-height: 1.5; }
.wiped-restore { flex: none; }

/* The window the alert centre is looking at, and the alerts outside it
   (QA 360). The row is dimmed rather than hidden -- the finding is often the
   reason to fetch the month -- and the flag says so in words. */
.alert-window { margin: 0 0 var(--space-3); }
.alert-row.is-outside { opacity: .78; }
.alert-flag.is-outside {
    border-color: color-mix(in srgb, var(--warning) 45%, transparent);
    color: var(--warning);
    background: color-mix(in srgb, var(--warning) 12%, transparent);
}
[data-density="compact"] .wiped-banner { padding: var(--space-4) var(--space-4); margin-bottom: var(--space-3); }
@media (max-width: 620px) {
    .wiped-banner { padding: var(--space-4) var(--space-4); }
    .wiped-restore { flex: 1 1 auto; }
}

.dash-live-banner {
    --live-color: var(--sky);
    --live-soft: var(--sky-soft);
    position: relative;
    overflow: hidden;
    background:
        radial-gradient(120% 130% at 100% 0%, color-mix(in srgb, var(--live-color) 12%, transparent), transparent 60%),
        var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-xl);
    padding: var(--space-5) var(--space-6) var(--space-5) var(--space-6);
    box-shadow: var(--shadow-md);
    margin-bottom: var(--space-4);
    cursor: pointer;
    animation: rise 0.3s var(--ease-out) both;
    transition: transform var(--dur) var(--ease-spring),
                box-shadow var(--dur) var(--ease),
                border-color var(--dur) var(--ease);
}
.dash-live-banner::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 5px;
    background: linear-gradient(180deg, var(--live-color), color-mix(in srgb, var(--live-color) 30%, transparent));
    box-shadow: 0 0 18px -2px var(--live-color);
}
.dash-live-banner:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-lg);
    border-color: color-mix(in srgb, var(--live-color) 40%, transparent);
}

.live-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-3);
    flex-wrap: wrap;
}
.live-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-xl);
    background: var(--live-soft);
    color: var(--live-color);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--live-color) 26%, transparent);
}
.live-pulse-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    animation: livePulse 1.9s var(--ease) infinite;
}
.live-title {
    font-size: var(--fs-xl);
    font-weight: 800;
    letter-spacing: 0.1px;
    line-height: 1.25;
}
.live-sub { margin-top: var(--space-2); font-size: var(--fs-base); color: var(--text-soft); }

.live-bar-wrap { margin-top: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
.live-bar-track {
    height: 9px;
    border-radius: var(--r-xl);
    background: var(--gray-soft);
    overflow: hidden;
    box-shadow: inset 0 1px 2px rgba(11, 25, 45, 0.12);
}
/* SEC-17: the fill is an `<svg class="live-bar-fill">` whose width is a geometry
   attribute, not a style. The class stayed on the sized element so the
   section-23 override below still applies; the paint moved onto the rect as a
   `fill`, which is what an SVG element takes instead of a `background`. */
.live-bar-fill {
    display: block;
    height: 100%;
    border-radius: var(--r-xl);
    /* All six bars round the svg, never the rect -- see `.mix-bar .bar-fill`. */
    overflow: hidden;
    background: linear-gradient(90deg, var(--sky), var(--live-color), var(--accent));
    box-shadow: 0 0 14px -2px var(--live-color);
    transition: width 0.5s var(--ease-out);
}
.live-bar-fill rect { fill: var(--live-color, var(--flight)); }
.live-bar-labels {
    display: flex;
    justify-content: space-between;
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.5px;
    color: var(--text-faint);
}

.rest-day-card {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-radius: var(--r-lg);
    background:
        radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--green) 14%, transparent), transparent 65%),
        var(--green-soft);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--green) 24%, transparent);
}
.rest-icon { font-size: var(--fs-3xl); line-height: 1; flex: none; filter: drop-shadow(0 3px 6px rgba(15, 157, 88, 0.35)); }
.rest-text { min-width: 0; }
.rest-title { font-size: var(--fs-md); font-weight: 800; color: var(--green); }
.rest-sub { font-size: var(--fs-sm); color: var(--text-soft); margin-top: var(--space-1); }

.dash-grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--gap);
    margin-bottom: var(--gap);
}
/* The grid holds "Today" alone now -- next flight moved out to its own full-width
   card above. It used to be `nth-child(1) { span 4 }` / `nth-child(2) { span 8 }`
   against two cards; left as-is the lone card would claim 4 of 12 columns and
   leave the rest of the row empty. The `nth-child` pair is kept so a second card
   dropped in here still lays out the way it always did. */
.dash-grid > .dash-card:nth-child(1) { grid-column: span 12; }
.dash-grid > .dash-card:nth-child(2) { grid-column: span 8; }

/* Next flight, top of the dashboard. Slightly more padding than the other cards
   because it is the one thing being read at a glance.
   PERF-02: `.dash-next-card` was here, and `.dash-sub` below it. Neither was ever
   emitted -- the card renders as `.dash-duty` and the ops widget -- so both are
   gone. `.dash-big` stays: it is unemitted too, but `test_visual_tokens.py`
   asserts its print rule, and deleting an assertion's subject is not extracting. */

.dash-card,
.chart-card {
    position: relative;
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    padding: var(--pad);
    box-shadow: none;
    margin-bottom: 0;
    overflow: hidden;
    transition: border-color var(--dur-fast) var(--ease);
}
/* The accent gradient top-edge is retired: a 1px border does the separating. */
.dash-card::before,
.chart-card::before { display: none; }
.dash-card:hover,
.chart-card:hover {
    border-color: var(--card-border-2);
}

.dash-title {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--fs-xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.9px;
    color: var(--text-faint);
    margin-bottom: var(--space-4);
}
.dash-title .dicon {
    width: 24px;
    height: 24px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--r-md);
    background: var(--sky-soft);
    color: var(--sky);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sky) 22%, transparent);
}
.dash-title .dicon svg { width: 13px; height: 13px; }

.dash-body { min-height: 54px; }
.dash-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 54px;
    padding: var(--space-4) var(--space-2);
    text-align: center;
    font-size: var(--fs-base);
    line-height: 1.5;
    color: var(--text-faint);
}

.dash-duty {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-3);
    margin: 0 calc(-1 * var(--space-3));
    border-radius: var(--r-sm);
    cursor: pointer;
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 70%, transparent);
    transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.dash-duty:last-child { border-bottom: none; }
.dash-duty:hover { background: var(--surface-2); transform: translateX(3px); }

.dash-big {
    font-size: var(--fs-3xl);
    font-weight: 800;
    letter-spacing: -0.03em;
    line-height: 1.15;
    background: linear-gradient(160deg, var(--text) 25%, color-mix(in srgb, var(--accent) 70%, var(--text)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
/* PERF-02: `.dash-sub` was here. Unemitted, unpinned, gone -- see the note above
   `.dash-next-card`'s old place. */

.dash-week,
.dash-changes { display: flex; flex-direction: column; }

.week-mini {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-3);
    margin: 0 calc(-1 * var(--space-3));
    border-radius: var(--r-sm);
    cursor: pointer;
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 70%, transparent);
    transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.week-mini:last-child { border-bottom: none; }
.week-mini:hover { background: var(--surface-2); transform: translateX(3px); }
/* weekend: dashed separator + tinted day badge, readable without colour */
.week-mini.weekend { border-bottom-style: dashed; }
.week-mini.weekend .wm-day {
    background: color-mix(in srgb, var(--text-faint) 9%, var(--surface-2));
    border-color: var(--card-border-2);
}
.week-mini.weekend .wm-dow { color: var(--text-soft); }

.wm-day {
    flex: none;
    width: 46px;
    text-align: center;
    padding: var(--space-2) 0;
    border-radius: var(--r-xs);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
}
.wm-dow {
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--text-faint);
}
.wm-num { font-size: var(--fs-lg); font-weight: 800; }
.wm-span { flex: 1; min-width: 0; font-size: var(--fs-base); color: var(--text-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wm-span b { color: var(--text); font-weight: 700; }

.chg-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--fs-base);
    padding: var(--space-3);
    margin: 0 calc(-1 * var(--space-3));
    border-radius: var(--r-sm);
    color: var(--text-soft);
    cursor: pointer;
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 70%, transparent);
    transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.chg-item:last-child { border-bottom: none; }
.chg-item:hover { background: var(--surface-2); transform: translateX(3px); }
.chg-item .date { color: var(--text-faint); min-width: 78px; font-weight: 600; }

/* ============================== 13 ┬À TRIPS ================================ */

.trip-list { display: flex; flex-direction: column; gap: var(--space-4); }
/* The rotation card is .rot, not .trip-card: app.js:4252 sets
   `card.className = 'rot'` and every child it builds is a .rot-* element, so the
   106 lines this section used to hold -- .trip-card, .trip-head, .trip-route,
   .trip-meta, .trip-body, .trip-leg, .trip-off, .trip-hotel and the .tleg-*
   leg furniture -- could never match anything. The live styles are in 23.6.
   .trip-list is still emitted (app.js:60), so the container stays here. */

/* ======================= 14 ┬À STATS PANE & CHARTS ========================= */

.stats-year-row {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
    font-size: var(--fs-sm);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-faint);
}
.stats-year-row input {
    width: 96px;
    font-size: var(--fs-md);
    font-weight: 800;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-3) var(--space-4);
    box-shadow: var(--shadow-sm);
    transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
/* A11Y-04. The design's focus treatment on an input is a border tint plus a
   soft halo, and `outline: none` removed the ring so the two could not stack.
   The indicator was then visible -- but it depended on the border's contrast,
   it is not painted where an ancestor clips with `overflow: hidden` (this file
   clips on `.modal`, `.dash-card`, `.calendar-view`, `.day-card` and
   `.stat-card`), and forced-colours mode drops box-shadows entirely, which on
   Windows left a focused field with nothing at all.

   So the outline is kept, but only where the user is navigating by keyboard:
   the `:focus` rule below keeps the tint on a mouse click exactly as before,
   and a second `:focus-visible` rule adds the same ring the global rule uses.
   Both are one pseudo-class, so source order decides -- which is why the ring
   comes second, and why a comment saying so is worth the three lines. */

.stats-year-row input:focus {
    outline: none;
    border-color: var(--sky);
    box-shadow: var(--ring-soft);
}
.stats-year-row input:focus-visible {
    outline: 2px solid var(--accent-ink);
    outline-offset: 2px;
}
.stats-year-row .spacer { flex: 1; }
/* L75: the year row could not wrap. On the map tab its right-hand pill carries
   three real figures -- airports, kilometres, nautical miles -- and measured at a
   320px viewport that pill is 165px wide beside a 37px label and a 96px input, so
   the row laid out at 346px inside a 296px box and the document scrolled sideways
   by 26px. The figures are worth keeping, so the row wraps rather than
   truncating them; the spacer keeps the pill right-aligned on the first line when
   there is room for both. */
.stats-year-row { flex-wrap: wrap; row-gap: var(--space-2); }
.stats-year-row > .status-pill { min-width: 0; }

.chart {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    /* L73: was a fixed `height`. Twelve bars on a 320px screen leave ~20px each, and
       `.bar-val` below is nowrap, so the labels collided. A `min-height` keeps the
       usual size and lets the chart grow when the labels need room. */
    min-height: 165px;
    padding-top: var(--space-3);
}

.bar-wrap {
    flex: 1;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    min-width: 0;
}
.bar-val {
    font-size: var(--fs-2xs);
    font-weight: 800;
    color: var(--text-faint);
    white-space: nowrap;
    /* L73: nowrap plus a 20px bar collided. Truncate inside the bar instead of
       overprinting the neighbouring one. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bar {
    width: 100%;
    max-width: 28px;
    min-height: 3px;
    border-radius: var(--r-md) var(--r-md) var(--r-xs) var(--r-xs);
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 55%, var(--sky)),
                var(--accent) 85%,
                color-mix(in srgb, var(--accent) 70%, #000000));
    box-shadow: 0 -2px 12px -4px color-mix(in srgb, var(--accent) 70%, transparent);
    transform-origin: bottom;
    animation: growBar 0.6s var(--ease-out) both;
    transition: filter var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
/* PERF-02: `.bar.multi`, `.bar-wrap:hover .bar-label` and `.bar-label` were here.
   The bar chart renders `#bar-chart` as SVG now; the only `.bar*` markup left is
   the L73 probe's, which builds `.bar-wrap`/`.bar-val`/`.bar` and asserts on the
   first two. Nothing emits `.bar-label` since e725a2c, and nothing ever emitted
   `.bar.multi`. */
.bar:hover { filter: brightness(1.12) saturate(1.1); transform: scaleX(1.12); }

/* months without duties: neutral stub instead of a coloured sliver */
.bar-wrap:has(.bar-val:empty) .bar {
    background: var(--gray-soft);
    box-shadow: none;
    opacity: 0.7;
}

.mix-list { display: flex; flex-direction: column; gap: var(--space-3); }

.mix-row {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    font-size: var(--fs-sm);
    color: var(--text-soft);
}
.mix-row .dot {
    width: 11px;
    height: 11px;
    flex: none;
    border-radius: var(--r-xs);
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 15%, transparent);
}
.mix-row .nm { flex: 1; min-width: 0; font-weight: 700; color: var(--text); }
.mix-row .ct { font-weight: 800; color: var(--text); }
.mix-bar {
    flex: 1.1;
    height: 8px;
    min-width: 40px;
    background: var(--gray-soft);
    border-radius: var(--r-xl);
    overflow: hidden;
    box-shadow: inset 0 1px 2px rgba(11, 25, 45, 0.08);
}
/* SEC-17: the fill is an svg now -- same track, same animation, the width as a
   geometry attribute. The tone is a closed set, so it is classes on the rect
   (`.sfill-*`); the base is off-ink, exactly what the old `|| '--off-ink'`
   fallback painted. `transform-box` because a CSS scale on an SVG rect defaults
   its origin to the viewport, not the bar. */
.mix-bar .bar-fill {
    display: block;
    height: 100%;
    /* The rounding lives here, not as `rx` on the rect: an SVG rounded corner
       rasterises a hair differently from the CSS `border-radius` the old `<i>`
       had, and the visual baselines count hairs. Clipping the square rect with
       the same CSS radius the track already uses renders through the same path
       as before. */
    border-radius: var(--r-xl);
    overflow: hidden;
}
.mix-bar .bar-fill rect {
    fill: var(--off-ink);
    animation: growBarX 0.7s var(--ease-out) both;
    transform-box: fill-box;
    transform-origin: left center;
}
.mix-bar .bar-fill rect.sfill-flight   { fill: var(--flight); }
.mix-bar .bar-fill rect.sfill-hotel    { fill: var(--hotel); }
.mix-bar .bar-fill rect.sfill-standby  { fill: var(--standby); }
.mix-bar .bar-fill rect.sfill-training { fill: var(--training); }
.mix-bar .bar-fill rect.sfill-reserve  { fill: var(--reserve); }

/* All twelve months fit comfortably, so no nested scrollbar: a scrollbar on
   top of the numbers reads worse than a slightly taller card. */
.months-table { display: flex; flex-direction: column; }
.mtr {
    display: grid;
    grid-template-columns: 1fr auto auto auto;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-3) var(--space-3);
    margin: 0 calc(-1 * var(--space-3));
    border-radius: var(--r-xs);
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 70%, transparent);
    font-size: var(--fs-base);
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.mtr:last-child { border-bottom: none; }
.mtr:hover { background: var(--surface-2); transform: translateX(3px); }
.mtr .mo { font-weight: 800; letter-spacing: 0.2px; }
.mtr .va { color: var(--text-soft); text-align: right; }
.mtr.is-zero-month { opacity: 0.55; }
.mtr.is-zero-month .mo,
.mtr.is-zero-month .va { color: var(--text-faint); }
.mtr-bar {
    display: block;
    height: 3px;
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--text-faint) 18%, transparent);
    overflow: hidden;
    grid-column: 1 / -1;
}
.mtr-bar .bar-fill { display: block; height: 100%; }
.mtr-bar .bar-fill rect { fill: var(--flight); fill: url(#mtr-bar-grad); }
.mtr.is-zero-month .mtr-bar { display: none; }

/* L74: two columns, not three equal ones.

   The three cards this grid held were 5, 12 and 2 rows tall. `repeat(3, 1fr)`
   gave all three the same width *and* -- because `align-items` stretched them --
   the height of the tallest, so two thirds of Duty mix and Time breakdown was
   empty card. By month, the only one of the three that has columns to line up,
   was the one squeezed into a third of the width.

   So Duty mix and Time breakdown share a column and stack, and By month takes
   the rest. `minmax(0, 1fr)` rather than `1fr`: the By-month rows are nowrap,
   and a bare `1fr` lets a row set the track's minimum instead of the column
   doing it -- the same trap the month calendar was in. */
.mix-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-4);
    align-items: start;
}

/* The stacked half. A plain column with the grid's own gap, so the two cards
   inside it keep the same rhythm as every other pair of cards on the page. */
.mix-stack {
    display: grid;
    gap: var(--space-4);
    align-content: start;
    min-width: 0;
}

/* =========================== 15 ┬À ROUTE MAP ============================== */

.map-card svg {
    width: 100%;
    height: auto;
    display: block;
    border-radius: var(--r-lg);
    background: linear-gradient(170deg, #dcebf9, #c9ddf2);
    box-shadow: inset 0 0 30px rgba(13, 30, 60, 0.12), var(--shadow-sm);
}
[data-theme="dark"] .map-card svg {
    background: linear-gradient(170deg, #071022, #04080f);
    box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.6);
}

.map-ocean { fill: transparent; transition: fill 0.35s ease; }
[data-theme="dark"] .map-ocean { fill: transparent; }

.map-land {
    fill: #ffffff;
    stroke: rgba(13, 30, 60, 0.18);
    stroke-width: 0.75;
    stroke-linejoin: round;
    transition: fill 0.35s ease, stroke 0.35s ease;
}
[data-theme="dark"] .map-land {
    fill: #17253f;
    stroke: rgba(255, 255, 255, 0.14);
}

.map-grid {
    stroke: rgba(13, 30, 60, 0.09);
    stroke-width: 0.75;
    stroke-dasharray: 3 4;
}
[data-theme="dark"] .map-grid { stroke: rgba(255, 255, 255, 0.07); }

.map-route {
    fill: none;
    stroke: var(--accent);
    stroke-opacity: 0.5;
    stroke-linecap: round;
    cursor: pointer;
    transition: stroke-opacity 0.18s ease, stroke-width 0.18s ease, filter 0.18s ease;
}
.map-route:hover,
.map-route.highlighted {
    stroke-opacity: 1;
    filter: drop-shadow(0 0 5px var(--accent));
}

.map-dot {
    fill: var(--sky);
    stroke: var(--card);
    stroke-width: 1.5;
    cursor: pointer;
    transition: r 0.18s var(--ease-spring), stroke-width 0.18s ease,
                fill 0.18s ease, filter 0.18s ease;
}
.map-dot:hover,
.map-dot.highlighted {
    r: 6.5px;
    stroke-width: 2.5px;
    fill: var(--accent);
    filter: drop-shadow(0 0 6px var(--accent));
}
.map-dot.base-hub { fill: var(--accent); }

.map-hub-ring {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.8;
    pointer-events: none;
    animation: hubPulse 2.8s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.map-label {
    font-size: var(--fs-xs);
    font-weight: 700;
    fill: var(--text-soft);
    paint-order: stroke;
    stroke: var(--card);
    stroke-width: 3.5px;
    pointer-events: none;
    user-select: none;
}

/* UI-02: single .map-tooltip block (was split §map + §cockpit with a
   partial override by source order). Values below are the merged result:
   cockpit flat card wins for background/border/radius/padding/font. */
.map-tooltip {
    position: absolute;
    top: 18px;
    left: 18px;
    z-index: 10;
    max-width: 300px;
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-meta);
    line-height: 1.4;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-lg);
    pointer-events: none;
}
.map-tooltip.hidden { display: none; }
.map-tooltip .mt-code { font-size: var(--fs-md); font-weight: 800; }
.map-tooltip .mt-city { font-weight: 600; color: var(--text-soft); }
.map-tooltip .mt-stat { margin-top: var(--space-2); font-size: var(--fs-xs); color: var(--text-faint); }

.map-hint {
    margin-top: var(--space-4);
    text-align: center;
    letter-spacing: 0.1px;
}

.map-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px dashed var(--card-border-2);
}
.map-chip {
    font-size: var(--fs-xs);
    font-weight: 700;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-xl);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
    color: var(--text-soft);
    cursor: pointer;
    transition: all var(--dur) var(--ease);
}
.map-chip:hover {
    background: var(--sky-soft);
    color: var(--sky);
    border-color: color-mix(in srgb, var(--sky) 45%, transparent);
    transform: translateY(-2px);
    box-shadow: var(--shadow-sm);
}
.map-chip.active {
    color: #fff;
    background: linear-gradient(180deg, var(--bg-grad-2), var(--bg-grad-1));
    border-color: transparent;
    box-shadow: 0 6px 16px -6px rgba(7, 20, 38, 0.8);
}
[data-theme="dark"] .map-chip.active {
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 90%, #ffffff),
                color-mix(in srgb, var(--accent) 78%, #000000));
}

/* ======================= 16 ┬À MODALS & FORMS ============================= */

.modal-root {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-6);
    background: radial-gradient(900px 600px at 50% 0%, rgba(9, 18, 34, 0.6), rgba(6, 11, 22, 0.72));
    -webkit-backdrop-filter: blur(8px) saturate(0.9);
    backdrop-filter: blur(8px) saturate(0.9);
    animation: fadeIn 0.18s ease both;
}
@keyframes fadeIn { from { opacity: 0; } }

/* --- The duty editor is a slide-over, not a centred modal -----------------
   Editing a duty used to cover the calendar with a centred dialog. The crew
   member picks a day, loses sight of the month, fixes a time, and comes back to
   a grid they have to re-find their place in. Worse, the thing being corrected
   -- "is this the right sector for this date?" -- is a question only the
   calendar can answer, and the calendar was hidden behind the dialog asking it.

   So the root no longer dims and blocks: it is an invisible full-height rail on
   the right, the panel is full height and flush to the edge, and the month stays
   visible and interactive to its left. Only *this* modal becomes a drawer; the
   settings, history and purge dialogs stay centred, because they are
   interruptions by nature -- a purge confirmation that leaves the page live
   beside it would be a different and worse dialog.

   `aria-modal="true"` is removed from the markup by the note at index.html:
   with the page still reachable, claiming modality would tell a screen reader
   the opposite of what is true. Focus is *not* trapped here, for the same
   reason -- showModal()'s Tab trap is scoped to the dialog and this one no
   longer is a dialog. */
#duty-modal {
    z-index: 118;
    align-items: stretch;
    justify-content: flex-end;
    padding: 0;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    animation: none;
    pointer-events: none;
}
#duty-modal .modal {
    pointer-events: auto;
    width: min(440px, 100%);
    max-width: none;
    max-height: none;
    height: 100%;
    border-radius: 0;
    border-right: 0;
    border-top: 0;
    border-bottom: 0;
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    backdrop-filter: blur(24px) saturate(180%);
    -webkit-backdrop-filter: blur(24px) saturate(180%);
    box-shadow: var(--shadow-xl);
    animation: drawer-in var(--dur) var(--ease-out) both;
    padding-bottom: var(--space-7);
}
/* The rounded top-edge highlight `.modal::before` draws would be a stray line
   against a flush edge. */
#duty-modal .modal::before { display: none; }

@keyframes drawer-in {
    from { transform: translateX(24px); opacity: 0; }
    to   { transform: translateX(0);    opacity: 1; }
}

@media (max-width: 620px) {
    /* On a phone there is no "beside" to preserve, so it becomes a bottom sheet:
       full width, and anchored to the bottom where a thumb is. */
    #duty-modal { align-items: flex-end; }
    #duty-modal .modal {
        width: 100%;
        height: auto;
        max-height: 88dvh;
        border-radius: var(--r-xl) var(--r-xl) 0 0;
        border-left: 1px solid var(--card-border-2);
        border-top: 1px solid var(--card-border-2);
        border-right: 0;
        padding-bottom: var(--space-6);
        animation: sheet-up var(--dur) var(--ease-out) both;
    }
    @keyframes sheet-up {
        from { transform: translateY(28px); opacity: 0; }
        to   { transform: translateY(0);    opacity: 1; }
    }
}

/* The day drawer's own footer, where "add a duty" lives. Separate from
   `.day-actions` (which is the per-card row) so the two do not inherit each
   other's layout. */
.day-sheet-foot {
    flex: none;
    padding: var(--space-4) var(--space-4) var(--space-5);
    border-top: 1px solid var(--card-border);
    background: color-mix(in srgb, var(--surface-2) 60%, transparent);
}
.day-sheet-foot .day-actions { margin-bottom: 0; }

/* The per-duty edit affordance inside the drawer. A `.mini` so it inherits the
   44px touch floor, given an icon-plus-label rather than a bare pencil so it is
   not a mystery square on the card. */
.rot-edit {
    flex: none;
    margin-left: auto;
    align-self: center;
}

.modal {
    position: relative;
    width: 100%;
    max-width: 500px;
    max-height: 88dvh;
    overflow-y: auto;
    background: var(--card);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-xl);
    box-shadow: var(--shadow-xl);
    animation: pop 0.26s var(--ease-spring) both;
}
.modal::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    background: linear-gradient(90deg,
                color-mix(in srgb, var(--accent) 70%, transparent),
                color-mix(in srgb, var(--sky) 70%, transparent));
    pointer-events: none;
}
@keyframes pop {
    from { opacity: 0; transform: translateY(14px) scale(0.965); }
    to   { opacity: 1; transform: none; }
}

.modal-head {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-6);
    background: color-mix(in srgb, var(--card) 88%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--card-border);
}
.modal-head h2,
.modal-head h3 {
    font-size: var(--fs-lg);
    letter-spacing: -0.01em;
    background: linear-gradient(160deg, var(--text), color-mix(in srgb, var(--text) 55%, var(--sky)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.modal-x {
    flex: none;
    width: 32px;
    height: 32px;
    display: grid;
    place-items: center;
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-faint);
    font-size: var(--fs-2xl);
    line-height: 1;
    cursor: pointer;
    border-radius: var(--r-xs);
    transition: color var(--dur-fast) var(--ease),
                background var(--dur-fast) var(--ease),
                transform 0.28s var(--ease);
}
.modal-x:hover { color: var(--danger); background: var(--danger-soft); transform: rotate(90deg); }

#duty-form,
.settings-body,
.io-body { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-5) var(--space-6) var(--space-6); }
.io-body { gap: var(--space-6); }

.field { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; min-width: 0; }
.field-row { display: flex; gap: var(--space-4); }
.field-row .field { min-width: 0; }

.field label {
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-faint);
}

.field input[type="date"],
.field input[type="month"],
.field input[type="time"],
.field input[type="datetime-local"],
.field input[type="text"],
.field select,
.io-body textarea,
.search-body input,
#search-input {
    font-size: var(--fs-base);
    color: var(--text);
    /* Anything you type into or press is the app's interactive level, one step
       above a card. It used to borrow --surface-2, which put a field on the same
       plane as the panel holding it -- the form read as one flat slab with a
       border drawn across it rather than as controls sitting in a card. */
    background: var(--input);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-3) var(--space-4);
    width: 100%;
    transition: border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease),
                background var(--dur-fast) var(--ease);
}
.field input:hover,
.field select:hover,
.io-body textarea:hover { border-color: var(--card-border-2); }

.field input:focus,
.field select:focus,
.io-body textarea:focus,
#search-input:focus {
    outline: none;
    background: var(--card);
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    box-shadow: var(--ring);
}

/* A11Y-04 -- see the note above the year row. Same treatment, same reason: the
   tint is the design, the ring is the guarantee. */
.field input:focus-visible,
.field select:focus-visible,
.io-body textarea:focus-visible,
#search-input:focus-visible {
    outline: 2px solid var(--accent-ink);
    outline-offset: 2px;
}

.field select {
    appearance: none;
    padding-right: var(--space-8);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 17px) 50%, calc(100% - 12px) 50%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
    cursor: pointer;
}

[data-theme="dark"] .field input,
[data-theme="dark"] .field select,
[data-theme="dark"] .io-body textarea,
[data-theme="dark"] #search-input {
    color-scheme: dark;
}

.modal-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px solid var(--card-border);
}
.modal-actions .spacer { flex: 1; }

.ghost-delete { margin-right: auto; }

/* settings */
.set-block { display: flex; flex-direction: column; gap: var(--space-4); }
.set-label {
    font-size: var(--fs-xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-soft);
}
.set-block.row-toggle { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--space-5); }
.set-block .hint { font-size: var(--fs-xs); color: var(--text-faint); }

/* danger zone: the irreversible action is visually separated from the
   ordinary reset above it, so it is never hit by muscle memory */
.set-block.danger-zone {
    margin-top: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px dashed color-mix(in srgb, var(--danger) 40%, transparent);
}
.set-block.danger-zone .set-label {
    color: var(--danger);
    letter-spacing: 0.2px;
    margin-bottom: var(--space-3);
}

.danger-modal { border-top: 3px solid var(--danger); }
.purge-lede { font-size: var(--fs-base); color: var(--text-soft); line-height: 1.55; }
.purge-list {
    margin: var(--space-4) 0;
    padding-left: var(--space-6);
    font-size: var(--fs-base);
    color: var(--text-soft);
    line-height: 1.6;
}
.purge-list li { margin-bottom: var(--space-1); }
.purge-list li::marker { color: var(--danger); }
.purge-keep {
    font-size: var(--fs-sm);
    color: var(--text-faint);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--r-sm);
    background: var(--green-soft);
    color: var(--green-ink);
}
.purge-check { display: block; margin-top: var(--space-4); }
.purge-check input[type="text"] {
    width: 100%;
    font-family: var(--mono);
    font-size: var(--fs-base);
    color: var(--text);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-3) var(--space-4);
    letter-spacing: 1.5px;
    text-transform: uppercase;
    transition: border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease),
                background var(--dur-fast) var(--ease);
}
.purge-check input[type="text"]:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent);
    outline: none;
}

/* A11Y-04 -- see the note above the year row. On this field the ring matters more
   than anywhere else: it is the only control in the app whose focus means "you
   are about to type the word that erases everything", and a tinted border is
   not an indicator a Windows user in forced-colours mode can see. */
.purge-check input[type="text"]:focus-visible {
    outline: 2px solid var(--accent-ink);
    outline-offset: 2px;
}
.purge-check input::placeholder { letter-spacing: 0.4px; text-transform: none; }
/* the destructive button stays inert until the word matches */
#purge-go:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

.swatches { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.swatch {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 3px solid var(--card);
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    position: relative;
    transition: transform 0.16s var(--ease-spring), box-shadow 0.16s ease;
}
.swatch::after {
    content: "";
    position: absolute;
    inset: -6px;
    border-radius: 50%;
    border: 2px solid transparent;
    transition: border-color 0.16s ease;
}
.swatch:hover { transform: scale(1.14); box-shadow: var(--shadow-md); }
.swatch[aria-pressed="true"] { box-shadow: var(--shadow-md); }
.swatch[aria-pressed="true"]::after { border-color: currentColor; }

.switch { position: relative; display: inline-block; width: 50px; height: 28px; flex: none; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider {
    position: absolute;
    inset: 0;
    background: var(--gray-soft);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-xl);
    transition: 0.24s var(--ease);
    cursor: pointer;
}
.switch .slider::before {
    content: "";
    position: absolute;
    width: 21px;
    height: 21px;
    left: var(--space-2);
    top: var(--space-1);
    background: #fff;
    border-radius: 50%;
    box-shadow: var(--shadow-sm);
    transition: transform 0.24s var(--ease-spring);
}
.switch input:checked + .slider {
    background: linear-gradient(180deg,
                color-mix(in srgb, var(--accent) 78%, #ffffff), var(--accent));
    border-color: transparent;
}
.switch input:checked + .slider::before { transform: translateX(21px); }

/* io / data blocks */
.io-block { display: flex; flex-direction: column; gap: var(--space-4); }
.io-title {
    font-size: var(--fs-xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-faint);
}
.io-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.io-actions label.btn { cursor: pointer; }
.io-hint { font-size: var(--fs-sm); line-height: 1.55; color: var(--text-faint); }
.io-hint code {
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    padding: var(--space-0) var(--space-2);
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
}
.io-body textarea {
    font-family: var(--mono);
    font-size: var(--fs-sm);
    line-height: 1.55;
    resize: vertical;
}

.login-msg { min-height: 1.2em; font-size: var(--fs-base); color: var(--text-faint); margin: var(--space-2) 0 0; font-weight: 600; }
.login-msg.err { color: var(--error); }
.login-msg.ok { color: var(--green); }
/* Where the credentials go. Small, quiet, and above the button rather than
   below it: the reader should have it before deciding to type a password. */
.gate-note {
    margin: var(--space-3) 0 0;
    font-size: var(--fs-2xs);
    line-height: 1.45;
    color: var(--text-faint);
}

/* --- sign-in gate -----------------------------------------------------------
   The first thing a new user sees. Deliberately not a .modal-root: it carries
   no close control and ignores Esc, because dismissing it would leave the app
   in a state where nothing works and nothing says why. */
.gate {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-7);
    /* A textured ground rather than a flat fill: two very wide, very low-alpha
       radial washes in the palette's own blue, plus a faint grid. All three sit
       far below the contrast threshold of anything drawn on top, so the card is
       unaffected by them -- they exist so the sign-in is not a rectangle floating
       in a void, which is what a flat #0b0f17 behind it looked like. */
    background:
        radial-gradient(900px 520px at 18% -8%,
            color-mix(in srgb, var(--sky) 13%, transparent), transparent 62%),
        radial-gradient(760px 480px at 92% 108%,
            color-mix(in srgb, var(--accent) 11%, transparent), transparent 60%),
        var(--bg);
    overflow-y: auto;
}
/* The grid is drawn with repeating-linear-gradient rather than an image so it
   costs no request and stays crisp at any DPR. 64px matches the app's spacing
   rhythm closely enough that it reads as structure, not as a texture. */
.gate::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        repeating-linear-gradient(0deg,
            rgba(255, 255, 255, 0.022) 0, rgba(255, 255, 255, 0.022) 1px, transparent 1px, transparent 64px),
        repeating-linear-gradient(90deg,
            rgba(255, 255, 255, 0.022) 0, rgba(255, 255, 255, 0.022) 1px, transparent 1px, transparent 64px);
    pointer-events: none;
}

.gate-card {
    position: relative;
    width: min(430px, 100%);
    /* Real glass: a blur and a saturate over the textured ground behind, plus a
       hairline top edge that catches the light the way a bevelled pane does. The
       `inset` highlight is the part that makes it read as glass rather than as a
       translucent rectangle -- it is one pixel of white at 7%. */
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    backdrop-filter: blur(28px) saturate(180%);
    -webkit-backdrop-filter: blur(28px) saturate(180%);
    border: 1px solid var(--glass-brd);
    border-radius: var(--r-xl);
    padding: var(--space-8) var(--space-8) var(--space-6);
    box-shadow: var(--shadow-xl),
                inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.gate-brand {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-6);
}
.gate-logo {
    flex: none;
    border-radius: var(--r-md);
    box-shadow: 0 4px 16px -6px color-mix(in srgb, var(--sky) 70%, transparent);
}
.gate-brand h1,
.gate-brand h2 {
    margin: 0;
    font-size: var(--fs-xl);
    font-weight: 800;
    letter-spacing: -0.2px;
}
.gate-brand p {
    margin: var(--space-1) 0 0;
    font-size: var(--fs-sm);
    color: var(--text-faint);
}
.gate #gate-form { display: flex; flex-direction: column; gap: var(--space-4); }
.gate .field input { width: 100%; }
.gate-submit {
    width: 100%;
    justify-content: center;
    padding: var(--space-4);
    font-size: var(--fs-base);
}

/* --- Sync stepper ---------------------------------------------------------
   Three states, three looks, and the third is the important one: *pending* has
   to be legible as "not yet reached" without reading as an error. An empty ring
   at low alpha does that. A cross or a red dot would read as a failure.

   The done state draws the tick; it is rotated and scaled in by the shared
   `step-pop` keyframe so a completed step visibly lands rather than swapping
   shape. The active state pulses its ring. Both animations are cancelled by the
   single prefers-reduced-motion block at the end of this file, which is what
   keeps `test_css_reachability`'s behavioural check honest. */
.gate-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.gate-steps[hidden] { display: none; }

.gate-step {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--fs-sm);
    color: var(--text-faint);
    transition: color var(--dur) var(--ease);
}

.gate-step-dot {
    position: relative;
    width: 20px;
    height: 20px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 2px solid var(--card-border-2);
    color: transparent;
    transition: border-color var(--dur) var(--ease),
                background var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease);
}
/* The tick is always in the DOM and always at the same size; only its colour and
   scale change. Re-creating it per state is what makes the icon pop differently
   on a re-render than on first paint. */
.gate-step-dot svg {
    width: 11px;
    height: 11px;
    transform: scale(0.4);
    opacity: 0;
    transition: transform var(--dur) var(--ease-spring),
                opacity var(--dur-fast) var(--ease);
}
/* The label is the stepper's whole accessible surface, so it gets its own rule
   rather than inheriting: `min-width: 0` so a long French string truncates with
   an ellipsis instead of forcing the card wider, and `text-wrap: balance` so the
   two-line French labels wrap evenly rather than leaving a single orphan word. */
.gate-step-label {
    min-width: 0;
    line-height: 1.35;
    text-wrap: balance;
    overflow-wrap: anywhere;
}

.gate-step.is-active { color: var(--text); font-weight: 600; }
.gate-step.is-active .gate-step-dot {
    border-color: var(--sky);
    background: color-mix(in srgb, var(--sky) 16%, transparent);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--sky) 14%, transparent);
    animation: step-pulse 1.9s var(--ease) infinite;
}

.gate-step.is-done { color: var(--text-soft); }
.gate-step.is-done .gate-step-dot {
    border-color: var(--green);
    background: color-mix(in srgb, var(--green) 20%, transparent);
    color: var(--green-ink);
}
.gate-step.is-done .gate-step-dot svg { transform: scale(1); opacity: 1; }

@keyframes step-pulse {
    0%, 100% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--sky) 14%, transparent); }
    50%      { box-shadow: 0 0 0 7px color-mix(in srgb, var(--sky) 5%, transparent); }
}

/* Working, not broken.

   The check against eCrew takes about a minute, and the button was only dimmed
   to 60% -- a change nobody reads on a dark red button. So during the longest
   wait in the application the control looked exactly like an idle one, and a
   second click started a second sign-in. Now there is a spinner, the label
   dims too, and the accent flattens so the button reads as "busy" at a glance.

   The spinner respects reduced-motion: it stops spinning and stays visible, so
   the state is still legible without movement. */
.gate-spinner {
    display: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-top-color: transparent;
    opacity: 0.9;
    flex: none;
}
.gate-submit[disabled] {
    cursor: progress;
    opacity: 1;
    /* color-mix rather than --accent-soft: that token is a pale pink meant for
       tinted text on a light background, and as a button fill in dark mode it
       would be the brightest thing on the screen. */
    background: color-mix(in srgb, var(--accent) 20%, transparent);
    color: var(--text);
    box-shadow: none;
}
.gate-submit[disabled] .gate-spinner { display: block; animation: gate-spin 0.7s linear infinite; }
.gate-submit[disabled] > [data-i18n] { opacity: 0.75; }

@keyframes gate-spin { to { transform: rotate(360deg); } }

/* The reduced-motion opt-out for this spinner lives in the one consolidated
   @media (prefers-reduced-motion: reduce) block in section 22, where it is
   !important and cannot be lost to source order. */
.gate-note {
    margin: var(--space-5) 0 0;
    font-size: var(--fs-xs);
    line-height: 1.55;
    color: var(--text-faint);
    text-align: center;
}
.gate-lang {
    display: flex;
    justify-content: center;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--card-border);
}

/* --- header account chip ----------------------------------------------------
   Replaces the old Login button: the app has its own sign-in page, so the
   header only needs to show who is signed in and offer a way out. */
.account-wrap { position: relative; }
.account-btn { gap: var(--space-3); }
/* The chip carries a person's name, which is not the same length as a crew ID.

   It holds a `max-width` and truncates, because the header is a single row of
   controls and this is the only one whose content the app does not control: a
   long name would push "New duty", "Search" and "Print" off a laptop header,
   and at 320px it would wrap the whole hero onto a second row. The full name is
   on the button's title and in the account menu, so truncating here costs the
   header nothing the reader cannot get back in one hover. */
#account-name {
    display: block;
    max-width: 140px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Below the phone breakpoint the header is already a wrapped grid of buttons, so
   the chip gets the room it needs rather than the width it had to make up for. */
@media (max-width: 620px) {
    #account-name { max-width: 190px; }
}
.account-avatar {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-xs);
    font-weight: 800;
    color: #fff;
    background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 62%, #000));
    flex: none;
}
.account-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 60;
    min-width: 216px;
    padding: var(--space-2);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md, 10px);
    box-shadow: 0 18px 40px -22px rgba(0, 0, 0, 0.6);
}
.account-menu-head {
    padding: var(--space-3) var(--space-3) var(--space-3);
    border-bottom: 1px solid var(--card-border);
    margin-bottom: var(--space-2);
}
.account-menu-label {
    display: block;
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.7px;
    text-transform: uppercase;
    color: var(--text-faint);
}
.account-menu-head strong { font-size: var(--fs-base); }
.account-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: var(--space-3) var(--space-3);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text);
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    cursor: pointer;
}
.account-menu-item:hover { background: var(--gray-soft); }
.account-menu-item.danger { color: var(--error); }

/* --- company messages on the dashboard -------------------------------------
   The unread ones are the reason the portal will not show a schedule at all,
   so they are visually unmissable rather than a quiet list. */
.portal-badge {
    margin-left: var(--space-3);
    min-width: 19px;
    padding: var(--space-0) var(--space-2);
    border-radius: var(--r-xl);
    background: var(--danger);
    color: #fff;
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-align: center;
}
.portal-msgs {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    /* No bullets. The list was styled as a flex column with no padding, so the
       default disc markers rendered outside the box and hung over the card's
       left margin -- one stray dot per message, in the page gutter. Each row
       already carries an envelope icon, so the bullet said nothing. */
    list-style: none;
    margin: 0;
    padding: 0;
}
/* Shown before anything has been fetched, so the block that holds the
   "check the portal" button is reachable rather than invisible. */
/* A block that is present but cannot be filled. Emitted by renderPortalPanel()
   when the extras file does not exist, so the panel says why it is short instead
   of showing an empty list. Deliberately quiet: it is an absence, not news. */
.portal-block--off {
    opacity: .72;
    border-style: dashed;
}
.portal-empty {
    margin: var(--space-1) 0 var(--space-3);
    color: var(--text-soft);
    font-size: var(--fs-sm);
}
.portal-msg-btn {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    text-align: left;
    padding: var(--space-3) var(--space-3);
    border: 0;
    border-radius: var(--r-md);
    background: none;
    color: var(--text-soft);
    font-size: var(--fs-base);
    cursor: pointer;
}
.portal-msg-btn:hover { background: var(--gray-soft); color: var(--text); }
.portal-msg-ico { flex: none; width: 16px; height: 16px; color: var(--text-faint); }
.portal-msg-ico svg { width: 100%; height: 100%; }
.portal-msg--unread .portal-msg-ico { color: var(--accent-ink); }
.portal-msg-txt { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; flex: 1; }
.portal-msg-txt b { font-weight: 600; color: var(--text-soft); }
.portal-msg--unread .portal-msg-txt b { font-weight: 800; color: var(--text); }
.portal-msg-state { font-size: var(--fs-2xs); font-weight: 700; color: var(--text-faint); }
.portal-msg--unread .portal-msg-state { color: var(--accent-ink); }
.portal-msg-date {
    flex: none;
    font-size: var(--fs-xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
    white-space: nowrap;
}
.portal-block .io-actions { margin-top: var(--space-3); }

.msg-subject {
    margin: 0;
    font-size: var(--fs-base);
    font-weight: 800;
    line-height: 1.35;
    color: var(--text);
}
.msg-body {
    margin: 0;
    max-height: min(52vh, 420px);
    overflow-y: auto;
    padding: var(--space-4) var(--space-4);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    font-family: var(--mono);
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: var(--text-soft);
    white-space: pre-wrap;
    word-break: break-word;
}

/* --- full-history import --------------------------------------------------- */

/* Year picker. A chip is the smallest thing that still says which twelve
   months will be taken, so it is a button rather than a checkbox: tapping it
   is the whole interaction the feature asks for. */
.hist-years-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-1) 0 var(--space-2);
}
.hist-years-title {
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text-soft);
}
.hist-years {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
/* S2: the sector chain. Two places, one set of rules: a numbered list in the duty
   sheet, an inline continuation in a pairing's timeline node.

   The chain reads as a sequence, so it is marked up as one -- an ordered list in the
   sheet -- rather than as a row of spans, which a screen reader announces as seven
   unrelated words. In the pairing it stays inline, because there the entries are
   already the sequence and the chain is a refinement of one node.
   PERF-02: `.detail-legs` itself was here -- the wrapper 6e7cecc unwired. The
   chain (`.leg-chain` and below) is live; only the dead wrapper is gone. */
.leg-chain {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
}
.leg-chain-item {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    font-size: var(--fs-sm);
}
.leg-chain-n {
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}
.leg-chain-route {
    font-weight: 600;
}
.leg-chain-flight,
.leg-chain-time {
    color: var(--text-soft);
    font-variant-numeric: var(--tnum);
}

/* The sector chain is a *continuation* of the leg above it, so it gets its own
   line rather than competing with the flight number, the route and the times
   for the width of one row. `flex: 1 0 100%` makes that true at every width,
   and `min-width: 0` with `overflow-wrap` is what lets a four-sector duty break
   inside the box instead of pushing the whole page sideways. */
.rot-leg-chain {
    display: block;
    flex: 1 0 100%;
    min-width: 0;
    margin-top: var(--space-1);
    color: var(--text-soft);
    font-size: var(--fs-xs);
    overflow-wrap: anywhere;
}
.rot-chain-seg b { color: var(--text); }
.rot-chain-seg i {
    font-style: normal;
    font-variant-numeric: var(--tnum);
    margin-left: var(--space-1);
}
.rot-chain-join { margin: 0 var(--space-1); color: var(--text-faint); }

.hist-year {
    padding: var(--space-2) var(--space-4);
    min-width: 62px;
    border: 1px solid var(--gray);
    border-radius: var(--r-md);
    background: transparent;
    color: var(--text-soft);
    font: inherit;
    font-size: var(--fs-base);
    font-variant-numeric: var(--tnum);
    cursor: pointer;
}
.hist-year:hover { border-color: var(--text-soft); color: var(--text); }

/* S1: the per-month coverage strip inside a year chip. Twelve cells, one per
   month, saying what is known about that month before anything is imported.

   Each state carries a *shape* as well as a colour. Colour alone is not readable
   by everyone, and this is information rather than decoration: "the portal has
   nothing for March" and "we have not asked about March" are the two facts the
   strip exists to separate, and a crew member who cannot see the difference gets
   the same ambiguity back.

     imported -- a filled cell
     empty    -- a hairline outline   (the portal answered; the answer was nothing)
     failed   -- a diagonal hatch     (we asked; it broke)
     never    -- nothing at all       (not asked: the outline would imply an answer)

   `never` is therefore drawn as an absence rather than a fifth colour, which is the
   honest way to draw a state defined by the absence of a record. */
.hist-year-strip {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    /* `--hairline` is declared in :root rather than inlined here, because the rule
       this section enforces is "no spacing value is chosen by hand", and the reason
       this one is not on the scale belongs in the token rather than in a comment
       20 lines above its use: the smallest spacing step is 2px, which on a chip
       62px wide would put a visible gutter between all twelve cells and make the
       strip read as a barcode rather than a bar. */
    gap: var(--hairline);
    margin-top: var(--space-1);
    height: 6px;
    width: 100%;
}
.hist-year-cell {
    border-radius: var(--r-xs);
    background: var(--gray-soft);
}
.hist-year-cell.is-imported { background: var(--green-ink); }
.hist-year-cell.is-empty {
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--gray);
}
.hist-year-cell.is-failed {
    background: repeating-linear-gradient(
        45deg, var(--warning) 0 2px, transparent 2px 4px);
}
.hist-year-cell.is-never { background: transparent; }

/* The strip is information, so it must survive forced colours, where the palette
   is replaced wholesale and a `color-mix` or a translucent tint means nothing. The
   outline cells keep their shape; the filled and hatched ones are given an edge so
   they are still distinguishable. */
@media (forced-colors: active) {
    .hist-year-cell { forced-color-adjust: none; border: 1px solid CanvasText; }
    .hist-year-cell.is-never { border-color: transparent; }
}
.hist-year.is-on {
    border-color: var(--accent);
    /* A translucent tint of the accent, not --accent-soft: that token is a
       light pink in both themes, and white text on it is unreadable. */
    background: color-mix(in srgb, var(--accent) 24%, transparent);
    color: var(--text);
    font-weight: 700;
}
.hist-year:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
#hist-years-sum { margin: var(--space-3) 0 0; }
  /* --- eCrew unreachable (header) ----------------------------------------
     Amber rather than red: nothing here is wrong with the crew member's data or
     their machine, and the fix is to wait. Pairs with the offline pill, which
     answers the opposite question about the local server. */
  #portal-badge {
      border-color: #b8860b;
      color: var(--text);
      gap: var(--space-2);
  }
  #portal-badge .dot { background: #b8860b; }
  .portal-retry {
      font-size: var(--fs-xs);
      opacity: .8;
      font-variant-numeric: var(--tnum);
  }
  /* There was here a `@media (prefers-color-contrast: more) { #portal-badge
     { border-width: 2px } }`. `prefers-color-contrast` is **not** the feature
     name -- the standard is `prefers-contrast` -- so the query was invalid and
     the rule has never applied in any browser. Its intent is kept, in a block
     that can actually match, at the end of the file. */

  /* --- Saved roster copies (Settings) -------------------------------------
     A repair tool: this lists the rosters earlier imports replaced, so a fetch
     that brought back something wrong can be put back. Timestamps on the left so
     the list reads as a timeline, which is how it is actually used. */
  .backups-list {
      display: flex;
      flex-direction: column;
      gap: var(--space-2);
      margin-top: var(--space-3);
  }
  .backups-list .bk-sum { margin: 0 0 var(--space-1); }
  .backups-row {
      display: flex;
      align-items: center;
      gap: var(--space-3);
      padding: var(--space-2) var(--space-3);
      border: 1px solid var(--gray);
      border-radius: var(--r-md);
  }
  .backups-when {
      font-size: var(--fs-sm);
      font-variant-numeric: var(--tnum);
      color: var(--text);
      min-width: 132px;
  }
  .backups-info {
      font-size: var(--fs-sm);
      color: var(--text-soft);
      flex: 1;
  }
  .backups-row .btn { padding: var(--space-2) var(--space-3); font-size: var(--fs-sm); }

  /* --- Background import pill (header) ------------------------------------
     The import runs either way; this only says "it is happening" and offers the
     way back into the progress view, so a multi-year backfill does not have to be
     sat through. A button, not a span, because it is clickable. */
  #bg-import {
      position: relative;
      overflow: hidden;
      cursor: pointer;
      border: 1px solid var(--accent);
      color: var(--text);
      padding-right: var(--space-8);
  }
  #bg-import .dot {
      background: var(--accent);
      animation: bg-pulse 1.6s ease-in-out infinite;
  }
  @keyframes bg-pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: .35; }
  }
  /* The fill is the progress, drawn behind the label rather than as a separate
     bar, so the pill stays one line tall. */
  .bg-import-bar {
      position: absolute;
      left: 0;
      bottom: 0;
      height: 3px;
      width: 0;
      background: var(--accent);
      transition: width .3s ease;
  }
  #bg-import:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
  }

  /* A year with some of its months picked. Dashed rather than solid, so it cannot
     be mistaken for a fully selected year -- which is the whole point of showing
     it differently. */
  .hist-year.is-some {
      border-color: var(--accent);
      border-style: dashed;
      color: var(--text);
      font-weight: 700;
  }

  /* --- Detailed selection: the months of each picked year ---------------- */
  .hist-months {
      display: flex;
      flex-direction: column;
      gap: var(--space-3);
      margin: var(--space-1) 0 var(--space-2);
  }
  .hist-month-row {
      display: flex;
      align-items: flex-start;
      gap: var(--space-3);
      padding: var(--space-3) var(--space-3);
      border: 1px solid var(--gray);
      border-radius: var(--r-md);
  }
  .hist-month-year {
      font-size: var(--fs-sm);
      font-weight: 700;
      font-variant-numeric: var(--tnum);
      color: var(--text-soft);
      min-width: 42px;
      padding-top: var(--space-2);
  }
  .hist-month-grid {
      display: flex;
      flex-wrap: wrap;
      gap: var(--space-2);
      flex: 1;
  }
  .hist-month {
      min-width: 34px;
      padding: var(--space-2) var(--space-2);
      border: 1px solid var(--gray);
      border-radius: var(--r-sm);
      background: transparent;
      color: var(--text-soft);
      font: inherit;
      font-size: var(--fs-sm);
      font-variant-numeric: var(--tnum);
      cursor: pointer;
  }
  .hist-month:hover { border-color: var(--text-soft); color: var(--text); }
  .hist-month.is-on {
      border-color: var(--accent);
      background: color-mix(in srgb, var(--accent) 24%, transparent);
      color: var(--text);
      font-weight: 700;
  }
  /* Already in the roster: shown, but visibly so. Not disabled -- whether these
     get re-fetched is the "skip what I have" switch's decision, not the
     picker's, and silently refusing a month the crew member tapped would be a
     rule they could neither see nor undo. */
  .hist-month.is-have { border-style: dotted; opacity: .65; }
  .hist-month.is-have.is-on { opacity: 1; }
  .hist-month:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
  }


.hist-progress {
    height: 6px;
    border-radius: var(--r-xl);
    background: var(--gray-soft);
    overflow: hidden;
}
.hist-progress-bar {
    height: 100%;
    width: 0;
    border-radius: var(--r-xl);
    background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #000));
    transition: width var(--dur-fast) var(--ease);
}
.hist-status {
    margin: 0;
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text-soft);
    font-variant-numeric: var(--tnum);
}
.hist-log {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    max-height: 190px;
    overflow-y: auto;
    font-size: var(--fs-sm);
    color: var(--text-faint);
}
.hist-log li {
    display: flex;
    gap: var(--space-3);
    align-items: baseline;
    font-variant-numeric: var(--tnum);
}
.hist-log .hist-log-mark { width: 13px; flex: none; text-align: center; }
.hist-log li.ok .hist-log-mark { color: var(--green); }
.hist-log li.skip .hist-log-mark { color: var(--text-faint); }
.hist-log li.fail { color: var(--error); }
.hist-log li.fail .hist-log-mark { color: var(--error); }
.hist-log .hist-log-n { margin-left: auto; white-space: nowrap; }
/* A gap note spans the month it starts plus the span it covers, so it wraps
   instead of being squeezed by the auto-margin the right-aligned column uses. */
.hist-log li.gap {
    border-top: 1px dashed color-mix(in srgb, var(--card-border) 70%, transparent);
    padding-top: var(--space-2);
    margin-top: var(--space-1);
    align-items: flex-start;
}
.hist-log li.gap .hist-log-n {
    margin-left: 0;
    text-align: right;
    white-space: normal;
    min-width: 0;
    opacity: 0.85;
}

/* Report toggle on the month-loading overlay: asking whether to pay for the
   printable report, which is the slow half of an eCrew month fetch. */
.empty-detail {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-4);
    font-size: var(--fs-sm);
    line-height: 1.4;
    color: var(--text-faint);
    cursor: pointer;
    text-align: left;
}
.empty-detail input { accent-color: var(--accent); cursor: pointer; }

.help-table { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
.help-table td {
    padding: var(--space-3) var(--space-3);
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 70%, transparent);
    color: var(--text-soft);
}
.help-table td:first-child { width: 118px; white-space: nowrap; }
.help-table tr:last-child td { border-bottom: none; }

/* --- duty detail modal --------------------------------------------------- */

.detail-hero {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: var(--space-5) 0 var(--space-4);
    border-bottom: 1px solid var(--card-border);
}
.detail-hero .flight-num {
    font-size: var(--fs-2xl);
    font-weight: 800;
    letter-spacing: -0.01em;
    background: linear-gradient(160deg, var(--text) 30%, color-mix(in srgb, var(--accent) 55%, var(--text)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

/* PERF-02: `.detail-grid`, `.detail-grid .meta-item` and `.detail-grid .meta-val`
   were here -- the grid 6e7cecc unwired. `.detail-note` below is live. */

.detail-note {
    line-height: 1.55;
}

/* Operating crew list from the printable report: role, crew id, name. */
.detail-crew {
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px solid var(--card-border);
}
.detail-crew-title {
    font-size: var(--fs-label);
    font-weight: 650;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-soft);
    margin-bottom: var(--space-2);
}
.crew-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-2);
}
.crew-list li {
    display: grid;
    /* L72: `1fr` is `minmax(auto, 1fr)`, so this track cannot shrink below its
       content and a long crew name widened the grid inside the duty-detail modal.
       `minmax(0, 1fr)` lets it shrink; the wrapping is on `.crew-name`. Copied
       from `.strip-end span`, `.rot-leg-route` and `.wm-span`, which already
       do it. */
    grid-template-columns: 5.5rem 4.5rem minmax(0, 1fr);
    gap: var(--space-3);
    align-items: baseline;
    font-size: var(--fs-meta);
    padding: var(--space-2) 0;
    border-bottom: 1px dashed color-mix(in srgb, var(--card-border) 75%, transparent);
}
.crew-list li:last-child { border-bottom: none; }
.crew-role { color: var(--text-faint); font-weight: 600; }
.crew-id { color: var(--text-soft); font-variant-numeric: var(--tnum); }
.crew-name { color: var(--text); font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
/* PERF-02: `.detail-raw` was here -- the pre block the sheet no longer renders.
   `.detail-body` and `.detail-actions` below are live. */

.detail-body { padding: var(--space-2) var(--space-6) var(--space-3); }
.detail-actions { padding: var(--space-3) var(--space-6) var(--space-5); }

/* ======================= 17 ┬À SEARCH & COMMAND PALETTE ========================== */

.search-modal { max-width: 580px; }
.search-body { padding: var(--space-4) var(--space-5) var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
.palette-body { padding: 0; }
.palette-body #palette-input {
    border: none;
    border-bottom: 1px solid var(--card-border);
    border-radius: 0;
    padding: var(--space-5) var(--space-6);
    font-size: var(--fs-md);
    background: transparent;
}
.palette-body #palette-input:focus { box-shadow: none; background: var(--surface-2); }
.palette-body .search-results { padding: var(--space-3); }

#search-input { padding: var(--space-4) var(--space-5); font-size: var(--fs-md); }

.search-results {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-height: 52dvh;
    overflow-y: auto;
}

.result-item {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-4);
    background: var(--surface-2);
    border: 1px solid transparent;
    border-radius: var(--r-md);
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
}
.result-item:hover,
.palette-item.selected {
    background: var(--card);
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    transform: translateX(3px);
    box-shadow: var(--shadow-sm);
}
.result-item .date { flex: none; min-width: 80px; font-size: var(--fs-xs); font-weight: 700; color: var(--text-faint); }
.result-item .res-main { flex: 1; min-width: 0; font-size: var(--fs-base); font-weight: 700; }
.result-item .res-sub { flex: none; font-size: var(--fs-xs); color: var(--text-faint); }
.res-empty { padding: var(--space-6) 0; text-align: center; font-size: var(--fs-base); color: var(--text-faint); }

/* ============================== 18 ┬À TOASTS =============================== */

.toasts {
    position: fixed;
    right: 18px;
    bottom: 18px;
    z-index: 120;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-width: min(360px, calc(100vw - 36px));
    pointer-events: none;
}

.toast {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-4);
    overflow: hidden;
    padding: var(--space-4) var(--space-5);
    font-size: var(--fs-base);
    font-weight: 700;
    color: var(--text);
    background: var(--glass);
    border: 1px solid var(--glass-brd);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-lg);
    -webkit-backdrop-filter: blur(14px) saturate(1.6);
    backdrop-filter: blur(14px) saturate(1.6);
    pointer-events: auto;
    animation: toastIn 0.3s var(--ease-spring) both;
}
.toast::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--sky);
}
.toast.ok::before { background: var(--green); }
.toast.err::before { background: var(--error); }
.toast.info::before { background: var(--sky); }
.toast.leaving { animation: toastOut 0.24s var(--ease) both; }

.toast .tdot {
    width: 9px;
    height: 9px;
    flex: none;
    border-radius: 50%;
    background: var(--sky);
    color: var(--sky);
    box-shadow: 0 0 8px currentColor;
}
.toast.ok .tdot { background: var(--green); color: var(--green); }
.toast.err .tdot { background: var(--error); color: var(--error); }

@keyframes toastIn {
    from { opacity: 0; transform: translateX(28px) scale(0.94); }
    to   { opacity: 1; transform: none; }
}
@keyframes toastOut {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateY(6px) scale(0.95); }
}

.toast-row { display: inline-flex; align-items: center; gap: var(--space-4); min-width: 0; }
.toast-action {
    flex: none;
    border: none;
    cursor: pointer;
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--sky);
    background: var(--sky-soft);
    border-radius: var(--r-xs);
    padding: var(--space-2) var(--space-4);
    transition: transform var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}
.toast-action:hover { filter: brightness(0.96); transform: translateY(-1px); }
/* ========================= 19b - SKELETONS ==================================
   Placeholder blocks for content that is being fetched.

   The loading state used to be one spinner and one sentence, which means the
   layout arrives all at once and the page jumps as each region lands. A skeleton
   reserves the space first, so nothing moves when the real content replaces it --
   which is the whole reason to bother.

   They are built from a travelling gradient rather than a pulsing opacity,
   because opacity reads as "something is wrong with this block" while a sheen
   reads as "this block is on its way". The sheen is a `background-position`
   animation over a background twice the element's width, so it crosses without
   any layout change and without needing a second element.

   `will-change` is deliberately NOT set: a dozen of these can be on screen at
   once, and promoting each to its own compositor layer for a 1.4s animation is a
   lot of memory for a placeholder most people see for under a second. */
.skel {
    position: relative;
    overflow: hidden;
    background: var(--input);
    border-radius: var(--r-sm);
}
/* The highlight is a colour-mix against the token rather than a literal, so the
   sheen stays the same proportion of its own block in either theme. */
.skel::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg,
                transparent 0%,
                color-mix(in srgb, var(--sky) 16%, transparent) 50%,
                transparent 100%);
    background-size: 220% 100%;
    background-repeat: no-repeat;
    animation: skel-sweep 1.4s var(--ease) infinite;
}
@keyframes skel-sweep {
    from { background-position: 120% 0; }
    to   { background-position: -120% 0; }
}

.skel-line  { height: var(--fs-base); border-radius: var(--r-xs); }
.skel-title { height: var(--fs-lg);   width: 40%; }
.skel-pill  { height: 18px; border-radius: var(--r-full); }
.skel-block { height: 88px; border-radius: var(--r-md); }

/* The skeleton's widths, as classes rather than as `style="width:72%"` (SEC-17).
   Six one-off percentages of a loading placeholder is not what `style-src
   'unsafe-inline'` should be bought for: with those attributes gone the CSP can
   drop it, and a CSS injection could no longer reach for a selector to exfiltrate
   through. The widths stay deliberately varied -- a skeleton of one width reads
   as a rendering bug rather than as a loading state. */
.skel-w-46 { width: 46%; }
.skel-w-54 { width: 54%; }
.skel-w-64 { width: 64%; }
.skel-w-70 { width: 70%; }
.skel-w-72 { width: 72%; }
.skel-w-88 { width: 88%; }

/* SEC-17, part two: the seventeen that were constants, or a choice from a
   closed set. Every `style="` this application carried was one of two things: a
   constant, or a choice from a closed set of design tokens. Both are expressible
   as a class, and both now are. What is left inline is six bars whose width is
   a percentage computed per render -- see `docs/audit/RAPPORT.md` §5 for why
   those are still there and what they would cost to remove.

   The colours come first because they have a naming precedent: the badge family
   has been `badge-flight`, `badge-hotel`, `badge-off` since before this audit,
   so the dot family is `dot-flight`, `dot-hotel`, `dot-off`. A reader who knows
   what a flight looks like as a badge knows what it looks like as a dot, and the
   call site stops carrying a lookup table from duty type to CSS custom property
   name.

   Every rule is a single class, so the ones that are deliberately more specific
   -- `.status-pill.error .dot`, `#portal-badge .dot` -- still win, which is what
   a status pill's error dot should do. */
.dot-flight   { background: var(--flight); }
.dot-hotel    { background: var(--hotel); }
.dot-off      { background: var(--off-ink); }
.dot-standby  { background: var(--standby); }
.dot-training { background: var(--training); }
.dot-reserve  { background: var(--reserve); }
.dot-new      { background: var(--green); }
.dot-changed  { background: var(--amber); }

/* The live banner's right-hand figure. Four sentences, four colours, and the
   colour is the only thing distinguishing them at a glance -- so it is a class
   per state, named for the state and not for the hue, because the hue is a
   token that a rebrand may change.

   `12px` and `700` were literals; they are the scale's own `--fs-sm` and the
   weight the surrounding `.live-badge` already uses. The line the banner's own
   `--fs-*` step would have produced is one the scale does not have, and inventing
   a half step is exactly what L61 is about. */
.live-fig      { font-size: var(--fs-sm); font-weight: 700; }
.live-fig-base { color: var(--text-faint); font-weight: 400; }
.live-fig-air  { color: var(--sky); }
.live-fig-brief{ color: var(--amber); }
.live-fig-rest { color: var(--purple); }

/* Today's duty row: the name, and the report-to-arrival clock on the right. */
.today-duty-name { font-size: var(--fs-base); font-weight: 700; }
.today-duty-when {
    margin-left: auto;
    color: var(--text-soft);
    /* `12.5px` was a half step between `--fs-sm` and `--fs-base`. It is
       `--fs-sm`, and it is now the compact density's step, which is what the
       density setting is for. */
    font-size: var(--fs-sm);
}

/* A count sitting after a status pill, and the sub-caption under a mix row's
   label. */
.pill-count  { margin-left: var(--space-3); }
.mix-nm-sub  { font-size: var(--fs-xs); font-weight: 400; color: var(--text-faint); }
.no-duty-note{ color: var(--text-faint); }

/* The numeric headers of the destinations table carry their alignment in a rule
   of their own, down in the `.dest-table` block where it can win. It could not
   live here: see the note there. */

/* The caption under the skeleton keeps the spinner and the sentence. They are
   not redundant with the skeleton above: the skeleton shows *shape*, the caption
   says *state*, and a screen reader gets the caption alone because the skeleton
   is aria-hidden. */
.loading-caption {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    margin-top: var(--space-6);
}
.loading-caption p { margin: 0; }

/* A dashboard-shaped skeleton, shown while the roster is being read. Two columns
   because the dashboard is two columns -- reserving the wrong shape defeats the
   point, which is to stop the page jumping when the content lands. */
.skel-dash {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: var(--gap);
    margin-bottom: var(--gap);
}
.skel-dash > div { display: flex; flex-direction: column; gap: var(--space-3); }

@media (max-width: 620px) {
    .skel-dash { grid-template-columns: 1fr; }
}

/* =================== 19 ┬À LOADING / EMPTY / ERRORS ======================= */

.loading-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-11) 0;
    color: var(--text-soft);
    font-size: var(--fs-base);
}

.spinner {
    width: 38px;
    height: 38px;
    border: 3px solid var(--card-border);
    border-top-color: var(--accent-ink);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.error-banner {
    padding: var(--space-4) var(--space-5);
    margin-bottom: var(--space-5);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--accent-ink);
    background: var(--accent-soft);
    border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
    border-radius: var(--r-md);
    box-shadow: inset 0 0 24px color-mix(in srgb, var(--accent) 8%, transparent);
}

.empty-state { padding: var(--space-10) 0; text-align: center; color: var(--text-soft); }
.empty-state h2,
.empty-state h3 { margin: var(--space-4) 0 var(--space-2); color: var(--text); }
.empty-state p { font-size: var(--fs-base); }
.empty-actions { margin-top: var(--space-5); display: flex; justify-content: center; }
.empty-state .btn { padding: var(--space-3) var(--space-5); }
/* mark the empty state as decorative; the heading carries the meaning */
.empty-icon {
    position: relative;
    width: 82px;
    height: 82px;
    margin: 0 auto;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background:
        radial-gradient(circle at 32% 28%,
            color-mix(in srgb, var(--sky) 22%, transparent), transparent 68%),
        var(--surface-2);
    border: 1px solid var(--card-border);
    box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.empty-icon::before {
    content: "";
    position: absolute;
    inset: -9px;
    border-radius: 50%;
    border: 1px dashed var(--card-border-2);
    opacity: 0.8;
}
.empty-icon::after {
    content: "";
    width: 34px;
    height: 34px;
    background: var(--text-faint);
    opacity: 0.75;
    -webkit-mask: var(--plane-mask) center / contain no-repeat;
    mask: var(--plane-mask) center / contain no-repeat;
}

/* ==================== 19a ┬À MOBILE FOCUS SHEET ========================== */

/* Full-height sheet for the one thing you check in a corridor: when and
   where your next duty is. Scrolls off if the content is taller than the
   viewport, so it never traps the close button. */
/* Layer contract, written down because it used to be an accident.

   The sheet has two masters. It must sit ABOVE every dialog -- it is a phone
   surface opened from the header, and a dialog over it would trap the crew
   member in something they cannot see. It must sit BELOW the sign-in gate,
   which is the one overlay that is deliberately not dismissable: while the gate
   is up, a dismissible sheet covering it means the screen shows something you
   can close over something you cannot.

   `.modal-root` is 100, `.day-sheet` and `.toasts` 120, the gate 200. So 190 is
   the value that satisfies both, and it is now stated here rather than implied
   by two rules in different sections.

   At ≤620px this used to be raised to 240 ("the sheet is a phone surface;
   make sure it wins over any dialog"). 240 does win over the dialogs -- and it
   also wins over the gate, which is the finding. A dead-CSS removal during the
   audit deleted that override as collateral, which happened to leave 190 and so
   happened to fix the finding by luck. The value is right; the guarantee was not,
   so it is now here with a test that fails if it moves. */
.focus-sheet {
    position: fixed;
    inset: 0;
    z-index: 190;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: max(var(--space-6), env(safe-area-inset-top)) var(--space-6)
             max(var(--space-6), env(safe-area-inset-bottom));
    overflow-y: auto;
    background:
        radial-gradient(700px 420px at 82% -12%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 62%),
        radial-gradient(560px 380px at -10% 108%, color-mix(in srgb, var(--sky) 16%, transparent), transparent 60%),
        var(--bg);
    animation: focusIn 0.26s var(--ease-out) both;
}
@keyframes focusIn { from { opacity: 0; transform: translateY(12px); } }

/* L70: the one scroll lock. There were three mechanisms -- this, `body[data-modal-open]`
   at 3574, and an inline `document.body.style.overflow` set by app.js when the day
   sheet opens. They did not collide only because the inline value is *removed* rather
   than overwritten, so the last writer won by accident of ordering. */
body.focus-open,
body[data-modal-open],
body[data-scroll-lock] { overflow: hidden; }

.focus-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex: none;
}
.focus-label {
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 1.6px;
    text-transform: uppercase;
    color: var(--text-faint);
}
.focus-x {
    width: 40px;
    height: 40px;
    flex: none;
    display: grid;
    place-items: center;
    font-size: var(--fs-2xl);
    line-height: 1;
    cursor: pointer;
    color: var(--text-soft);
    background: var(--card);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-sm);
    transition: color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.focus-x:hover { color: var(--danger); transform: rotate(90deg); }

.focus-when {
    flex: none;
    margin-top: var(--space-3);
    font-size: var(--fs-base);
    font-weight: 700;
    color: var(--text-soft);
    text-transform: capitalize;
}
.focus-flight {
    flex: none;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.focus-flt {
    font-size: var(--fs-display);
    font-weight: 800;
    letter-spacing: -0.035em;
    line-height: 1;
    background: linear-gradient(155deg, var(--text) 30%, color-mix(in srgb, var(--accent) 68%, var(--text)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.focus-route {
    flex: none;
    font-size: var(--fs-display-route);
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--text);
}
.focus-times {
    flex: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
    gap: var(--space-3);
    margin-top: var(--space-4);
}
.focus-times > div {
    padding: var(--space-4) var(--space-4);
    border-radius: var(--r-md);
    background: var(--card);
    border: 1px solid var(--card-border);
    box-shadow: var(--shadow-sm);
}
.ft-key {
    display: block;
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.9px;
    text-transform: uppercase;
    color: var(--text-faint);
}
.ft-val {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--fs-xl);
    font-weight: 800;
    letter-spacing: -0.02em;
    font-variant-numeric: var(--tnum);
}

/* Short viewports (landscape phones, small SE): tighten the vertical rhythm
   so the whole duty fits without scrolling. */
@media (max-height: 620px) {
    .focus-sheet { padding-top: max(var(--space-4), env(safe-area-inset-top)); gap: var(--space-2); }
    .focus-when { margin-top: var(--space-2); font-size: var(--fs-base); }
    .focus-flt { font-size: var(--fs-display-short); }
    .focus-route { font-size: var(--fs-display-route-short); }
    .focus-times { gap: var(--space-3); margin-top: var(--space-3); }
    .focus-times > div { padding: var(--space-3) var(--space-4); }
    .ft-val { font-size: var(--fs-lg); margin-top: var(--space-0); }
    .focus-rest { margin-top: var(--space-3); padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); }
    .focus-go { padding-top: var(--space-4); padding-bottom: var(--space-4); }
}
@media (max-height: 460px) {
    .focus-times { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .focus-rest { display: none; }
}
.focus-rest {
    flex: none;
    margin-top: var(--space-4);
    padding: var(--space-4) var(--space-4);
    border-radius: var(--r-md);
    font-size: var(--fs-base);
    font-weight: 700;
}
.focus-rest.ok {
    background: var(--green-soft);
    color: var(--green-ink);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--green) 28%, transparent);
}
.focus-rest.bad {
    background: var(--danger-soft);
    color: var(--danger);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 34%, transparent);
}
/* push the action to the bottom without leaving a dead void when the
   content is short: spacer absorbs the slack, button stays put */
.focus-spacer { flex: 1 1 auto; min-height: 18px; }
.focus-go { flex: none; padding-top: var(--space-4); padding-bottom: var(--space-4); }
.focus-none {
    margin: auto 0;
    text-align: center;
    font-size: var(--fs-md);
    color: var(--text-soft);
}

/* The dashboard already shows the countdown, so the sheet is a touch-first
   affordance. Gate on pointer type, not width: a landscape phone is 844px
   wide but still needs it. */
@media (hover: hover) and (pointer: fine) {
    #focus-btn { display: none; }
}

/* (.focus-sheet { animation: none } under prefers-reduced-motion now lives in
   the one consolidated block in section 22.) */

/* ======================= 20 ┬À FOOTER & KEYCAPS =========================== */

footer {
    text-align: center;
    padding: var(--space-7) 0 var(--space-3);
    font-size: var(--fs-sm);
    color: var(--text-faint);
}
footer p { font-weight: 600; letter-spacing: 0.2px; }
.foot-mark {
    width: 74px;
    height: 3px;
    margin: 0 auto var(--space-4);
    border-radius: var(--r-xl);
    background: linear-gradient(90deg, transparent,
                color-mix(in srgb, var(--accent) 60%, transparent),
                color-mix(in srgb, var(--sky) 60%, transparent), transparent);
}

.kbd-row {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-5);
    margin-top: var(--space-3);
    font-size: var(--fs-xs);
}
.kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 19px;
    padding: var(--space-0) var(--space-2);
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--text-soft);
    background: linear-gradient(180deg, var(--card), var(--surface-2));
    border: 1px solid var(--card-border-2);
    border-bottom-width: 2px;
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-xs);
}

/* =========================== 21 ┬À OPS CHECKS ============================= */

.ops-item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-3);
    margin: 0 calc(-1 * var(--space-3));
    border-radius: var(--r-sm);
    font-size: var(--fs-base);
    color: var(--text-soft);
    border-bottom: 1px solid color-mix(in srgb, var(--card-border) 60%, transparent);
    transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.ops-item:last-child { border-bottom: none; }
/* The pointer belongs to a row that actually jumps to its date, and to nothing
   else. `.ops-item` used to carry `cursor: pointer` and this hover nudge for
   every row, while the handler it belonged to matched the date against the
   rendered -- translated -- sentence and therefore matched nothing in French.
   Every row looked live and none of them were. `.ops-holes` below already had to
   opt out by hand; this makes the opt-out the default rather than the exception,
   so a new kind of ops row cannot inherit a lie. */
.ops-item.ops-jump { cursor: pointer; }
.ops-item.ops-jump:hover { background: var(--surface-2); transform: translateX(3px); }

/* A missing month is a finding, and it carries the repair with it: an alert with
   no way to act on it is just a complaint. The button sits at the end so the row
   reads as "here is the problem, here is the fix" rather than as two things. */
.ops-holes { align-items: center; flex-wrap: wrap; cursor: default; }
.ops-holes:hover { transform: none; }
.ops-holes .btn {
    margin-left: auto;
    flex: none;
    padding: var(--space-1) var(--space-3);
    font-size: var(--fs-xs);
}

.ops-icon {
    flex: none;
    width: 22px;
    height: 22px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
    font-weight: 800;
}
.ops-bad  { color: var(--accent-ink); }
.ops-warn { color: var(--warning); }
.ops-info { color: var(--sky); }
.ops-bad .ops-icon  { background: var(--accent-soft); color: var(--accent-ink); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent); }
.ops-warn .ops-icon { background: var(--amber-soft); color: var(--warning); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--warning) 30%, transparent); }
.ops-info .ops-icon { background: var(--sky-soft); color: var(--sky); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sky) 25%, transparent); }

/* ---- "why is this flagged" -------------------------------------------------
   A rest row is a sentence with two numbers in it, and a number with no source
   is a complaint. This is the disclosure that carries the rest: the rule and its
   version, the source of the threshold, the two instants measured between, how
   short by, the date of the data -- and, in `.ops-caveat`, the sentence that
   stops a red row reading as a legal finding.

   It is its own control rather than the row's, because the row is already a link
   to that day: making the explanation the click target would mean losing the
   row to find out why it fired. */
.ops-item { flex-wrap: wrap; }
.ops-why {
    margin-left: auto;
    flex: none;
    padding: 0 var(--space-2);
    background: none;
    border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
    border-radius: var(--r-sm);
    color: inherit;
    font: inherit;
    font-size: var(--fs-xs);
    font-weight: 700;
    cursor: pointer;
    opacity: 0.85;
}
.ops-why:hover { opacity: 1; background: var(--surface-2); }
.ops-why:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ops-detail {
    flex: 1 0 100%;
    margin: var(--space-2) 0 var(--space-1) 30px;
    padding: var(--space-3);
    background: var(--surface-2);
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
    color: var(--text-soft);
}
.ops-detail.hidden { display: none; }
.ops-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 2px var(--space-3);
    margin: 0;
}
.ops-facts dt { color: var(--text-faint); font-weight: 700; }
.ops-facts dd { margin: 0; font-variant-numeric: tabular-nums; }
.ops-basis { margin: var(--space-2) 0 0; font-style: italic; }
.ops-caveat {
    margin: var(--space-2) 0 0;
    padding-top: var(--space-2);
    border-top: 1px dashed color-mix(in srgb, var(--card-border) 70%, transparent);
    color: var(--text-faint);
}

/* The same honesty one level up: the dashboard banner says which readings it
   could not establish. Without the hook this line is unstyled and inherits the
   banner's body colour, which is the one thing a caveat must not do. */
.live-uncert {
    margin-top: var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-faint);
}

/* ================ 22 ┬À MOTION / RESPONSIVE / PRINT ======================= */

/* `blob`, `blob2`, `routeDraw` and `bob` were removed with the retired hero
   decoration that used them; nothing referenced them but each other. */
@keyframes breathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}
@keyframes livePulse {
    0%   { transform: scale(0.9); box-shadow: 0 0 0 0 currentColor; }
    70%  { transform: scale(1);   box-shadow: 0 0 0 8px transparent; }
    100% { transform: scale(0.9); box-shadow: 0 0 0 0 transparent; }
}
@keyframes growBar {
    from { transform: scaleY(0.02); opacity: 0.2; }
    to   { transform: scaleY(1); opacity: 1; }
}
@keyframes growBarX {
    from { transform: scaleX(0.02); }
    to   { transform: scaleX(1); }
}
@keyframes hubPulse {
    0%   { r: 4; opacity: 0.9; stroke-width: 2.2; }
    80%  { opacity: 0.15; }
    100% { r: 22; opacity: 0; stroke-width: 0.5; }
}

/* body lock while a dialog is open */
body[data-modal-open] { overflow: hidden; }

@media (max-width: 860px) {
    /* One column. Below this the two halves are under ~420px each, which is
       too narrow for the By-month row (month, three figures and the portal
       figure on one line) and buys nothing over stacking. */
    .mix-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 1100px) {
    /* The tablet gap. The 620px phone wrap below fixes the crushed header, but
       the crush starts higher: measured at 768px, `.hero-top` shrank under
       `.hero-actions` (flex crush with nowhere to shrink to) and the language
       toggle at x=352 painted underneath the actions row starting at x=170 --
       visible, enabled, and unreachable by touch or mouse. The phone query's
       620px bound left 621-1100px in the broken state, which is every portrait
       tablet and every narrow desktop window.
       Same treatment, narrower scope: only the bar wraps here. The calendar,
       stats and sheet rules below stay phone-only -- at 768px they fit. This
       block sits before the 620px one on purpose, so the phone rules win ties
       under 620px. */
    .hero {
        height: auto;
        flex-wrap: wrap;
        row-gap: var(--space-2);
    }
    .hero-actions {
        flex: 1 1 100%;
        flex-wrap: wrap;
        justify-content: flex-start;
        row-gap: var(--space-2);
    }
    .hero-top { flex-wrap: wrap; row-gap: var(--space-2); }
    /* The bar is taller than --appbar-h now, so the sticky tab offset derived
       from it would slide the tabs underneath: same reason as below. */
    .tabs { position: static; }
}

@media (max-width: 620px) {
    /* The bar wraps rather than clipping. Measured at 360 px before this: the
       document scrolled to 491 px inside a 360 px viewport, 131 px of it cut off,
       and five header buttons including Data could not be reached at all --
       `.hero-actions` is `flex: none` with `flex-wrap: nowrap` and `.btn` is
       `white-space: nowrap`, so the block could not shrink, and `body` has
       `overflow-x: hidden`, which clips instead of scrolling. The old padding
       here was also 20px + 24px inside a 52px box. */
    .hero {
        height: auto;
        flex-wrap: wrap;
        row-gap: var(--space-2);
        padding: var(--space-3) var(--space-4);
    }
    .hero-actions {
        /* flex: 1 1 100% puts the actions on their own row and, crucially,
           constrains them to the hero's content box. Left as `flex: 0 0 auto`
           they keep their 467px max-content width and wrap *inside* a box that
           is already wider than the phone, so the wrap never helps. */
        flex: 1 1 100%;
        flex-wrap: wrap;
        justify-content: flex-start;
        row-gap: var(--space-2);
    }
    /* Same story one level down. .hero-top holds the brand, the status pills
       and the icons on one nowrap row; measured at 360 px its children ran
       14px past its own right edge, which is the 2px the document still
       clipped. Let the row wrap instead of assuming it fits. */
    .hero-top { flex-wrap: wrap; row-gap: var(--space-2); }
    .brand { min-width: 0; }
    /* The hero is now taller than --appbar-h, so a sticky offset derived from it
       would be a lie. Let the tabs scroll with the page, which is what a phone
       expects anyway. */
    .tabs { position: static; }
    main { padding: var(--space-2) var(--space-4) var(--space-9); }
    .tabs { padding: var(--space-3) var(--space-4); }
    .stats { grid-template-columns: repeat(3, 1fr); }
    .stats-cards { grid-template-columns: repeat(2, 1fr); }
    .cal-cell { min-height: 70px; padding: var(--space-2); }
    .cal-dow span { font-size: var(--fs-3xs); padding: var(--space-3) 0; letter-spacing: 0.4px; }
    .cal-evt { font-size: var(--fs-3xs); padding: var(--space-0) var(--space-2); border-left-width: 2px; }
    /* L75: a 52px cell cannot hold "ALG → CZL" *and* a time. Measured at 390px the
       chip truncated to "A 12:30" -- one letter of the departure airport and the
       time, which reads as a misprint rather than as an abbreviation, and the
       route was the part a crew member scans for.

       So on a phone a flight chip stops trying to be a summary and becomes a
       mark: the departure time alone, in the category's own colour on its own
       left border. The time is what the day sheet then opens on, and the day
       sheet is one tap away. The route is not lost -- it is in the Week, Day and
       List views, and it is the tap.

       Flights and deadheads only -- both carry a departure time. A layover, a
       standby and a training day have no time to show, so their chip is *only*
       its label; hiding that as well left a bare coloured sliver, and an event
       identified by nothing but its colour is the one thing a calendar must
       never do. */
    .cal-evt.t-flight .chip-txt,
    .cal-evt.t-deadhead .chip-txt { display: none; }
    .cal-evt.t-flight .cal-evt-time,
    .cal-evt.t-deadhead .cal-evt-time { margin-left: 0; }
    /* The month summary stops being six cards. At 390px those six tiles were two
       full rows -- about 110px -- before the first day of the month appeared, and
       two of the six read "0". They flow inline here instead, so the six figures
       wrap onto about two short lines and the grid starts a third of the way up
       the screen rather than halfway down.

       `display: flex` is doing the work and was missing: `.stat-card` is a plain
       block with `text-align: center`, so `flex-direction: row` on it is a
       no-op and the label simply wrapped under the figure on its own. The flow is
       on the *container* here, which is what lets a tile be as wide as its own
       text instead of a third of the screen. */
    #pane-calendar .stats {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2) var(--space-3);
    }
    #pane-calendar .stat-card {
        display: flex;
        flex: 0 0 auto;
        align-items: baseline;
        gap: var(--space-2);
        padding: var(--space-1) var(--space-3);
        border-radius: var(--r-sm);
        text-align: start;
    }
    #pane-calendar .stat-card::before,
    #pane-calendar .stat-card::after { display: none; }
    #pane-calendar .stat-num { font-size: var(--fs-base); letter-spacing: 0; }
    #pane-calendar .stat-label { margin-top: 0; font-size: var(--fs-3xs); letter-spacing: 0.4px; }
    .wk-col { min-height: 100px; padding: var(--space-2); }
    .wk-head .dnum { font-size: var(--fs-md); }
    /* `.mix-grid` is already one column below 860px, so the 620px block has
       nothing left to say about it. The `.stats` tile row still does. */
    .dash-grid { grid-template-columns: 1fr; }
    /* retune the density vars rather than hardcoding padding, so the
       compact setting still wins on a phone */
    .day-card {
        --dc-pad-y: 12px;
        --dc-pad-x: 12px;
        --dc-gap: 11px;
        --dc-rail: 42px;
    }
    [data-density="compact"] .day-card {
        --dc-pad-y: 8px;
        --dc-pad-x: 10px;
        --dc-gap: 9px;
        --dc-rail: 38px;
    }
    .day-rail { padding-right: var(--space-3); }
    .day-num { font-size: var(--fs-xl); }
    [data-density="compact"] .day-num { font-size: var(--fs-xl); }
    .month-info { min-width: 130px; }
    .month-label { font-size: var(--fs-lg); }
    .toasts { right: 12px; left: 12px; bottom: 12px; max-width: none; }
    .modal-root { padding: var(--space-4); }
}

@media (max-width: 420px) {
    .brand-text h1 { font-size: var(--fs-xl); }
    .brand-mark { width: 44px; height: 44px; border-radius: var(--r-lg); }
    .hero-actions .btn { flex: 1 1 auto; }
    .field-row { flex-direction: column; }
}

/* THE reduced-motion block. There were five of these, at 2642, 2944, 3576, 3800
   and 4605, and the outcome depended on source order:

   * 2642 (.gate-submit spinner) and 2944 (#bg-import dot) carried no
     !important, so they survived only because they happened to come after the
     rules they cancel. Reorder the file and they stop working silently.
   * 3576 (.focus-sheet) had the same problem.
   * 4605 repeated the blanket reset verbatim and, being later, was what actually
     won -- so the "stop them outright" intent of 3800 was defeated for anything
     not on its short list. .live-pulse-dot was never on any list.

   Measured before this consolidation, with prefers-reduced-motion: reduce
   emulated, .live-pulse-dot still computed animation-name livePulse,
   animation-duration 1e-05s, animation-iteration-count 1 and
   animation-play-state running: an animation still instantiated and running,
   fast-forwarded rather than stopped, which is what the comment here says should
   not happen.

   Everything below is !important, so nothing can lose on source order, and the
   named list is exhaustive -- every `infinite` animation the file declares
   (breathe, livePulse, hubPulse, gate-spin, bg-pulse, spin) appears in it.
   One-shot animations (fadeIn, pop, rise, focusIn, toastIn/Out, growBar,
   growBarX) are left to the blanket reset, which collapses them to a single
   instant frame, which is right: they are entrances, not loops. */
@media (prefers-reduced-motion: reduce) {
    html, body, *, *::before, *::after {
        scroll-behavior: auto !important;
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0s !important;
    }
    /* Stop the loops outright rather than fast-forwarding them, so nothing
       keeps repainting in the background. */
    .status-pill .dot,
    .live-pulse-dot,
    .map-hub-ring,
    .gate-submit[disabled] .gate-spinner,
    /* Added by the cockpit pass. The clock's "these are live" light and the
       stepper's active ring are both infinite loops, and both are exactly the
       kind of motion the preference exists to suppress -- a slow 3.2s breathe
       and a 1.9s pulse are gentle, but they never stop. Listed by name rather
       than left to the blanket rule above, because that rule only shortens the
       duration: an infinite animation with a 0.01ms duration still runs. */
    .clock-live,
    .gate-step.is-active .gate-step-dot,
    #bg-import .dot,
    .spinner,
    .focus-sheet,
    /* Refonte Flighty-Pro (section 24): the ticket's dash draw, the status
       pulse and the hub beacon are infinite loops of the same kind, and the
       modal pop is an entrance animation. Listed here -- the single block M47
       allows -- so the preference stills them with everything else. */
    .bento-hero .bp-arc-path,
    .bento-hero .bp-pulse,
    .bento-hero .bp-status.is-live .bp-pulse,
    #route-map .map-hub-ring,
    .modal-root,
    .modal-root .modal {
        animation: none !important;
    }
    /* The stepper's states are carried by colour and the tick, not by motion, so
       cancelling the loop costs nothing: the active step is still the one with
       the bright ring. */
    .gate-step.is-active .gate-step-dot {
        border-color: var(--sky);
        background: color-mix(in srgb, var(--sky) 16%, transparent);
    }
    .bg-import-bar { transition: none !important; }
}

/* One @media print block (L64). There were two, 800 lines apart, the second repeating
   `.hero`, `.tabs` and `.toasts` from the first and adding the day sheet. Repeating a
   selector across two print blocks is harmless until someone edits one of them. */
@media print {
    .hero, .tabs, .status-pill-row, .view-switch, .month-nav, .chips,
    .stats, .modal-root, .toasts, .print-hide, .empty-icon,
    .day-actions, .map-chips, footer,
    .day-sheet, .day-sheet-scrim {
        display: none !important;
    }

    /* The planning views (app-views.js). The doors and the chips are
       interaction -- a dead control on paper is noise -- while the rotation
       timeline and the flight sheet are the part a briefing actually
       reads, so they are kept and made unbreakable across a page. */
    .sheet-nav, .sheet-foot, .sync-actions, .stats-doors, .search-hint {
        display: none !important;
    }
    .round-sector, .round-hotel, .sheet-block {
        break-inside: avoid;
    }

    body {
        background: #fff;
        color: #000;
        /* L64: was a hand-picked `11pt`. Points are the right unit on paper -- a
           rem scale is a screen setting -- so it becomes a token rather than a
           number chosen once and never revisited. `padding: 0` below is kept on
           purpose: `@page { margin: 12mm }` at the end of this block is what puts
           the margin on paper, and adding it back here would print it twice. */
        font-size: var(--fs-print);
    }
    body::after { display: none; }
    main { max-width: 100%; padding: 0; }
    .pane { display: none !important; }
    .pane.active { display: block !important; }

    /* `.trip-card` was here and matched nothing; `.rot` is what tripCard()
       emits, and it is a bordered card like the rest of the list. */
    .calendar-view, .week-view, .day-view, .days, .rot,
    .dash-card, .chart-card, .stat-card, .day-card {
        box-shadow: none;
        border-color: #ccc;
        break-inside: avoid;
    }
    .cal-cell { min-height: 60px; }
    .cal-cell:hover, .cal-cell:hover::before { background: none; border-color: transparent; }
    .tab-btn { display: none; }
    .dash-title { color: #333; }

    /* The five headline numbers are painted with a gradient clipped into the
       text: `color: transparent` plus `background-clip: text`. Printers default
       to `print-color-adjust: economy`, which suppresses backgrounds -- so
       transparent text over no painted background comes out as nothing.
       Measured with the print media emulated: color rgba(0,0,0,0),
       -webkit-text-fill-color rgba(0,0,0,0). The Stats tiles' big numbers and
       every flight number in the detail sheet printed blank.
       Give them a real colour instead. -webkit-text-fill-color has to be reset
       as well, or the gradient keeps winning. */
    .stat-num, .dash-big, .modal-head h2, .modal-head h3,
    .detail-hero .flight-num, .focus-flt {
        background: none;
        color: #000;
        -webkit-text-fill-color: #000;
    }
    /* Same problem for the bars: a background-only fill is suppressed, so the
       monthly block-hours chart and every duty-mix bar printed empty. A hairline
       outline survives, which keeps the shape readable without a fill. */
    .chart .bar, .mix-bar {
        background: none !important;
        border: 1px solid #666;
    }
    @page { margin: 12mm; }
}
/* --- 23b KPI gauges, area chart, map preview -------------------------------
   Five cards, each a figure, a ring and a bar showing the same proportion in two
   registers: the ring for a glance, the bar for comparison against its
   neighbours. Two encodings of one number is not redundancy here -- a ring is
   read in a circle and a bar is read against a straight edge, and which one
   answers your question depends on which you are comparing against.

   The ring is a rotated SVG circle with a dash offset. It is rotated -90deg so
   the dash starts at twelve o'clock rather than at three; without it the gauge
   appears to start a quarter of the way round, which reads as "a quarter used"
   on an empty year. */
.kpi-row {
    display: grid;
    /* Five cards, since Pairings moved up out of the year tile row (L74).
       `minmax(0, 1fr)` so a long figure sets no track's minimum -- the caption
       under each one is prose, and a bare `1fr` would let it widen the column. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--gap);
    margin-bottom: var(--gap);
}
.kpi {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
        "dial figures"
        "bar  bar"
        "cap  cap";
    align-items: center;
    gap: var(--space-3) var(--space-4);
    padding: var(--space-4);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    transition: border-color var(--dur-fast) var(--ease);
}
.kpi:hover { border-color: var(--card-border-2); }

.kpi-dial {
    grid-area: dial;
    width: 46px;
    height: 46px;
    transform: rotate(-90deg);
    overflow: visible;
}
.kpi-track {
    fill: none;
    stroke: var(--card-border);
    stroke-width: 4;
}
.kpi-arc {
    fill: none;
    stroke: var(--sky);
    stroke-width: 4;
    stroke-linecap: round;
    transition: stroke-dashoffset var(--dur) var(--ease-out),
                stroke var(--dur-fast) var(--ease);
}
/* Past the plan. Only the FTL card can actually reach this in normal use, and
   when it does it is the one number on the page that has a legal consequence --
   so it goes coral, which is the app's alert role, not amber. */
.kpi.is-over .kpi-arc { stroke: var(--danger); }

/* The three “so far this year” cards use the sky ramp: they measure progress
   through time, which is what blue has meant in this app since the tokens were
   written.

   The FTL card is deliberately NOT given the elapsed-time treatment, and this
   is the correction the gauges were rewritten for. It is measured against the
   annual flight time limitation — a real regulatory cap at 900 hours, where
   going over has consequences — so it earns amber as “approaching a limit” and
   coral as “past it”. The first version divided all four by invented “plans” and
   lit this one coral on a perfectly ordinary 569 hours: 63% of the real cap and
   a third of the way through the year. A card that cries wolf on a normal
   schedule teaches people to ignore it, which costs the one alert on this page. */
.kpi[data-kpi="ftl"] .kpi-arc { stroke: var(--amber); }
.kpi[data-kpi="ftl"] .kpi-bar i { background: var(--amber); }
.kpi[data-kpi="ftl"].is-over .kpi-arc { stroke: var(--danger); }
.kpi[data-kpi="ftl"].is-over .kpi-bar i { background: var(--danger); }
.kpi.is-over .kpi-bar i { background: var(--danger); }

.kpi-figures {
    grid-area: figures;
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.kpi-num {
    font-size: var(--fs-xl);
    font-weight: 800;
    line-height: 1.15;
    letter-spacing: -0.01em;
    color: var(--text);
    font-variant-numeric: var(--tnum);
}
.kpi-label {
    font-size: var(--fs-label);
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kpi-bar {
    grid-area: bar;
    height: 4px;
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--text-faint) 16%, transparent);
    overflow: hidden;
}
.kpi-bar i {
    display: block;
    height: 100%;
    width: 0;
    border-radius: var(--r-full);
    background: linear-gradient(90deg, var(--sky), var(--flight-ink));
    transition: width var(--dur) var(--ease-out),
                background var(--dur-fast) var(--ease);
}

/* The caption states what the fraction is *of*. A gauge with no denominator
   invites the reader to assume it is a limit, and three of these four are not. */
.kpi-cap {
    grid-area: cap;
    font-size: var(--fs-3xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}

@media (max-width: 1100px) {
    /* Three across, not two: five cards in two columns leaves one card alone on
       a row of its own, which reads as a mistake rather than as a layout. */
    .kpi-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
    /* Two across, and the card turns on its side (see the block below). Five
       full-height cards stacked in a phone column pushed the chart, the duty mix
       and the monthly table off the bottom of the first screen. */
    .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* The ring and the bar are two encodings of one number. On a phone there is
       no neighbour to compare against and no room for both, so the ring goes and
       the bar stays -- it is the one that reads against the card's own edge. */
    .kpi {
        grid-template-areas:
            "figures figures"
            "bar bar"
            "cap cap";
        gap: var(--space-2) var(--space-3);
        padding: var(--space-3);
    }
    .kpi-dial { display: none; }
    .kpi-num { font-size: var(--fs-xl); }
}

/* --- 23c Gradient area chart ----------------------------------------------
   The drawing is geometry and stretches; the labels are text and must not.

   The SVG keeps `preserveAspectRatio="none"` so the plot fills its card at any
   width, which is what makes it work from a 360px phone to a 2400px display
   without a second layout. Everything inside it is a fill, a stroke or a hit
   target: the gridlines and the line carry `vector-effect: non-scaling-stroke`
   so a 1px rule stays 1px whatever the box does.

   What used to be inside the SVG was not geometry. The month ticks and the dots
   were, and the measured cost of that was real: at a 1398px card the 720-unit
   viewBox is stretched 1.94x horizontally, so "Jan" rendered visibly
   letter-spaced and the round dots came out as ellipses. So the labels moved
   out into HTML around the SVG, where the browser lays them out at their own
   size, and the dots went entirely -- the line already carries the shape, which
   is what the old comment here was reaching for when it said to keep them
   small enough that the stretch was not visible.

   It also fixes the unlabelled axes: three gridlines at 25/50/75% of the year's
   busiest month said nothing about what the height of anything was. There is a
   real scale now, and it is stated. */
.chart { width: 100%; }

/* The frame: a value gutter on the left, the plot beside it, the month row
   under both. `--chart-h` is the plot's height and nothing else; the labels sit
   outside it, so raising it does not stretch the type.

   `flex: 1` + `width: 100%` because `.chart` above is still the legacy flex bar
   container (`L73` pins its `min-height` and the `.bar-val` rules that test
   measures), and a flex item sizes to its content in the main axis: without
   these two the frame stopped at 70% of the card and the right-hand third of the
   chart was empty. The `min-height` and the `flex` are not in tension -- at any
   real size the frame is the taller of the two. */
.area-frame {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto;
    grid-template-areas:
        "ylab plot"
        ".    xlab";
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    align-self: stretch;
    column-gap: var(--space-3);
    row-gap: var(--space-2);
}
.area-y {
    grid-area: ylab;
    display: grid;
    align-content: space-between;
    justify-items: end;
    text-align: right;
    /* The gutter's three labels sit at the top, the midpoint and the bottom of
       the plot, so it is exactly the plot's height with no fixed value of its
       own -- `align-content: space-between` against a row the plot defines. */
}
.area-y span {
    font-size: var(--fs-3xs);
    font-weight: 600;
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
    white-space: nowrap;
    line-height: 1;
}
.area-y span:first-child { transform: translateY(-50%); }
.area-y span:last-child { transform: translateY(50%); }

.area-plot {
    grid-area: plot;
    position: relative;
    min-width: 0;
}

/* A 3.1:1 plot. The old viewBox was 720x200 (3.6:1) with `height: auto`, so a
   1398px card rendered it 388px tall -- two thirds of that height empty under a
   line whose own range used the top third of it. `clamp` rather than a fixed
   value so a narrow card still gets a plot you can read a month name in, and a
   wide one does not get a billboard. */
.area-chart {
    display: block;
    width: 100%;
    height: clamp(150px, 17vw, 240px);
}

.area-x {
    grid-area: xlab;
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: var(--text-faint);
}
.area-x span { text-align: center; min-width: 0; }
/* A month with no data yet is dimmed, exactly as it is on the chart's own axis.
   It stays legible rather than hidden: knowing the year has not reached December
   is the point of showing December at all. */
.area-x span.is-unknown { opacity: 0.5; }

.area-fill { fill: url(#area-fill); }
.area-line {
    fill: none;
    stroke: url(#area-stroke);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}
.area-grid {
    stroke: var(--card-border);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}
/* The baseline. The gridlines above are 25/50/75%; without a rule at zero the
   filled area has nothing to sit on, and "0 h" was a line you had to infer. */
.area-base { stroke: var(--card-border-2); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* The hit column sits above the drawing and is invisible; it is the click and
   keyboard target for a month, so it is a real focusable button rather than a
   click handler on the artwork. */
.area-hit {
    fill: transparent;
    cursor: pointer;
    transition: fill var(--dur-fast) var(--ease);
}
.area-hit:hover { fill: color-mix(in srgb, var(--sky) 8%, transparent); }
.area-hit:focus-visible {
    fill: color-mix(in srgb, var(--sky) 12%, transparent);
    outline: 2px solid var(--sky);
    outline-offset: -2px;
}

/* Months with no data yet, as opposed to months with no duties.

   A roster imported to date has nothing at all for the months that have not
   happened, and plotting those at zero draws a cliff at the right-hand edge that
   reads as "you stopped working in November". The line skips them, their ticks
   and columns are dimmed, and the gap is shaded so it reads as *absent* rather
   than as *empty* -- which is the difference between "no data" and "no work". */
.area-hit.is-unknown { fill: transparent; }
.area-hit.is-unknown:hover { fill: color-mix(in srgb, var(--text-faint) 8%, transparent); }
.area-x.is-unknown { fill: var(--text-faint); opacity: 0.45; }
.area-future {
    fill: color-mix(in srgb, var(--text-faint) 5%, transparent);
}

/* The map arc, when a destination row is hovered in the summary table. */
.map-route.is-preview {
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--sky) 70%, transparent));
}

/* === 23 · COCKPIT COMPONENTS ================================================
   Appended last so it overrides the legacy sections above without needing to
   rewrite 3,000 lines in place. Same DOM, same ids, same JS hooks — this is
   presentation only. Matte surfaces, 1px borders, tabular figures, no glow.
   ========================================================================== */

/* --- 23.1 Responsive 12-column spans ------------------------------------- */
@media (max-width: 1100px) {
    .dash-grid > .dash-card:nth-child(1),
    .dash-grid > .dash-card:nth-child(2) { grid-column: span 12; }

    /* Below 620px the grid is one column, so `span 12` above created eleven
       implicit tracks and the dashboard overflowed its own viewport at exactly the
       width where that is hardest to scroll past. The reset has to repeat the
       `:nth-child` pseudo-classes rather than name `.dash-card` plainly: those make
       the rule above specificity (0,3,0), and specificity is settled before source
       order is consulted, so a plainer selector loses no matter where it sits.

       The spans above are correct for 621-1100px, where the grid really is twelve
       wide. Below that a card should fill the row, and `1 / -1` rather than
       `span 12` says that in terms that survive a change of column count.

       (An earlier version of this comment claimed the problem was cascade order --
       as the audit does. It is not. Putting this block after the 1100px one changed
       nothing measurable, because it was never order deciding it.) */
}
@media (max-width: 620px) {
    .dash-grid > .dash-card:nth-child(1),
    .dash-grid > .dash-card:nth-child(2),
    .dash-grid > .dash-card { grid-column: 1 / -1; }
}

/* --- Bento grid -----------------------------------------------------------
   Twelve columns, hero on eight, layover on four. The two are read as a single
   question -- where am I going, and how long have I got -- so they sit on the
   same row; the old layout put the layover in a banner *under* both cards,
   which separated the two answers the crew member is actually holding together.

   The collapse points are 1100 and 620, matching `.dash-grid` below. They are
   `1 / -1` rather than `span 12` because the column count is a detail of this
   block and `1 / -1` says "fill the row" in terms that survive a change to it. */
.bento {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--gap);
    margin-bottom: var(--gap);
    align-items: start;
}
.bento-hero { grid-column: span 8; }
.bento-stay { grid-column: span 4; }

@media (max-width: 1100px) {
    .bento-hero, .bento-stay { grid-column: span 12; }
}
@media (max-width: 620px) {
    .bento-hero, .bento-stay { grid-column: 1 / -1; }
}

/* --- Boarding pass --------------------------------------------------------
   The hero is treated like a boarding pass: a punched card. That is one long
   edge of paired notches and a perforation line between the route and the
   detail block, and it is drawn rather than imaged so it stays sharp at any DPR
   and costs nothing.

   The notches are radial-gradients positioned outside the card's own box. They
   have to be *behind* the content and must not be clipped, which is why the
   hero carries `overflow: visible` -- the default -- while `.dash-card` sets
   `overflow: hidden` to stop long chips escaping their cell. The hero opts back
   out of that. */
.bento-hero { overflow: visible; }
.bento-hero::before,
.bento-hero::after {
    content: "";
    position: absolute;
    top: 62%;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--bg);
    box-shadow: 0 0 0 1px var(--card-border);
    transform: translateY(-50%);
    pointer-events: none;
    z-index: 1;
}
.bento-hero::before { left: 0; }
.bento-hero::after { right: 0; }

/* The perforation. Two dashed halves meeting at the notches, so the eye reads
   "tear here" rather than "there is a line". */
.bento-hero .dash-body {
    position: relative;
}
.bento-hero .flight-strip {
    position: relative;
    border: 0;
    background: none;
    padding: 0;
    gap: var(--space-5);
}
.bento-hero .flight-strip::after {
    content: "";
    position: absolute;
    left: calc(var(--space-3) * -1);
    right: calc(var(--space-3) * -1);
    top: 62%;
    height: 0;
    border-top: 1px dashed var(--card-border-2);
    pointer-events: none;
}
/* The card's own title row is the pass header; the border under it becomes the
   punch line the route sits on. */
.bento-hero .dash-title {
    border-bottom: 1px solid var(--card-border);
    padding-bottom: var(--space-3);
    margin-bottom: var(--space-4);
}

/* --- Escale card ----------------------------------------------------------
   Rows of key/value, not prose. A hotel name and a rest duration are facts, and
   facts read better as a two-column list than as a sentence assembled at
   runtime -- and the sentence version could not be translated word by word. */
.stay-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}
.stay-state {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-label);
    font-weight: 800;
    letter-spacing: 0.7px;
    text-transform: uppercase;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-full);
}
.stay-state.is-active {
    color: var(--hotel-ink);
    background: color-mix(in srgb, var(--green) 14%, transparent);
    border: 1px solid var(--hotel-line);
}
.stay-state.is-next {
    color: var(--text-soft);
    background: var(--input);
    border: 1px solid var(--card-border);
}
/* The countdown is the one number on this card that is changing, so it is the
   only one given the mono face and the full-strength ink. */
.stay-count {
    font-size: var(--fs-base);
    font-weight: 800;
    color: var(--hotel-ink);
    font-variant-numeric: var(--tnum);
    white-space: nowrap;
}
/* Pack excellence §1: discreet badge when the stop airport was inferred from
   the bracketing flights rather than named by the portal. */
.stay-inferred {
    font-size: var(--fs-meta);
    font-weight: 700;
    letter-spacing: 0.4px;
    color: var(--text-faint);
    border: 1px dashed var(--card-border-2);
    border-radius: var(--r-full);
    padding: var(--hairline) var(--space-2);
    white-space: nowrap;
}

.stay-rows {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.stay-row {
    display: grid;
    grid-template-columns: 5.5rem 1fr;
    gap: var(--space-3);
    align-items: baseline;
}
.stay-key {
    font-size: var(--fs-label);
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
}
.stay-val {
    font-size: var(--fs-sm);
    color: var(--text);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The weather line at the foot of the card. Quieter than the rows above it: it
   is context, not a roster fact, and it should not compete with the rest time
   for the eye. */
.stay-wx {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--card-border);
    font-size: var(--fs-sm);
}
.stay-wx.is-empty {
    color: var(--text-faint);
    font-size: var(--fs-label);
    font-style: italic;
}
.stay-wx-apt {
    font-weight: 800;
    letter-spacing: 0.06em;
    color: var(--text);
}
.stay-wx-temp { font-weight: 700; color: var(--sky-ink); }
.stay-wx-name { color: var(--text-soft); min-width: 0; }
.stay-wx-range {
    margin-left: auto;
    color: var(--text-faint);
    font-size: var(--fs-label);
    font-variant-numeric: var(--tnum);
}

/* --- 23.2 Flight strip ---------------------------------------------------- */
/* Modelled on an electronic flight progress strip: a header rail with the
   flight number, a route band with the sector between two airport blocks,
   then a time grid with a 1px divider between columns. */
.flight-strip {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    background: var(--surface-2);
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease),
                background var(--dur-fast) var(--ease);
}
.flight-strip:hover { border-color: var(--flight-line); background: var(--card); }
.flight-strip:focus-visible { box-shadow: var(--ring-soft); }

.strip-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
.strip-flt {
    font-size: var(--fs-lg);
    font-weight: 700;
    letter-spacing: 0.04em;
    font-variant-numeric: var(--tnum);
}
.strip-dep-in {
    margin-left: auto;
    font-size: var(--fs-meta);
    font-weight: 700;
    color: var(--flight-ink);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}

/* Route band: [APT] ——— [APT] */
.strip-route {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-1) 0;
}
.strip-end { display: flex; flex-direction: column; gap: var(--space-0); flex: none; }
.strip-end:last-child { text-align: right; align-items: flex-end; }
.strip-end b {
    font-size: var(--fs-xl);
    font-weight: 700;
    letter-spacing: 0.06em;
    line-height: 1.1;
}
.strip-end span {
    font-size: var(--fs-meta);
    color: var(--text-faint);
    white-space: nowrap;
    max-width: 110px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.strip-leg {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 60px;
}
/* Dashed rail reads as "in the air"; the cap dots are the endpoints. */
.strip-leg-line {
    flex: 1;
    height: 1px;
    border-top: 1px dashed var(--card-border-2);
}
.strip-leg::before,
.strip-leg::after {
    content: "";
    width: 4px;
    height: 4px;
    border-radius: var(--r-xl);
    border: 1px solid var(--card-border-2);
    flex: none;
}
.strip-block {
    font-size: var(--fs-label);
    font-weight: 600;
    color: var(--text-faint);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}

/* Time grid. Scheduled value on top, station-local underneath — the two
   clocks a crew member actually compares. */
.strip-times {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    border-top: 1px solid var(--card-border);
    padding-top: var(--space-3);
}
.strip-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-0);
    padding: 0 var(--space-3);
    min-width: 0;
}
.strip-col + .strip-col { border-left: 1px solid var(--card-border); }
.strip-col:first-child { padding-left: 0; }
.strip-key {
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.strip-col b {
    font-size: var(--fs-base);
    font-weight: 700;
    line-height: 1.2;
    font-variant-numeric: var(--tnum);
}
.strip-local {
    font-size: var(--fs-label);
    color: var(--flight-ink);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}

/* Legal-rest progress bar */
.strip-rest { display: flex; align-items: center; gap: var(--space-3); }
.strip-rest-bar {
    flex: 1;
    height: 3px;
    border-radius: var(--r-xl);
    background: var(--card-border);
    overflow: hidden;
}
.strip-rest-bar .bar-fill { display: block; height: 100%; border-radius: var(--r-full); overflow: hidden; }
.strip-rest-bar .bar-fill rect { fill: var(--hotel); }
.strip-rest.bad .strip-rest-bar .bar-fill rect { fill: var(--danger); }
.strip-rest-txt {
    font-size: var(--fs-label);
    font-weight: 600;
    color: var(--text-faint);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}
.strip-rest.bad .strip-rest-txt { color: var(--danger); }

.strip-foot {
    font-size: var(--fs-label);
    color: var(--text-faint);
    padding-top: var(--space-2);
    border-top: 1px solid var(--card-border);
    letter-spacing: 0.3px;
}

/* --- 23.3 Operational status widget --------------------------------------- */
/* Where you are right now and how long until you owe a report time. The most
   important information on the dashboard, so it gets the strongest frame. */
.op-status {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--card-border-2);
    border-left: 2px solid var(--hotel);
    border-radius: var(--r-md);
    background: var(--card);
    margin-bottom: var(--gap);
}
.op-status.is-duty { border-left-color: var(--flight); }
.op-status.is-off { border-left-color: var(--off-line); }
.op-badge { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.op-badge-main {
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: 0.01em;
}
.op-badge-sub {
    font-size: var(--fs-meta);
    color: var(--text-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.op-countdown {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-soft);
}
.op-countdown b {
    font-size: var(--fs-md);
    font-weight: 700;
    color: var(--text);
    font-variant-numeric: var(--tnum);
}
/* PERF-02: the rest-progress bar was here (`.op-rest`, `.op-rest-bar` and its
   ok/bad variants). Designed -- "filled = elapsed, the 2px cap = the legal
   minimum" -- but never wired: no commit ever emitted it, and the ops widget
   renders `.op-badge`/`.op-countdown`/`.op-meta` instead. */
.op-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    font-size: var(--fs-meta);
    color: var(--text-faint);
}
.op-meta b { color: var(--text-soft); font-weight: 600; font-variant-numeric: var(--tnum); }

/* --- 23.4 Duty chips in calendar cells ------------------------------------ */
/* Month cells were loose text; these are compact pills that fit two per cell
   without overflowing. `[AH462 07:05Z]` with a type dot. */
.cal-chip, .cal-lane {
    display: flex;
    /* The chip is a COLUMN: its rows must SPAN it. Centred, they shrank to their
       content, so `margin-left: auto` had nothing to push against and the crossing
       rectangle stopped 8px into the next day instead of 149px. */
    align-items: stretch;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-0) var(--space-2);
    border-radius: var(--r-xs);
    border: 1px solid var(--flight-line);
    background: var(--flight-soft);
    color: var(--flight-ink);
    font-size: var(--fs-label);
    font-weight: 700;
    line-height: 1.5;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-variant-numeric: var(--tnum);
}
.cal-chip .tdot { width: 4px; height: 4px; border-radius: var(--r-full); background: currentColor; flex: none; }
.cal-chip .chip-txt { overflow: hidden; text-overflow: ellipsis; }
.cal-chip.t-hotel   { border-color: var(--hotel-line);   background: var(--hotel-soft);   color: var(--hotel-ink); }
.cal-chip.t-standby { border-color: var(--standby-line); background: var(--standby-soft); color: var(--standby-ink); }
.cal-chip.t-training{ border-color: var(--training-line);background: var(--training-soft);color: var(--training-ink); }
.cal-chip.t-reserve { border-color: var(--reserve-line); background: var(--reserve-soft); color: var(--reserve-ink); }
.cal-chip.t-off     { border-color: var(--off-line);     background: var(--off-soft);     color: var(--off-ink); }
.cal-chip.t-deadhead { border-style: dashed; }

/* --- 23.5 Month summary row -----------------------------------------------
   Removed. `.cal-summary`, `.cal-summary-cell`, `.cal-summary-cell b` and
   `.cal-summary-cell span` (35 lines) are emitted by nothing: `cal-summary` has
   zero occurrences in app.js, index.html and i18n.js, and the month totals that
   would have gone in it are rendered by renderMonthStats() into `#cal-stats`
   instead (app.js:3842). Nothing referenced them but each other. */


/* --- 23.6 Rotation timeline (Rounds) -------------------------------------- */
/* A rotation becomes a vertical spine: flight legs and layover nodes joined by
   a rail, so the shape of the pairing is readable at a glance. */
.rot {
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    background: var(--card);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--gap);
}
.rot-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding-bottom: var(--space-3);
    margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
.rot-route {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: 0.05em;
}
.rot-route i { color: var(--text-faint); font-style: normal; font-weight: 400; }
.rot-metrics { display: flex; gap: 16px; flex-wrap: wrap; margin-left: auto; }
.rot-metric { display: flex; flex-direction: column; gap: 0; align-items: flex-end; padding-left: 14px; border-left: 1px solid var(--card-border); }
.rot-metric:first-child { border-left: 0; padding-left: 0; }
.rot-metric b { font-size: var(--fs-base); font-weight: 700; font-variant-numeric: var(--tnum); }
/* The span inside the dates metric. It is a class rather than the inline
   `style="color:var(--text-faint)"` that was there: the arrow needs to read as
   a separator between two dates rather than as part of either one, and an inline
   style is the one thing in this block that cannot be restyled or audited. */
.rot-span { white-space: nowrap; }
.rot-metric b i { font-style: normal; color: var(--text-faint); padding: 0 var(--space-1); }
.rot-metric span {
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
}

.rot-timeline { display: flex; flex-direction: column; }
.rot-node {
    position: relative;
    display: flex;
    gap: var(--space-3);
    padding: 0 0 var(--space-3) 0;
}
/* The spine. Every node draws its own segment so a node can be reordered
   without breaking the rail. */
.rot-node::before {
    content: "";
    position: absolute;
    left: 5px;
    top: 12px;
    bottom: 0;
    width: 1px;
    background: var(--card-border-2);
}
.rot-node:last-child { padding-bottom: 0; }
.rot-node:last-child::before { display: none; }
.rot-dot {
    position: relative;
    z-index: 1;
    flex: none;
    width: 11px;
    height: 11px;
    margin-top: var(--space-1);
    border-radius: var(--r-xl);
    border: 2px solid var(--bg);
    background: var(--flight);
    box-shadow: 0 0 0 1px var(--flight-line);
}
.rot-node.is-layover .rot-dot { background: var(--hotel); box-shadow: 0 0 0 1px var(--hotel-line); }
.rot-node.is-end .rot-dot { background: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.rot-card {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    /* Wraps. The sector chain is one long inline run, and a leg node on a 390px
       phone put the flight number, the route, the times and the chain on a
       single nowrap line: the document scrolled to 572px inside a 390px
       viewport, so the whole right edge of every pairing was unreachable. */
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    background: var(--surface-2);
}
/* L75: a layover is context, not a duty. This row used to be a full-width card
   in the hotel green -- a filled 10% wash with a coloured border -- exactly as
   loud as the flight above and below it, so on the ALG->CZL->JED->ORN->ALG
   pairing the eye landed on "Between legs" and "RADISSON BLU JEDDAH CORNICHE"
   and skipped the four sectors they sit between.

   The rail dot is already the hotel colour and the text already names the hotel,
   so the fill was carrying nothing those two cues do not. It goes: the row keeps
   its dot, its label and its rest-hours badge, and loses the panel. */
.rot-node.is-layover .rot-card {
    border-color: var(--card-border);
    background: none;
    box-shadow: none;
}
.rot-node.is-layover .rot-hotel {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-soft);
}
.rot-leg-flt {
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: 0.04em;
    flex: none;
    font-variant-numeric: var(--tnum);
}
.rot-leg-route {
    font-size: var(--fs-sm);
    color: var(--text-soft);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.rot-leg-time {
    margin-left: auto;
    font-size: var(--fs-meta);
    color: var(--text-faint);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}
.rot-rest-badge {
    flex: none;
    font-size: var(--fs-label);
    font-weight: 700;
    padding: var(--space-0) var(--space-2);
    border-radius: var(--r-xs);
    border: 1px solid var(--hotel-line);
    background: var(--card);
    color: var(--hotel-ink);
    white-space: nowrap;
    font-variant-numeric: var(--tnum);
}
.rot-hotel {
    flex: 1;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- 23.7 Charts --------------------------------------------------------- */
/* Solid tone bars on a flat ground, no gradients. */
.chart .bar {
    fill: var(--flight);
    opacity: 0.85;
    transition: opacity var(--dur-fast) var(--ease);
}
.chart .bar:hover { opacity: 1; }
/* `.bar-hit`, `.axis-line`, `.grid-line` and `.chart text` were here: they are
   SVG affordances for an SVG chart. The monthly-hours chart is a div stack
   (`#bar-chart.chart` holding `.bar-wrap > .bar-val/.bar/.bar-label`,
   app.js:4464-4481), so there is no SVG inside `.chart` for any of the four to
   match. The only `<text>` element the app creates is the route map's airport
   label, and that lives under `#route-map`, not `.chart`. */

/* Stat tiles: flat, dense, left-aligned. */
.stat-card {
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    padding: var(--space-3) var(--space-3);
    box-shadow: none;
    transition: border-color var(--dur-fast) var(--ease);
}
.stat-card:hover { border-color: var(--card-border-2); }
.stat-num {
    font-size: var(--fs-xl);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
    font-variant-numeric: var(--tnum);
}
.stat-label {
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-top: var(--space-0);
}

.stats, .stats-cards, .mix-grid {
    gap: var(--gap) !important;
}
.stats, .stats-cards {
    margin-bottom: var(--gap) !important;
}
/* Audit 2026-10-14 UI-01 : `.mix-grid` n'avait rien à gagner ici (redondant
   prouvé par test_css_overrides.py) : déclaration propre, sans flag. */
.mix-grid {
    margin-bottom: var(--gap);
}
.stats, .stats-cards {
    grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)) !important;
}

/* Duty-mix rows. The bar's fill is the svg rect the row builds (class `sfill-*`
   for the tone), styled at 23.7 -- `.mix-bar-fill` matched nothing. */
.mix-row, .months-table td, .months-table th { font-variant-numeric: var(--tnum); }
.mix-row:hover { background: var(--surface-2); }

/* --- 23.8 Route map ------------------------------------------------------ */
/* Full-bleed dark canvas with great-circle arcs carrying a faint glow, and a
   summary panel docked to the right on wide screens. */
.map-card {
    padding: 0;
    border-radius: var(--r-md);
    background: var(--surface-2);
    /* Was `style="position:relative;"` on the element (SEC-17). The tooltip is
       positioned against this card, so this is where the containing block
       belongs -- not on the element, where it could be dropped by anyone editing
       the markup without knowing what it anchored. */
    position: relative;
}
#route-map {
    width: 100%;
    height: auto;
    display: block;
    background:
        radial-gradient(120% 80% at 50% 0%, rgba(255,255,255,0.02), transparent 60%),
        var(--surface-2);
}
#route-map .route-arc {
    fill: none;
    stroke: var(--flight);
    stroke-linecap: round;
    filter: drop-shadow(0 0 2px color-mix(in srgb, var(--flight) 55%, transparent));
}
#route-map .route-node { fill: var(--card); stroke: var(--flight-ink); stroke-width: 1; }
/* PERF-02: `#route-map .route-label` was here. The map labels its airports with
   `.map-label` (set in app-render.js); nothing ever emitted `.route-label`. */
/* UI-02: second .map-tooltip block merged into the single definition above. */
.map-hint { color: var(--text-faint); }

/* Destination summary panel */
.map-summary {
    border: 1px solid var(--card-border);
    border-radius: var(--r-md);
    background: var(--card);
    /* L71: was `overflow: hidden`, which clipped the six columns of `.dest-table` at
       phone widths -- `.map-layout` drops to one column at 1080px -- with no way to
       reach the right-hand ones, the frequency bars included.
       `auto hidden` is x then y: the first value was written backwards once and gave
       `overflow-x: hidden`, which the test caught by measuring the resolved property
       rather than the declaration. */
    overflow: auto hidden;
    margin-bottom: var(--gap);
}
.map-summary-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
.map-summary-head b { font-size: var(--fs-base); font-weight: 700; }
.map-summary-head span { font-size: var(--fs-meta); color: var(--text-faint); margin-left: auto; }
.dest-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-meta);
    /* L71: fixed layout so the scrollbar above is the thing that gives way, rather
       than one long airport code deciding the column widths. */
    table-layout: fixed;
}
.dest-table th {
    text-align: left;
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--text-faint);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
.dest-table td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--card-border);
    font-variant-numeric: var(--tnum);
}
.dest-table tr:last-child td { border-bottom: 0; }
.dest-table tr:hover td { background: var(--surface-2); }
.dest-code { font-weight: 700; letter-spacing: 0.05em; }
.dest-city { color: var(--text-soft); }
.dest-num { text-align: right; color: var(--text-soft); }
/* SEC-17: the three numeric `<th>` used to say `style="text-align:right"`, and
   they now say it here. The selector is `.dest-table th.dest-th-num` and not the
   bare `.dest-th-num` on purpose: the rule immediately above, `.dest-table th`,
   is what makes a header a header, and it also declares `text-align: left`. An
   inline style outranked that. A single class does not -- (0,1,0) loses to
   (0,2,0) -- so the obvious version of this change compiles, passes every static
   check, and quietly left-aligns three column headers.

   That is not a hypothetical: it is what the first attempt did, and the only
   thing that noticed was `test_visual_regression.py`, on the one screen that
   renders this table. With `table-layout: fixed` and three uppercase labels
   wider than their columns, left-aligning them made the words overlap. Worth
   recording because the lesson generalises -- an inline style is the only
   declaration in a stylesheet that wins by default, so every one this audit
   removed was a declaration silently relying on winning. */
.dest-table th.dest-th-num { text-align: right; }
.dest-freq {
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
}
/* One definition, because this class is emitted by two components and was
   declared twice: the rule here set 3px and `--card-border`, and a later rule
   in the sheets section set 8px and `--accent`. The later one won on source
   order, so the destinations table was silently drawing a bar its own stylesheet
   had never described -- heavier than intended, and painted in the crew
   member's chosen palette, which is a colour that means "this accent" and not
   "flown often". The fill is `--flight`, the duty colour, in both places.

   No width here: `.dest-freq` gives the table's bar a fixed track and
   `.dest-row` lets it fill its own grid column, which is the right shape for
   each. When both declared `width: 44px`, the row's bar sat at 44px inside a
   `2fr` track and the rest of the column was empty. */
.dest-freq-bar {
    height: 6px;
    border-radius: var(--r-full);
    background: var(--card-border);
    overflow: hidden;
}
.dest-freq .dest-freq-bar { width: 48px; flex: none; }
/* SEC-17: the fill is an svg now. The `:empty`/`:not(:empty)` floor pair died
   with the `<i>` -- and it had been dead longer than that: an `<i>` carrying
   only a `style` attribute is always `:empty`, so the 3px floor never applied
   and every bar rendered at its exact percentage. The svg renders the same. */
.dest-freq-bar .bar-fill {
    display: block;
    height: 100%;
    /* No radius of its own: the track clips, exactly as the old `<i>` relied on. */
    overflow: hidden;
}
.dest-freq-bar .bar-fill rect { fill: var(--flight); }
/* The smallest count is still a count.

   The bar is linear against the busiest row, and on the year map the busiest is
   the base -- every departure and every arrival -- so a 1-flight airport is
   1.1% of a 48px track: half a pixel. The number is printed beside it either
   way. (SEC-17: the `:empty`/`:not(:empty)` floor pair that was here died with
   the `<i>` -- an `<i>` carrying only a `style` attribute is always `:empty`,
   so the 3px floor never applied.) */

/* --- 23.9 Day drawer ----------------------------------------------------- */
/* Slide-over for a clicked calendar day: the month stays visible behind it. */
.day-sheet {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(420px, 92vw);
    z-index: 120;
    display: flex;
    flex-direction: column;
    background: var(--card);
    border-left: 1px solid var(--card-border-2);
    box-shadow: var(--shadow-xl);
    transform: translateX(100%);
    transition: transform var(--dur) var(--ease-out);
}
.day-sheet.open { transform: none; }
.day-sheet-scrim {
    position: fixed;
    inset: 0;
    z-index: 110;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur) var(--ease);
}
.day-sheet-scrim.open { opacity: 1; pointer-events: auto; }
.day-sheet-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--card-border);
}
.day-sheet-head b { font-size: var(--fs-base); font-weight: 700; }
.day-sheet-x {
    margin-left: auto;
    width: 26px;
    height: 26px;
    border-radius: var(--r-sm);
    border: 1px solid var(--card-border);
    background: var(--surface-2);
    color: var(--text-soft);
    cursor: pointer;
    font-size: var(--fs-md);
    line-height: 1;
}
.day-sheet-x:hover { color: var(--text); border-color: var(--card-border-2); }
.day-sheet-body { overflow-y: auto; padding: var(--space-3) var(--space-4) var(--space-7); }

/* --- 23.10 Compact list rows -------------------------------------------- */
/* `.dv-row` was paired with `.dash-duty` in both rules here and matched
   nothing: the dashboard's duty card is `.dash-duty` (app.js:3056) and the
   list-view row has no class of its own. */
.dv-list, .duty-meta, .dash-week { gap: var(--gap) !important; }
/* Audit 2026-10-14 UI-01 : `.dash-week` redondant prouvé : même valeur sans flag. */
.dash-week { gap: var(--gap); }
.dash-duty {
    min-height: var(--row-h);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    background: var(--card);
}
.dash-duty:hover { border-color: var(--card-border-2); }

/* --- 23.11 Modals: flatter, denser -------------------------------------- */
.modal-root {
    background: rgba(0, 0, 0, 0.62);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
.modal {
    background: var(--card);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-xl);
    max-height: min(86vh, 760px);
}
.modal-head {
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--card-border);
}
.modal-head h2,
.modal-head h3 { font-size: var(--fs-base); font-weight: 700; }
.modal-x {
    width: 26px;
    height: 26px;
    border-radius: var(--r-sm);
    color: var(--text-soft);
}
.modal-x:hover { background: var(--surface-2); color: var(--text); }

/* --- 23.12 Buttons ------------------------------------------------------ */
.btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 28px;
    padding: 0 var(--space-4);
    border-radius: var(--r-sm);
    font-size: var(--fs-base);
    font-weight: 600;
    letter-spacing: 0;
    cursor: pointer;
    border: 1px solid var(--card-border);
    background: var(--card);
    color: var(--text-soft);
    box-shadow: none;
    white-space: nowrap;
    transition: background var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}
.btn svg { width: 14px; height: 14px; flex: none; }
.btn:hover {
    background: var(--surface-2);
    color: var(--text);
    border-color: var(--card-border-2);
    /* L74: section 23 flattens five neighbouring hover transforms and left this
       one, so a 28px button still lifted while its neighbours did not. Flattened
       for consistency. The cost the report actually names is the twenty
       `!important` this whole section uses to avoid rewriting 3,000 lines in
       place, and that does not go away here -- it goes away when M41 removes the
       dead code. */
    transform: none;
}
.btn:active { transform: scale(0.98); }
.btn-light, .btn-ghost {
    background: transparent;
    border-color: var(--card-border);
    color: var(--text-soft);
}
.btn-accent {
    background: color-mix(in srgb, var(--accent) 18%, var(--card));
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    color: var(--text);
}
.btn-accent:hover { background: color-mix(in srgb, var(--accent) 26%, var(--card)); }
.btn-soft { background: var(--surface-2); }
.btn-danger { background: transparent; border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); }

.icon-btn {
    width: 28px;
    height: 28px;
    border-radius: var(--r-sm);
    border: 1px solid var(--card-border);
    background: transparent;
    color: var(--text-soft);
    display: grid;
    place-items: center;
}
.icon-btn:hover { background: var(--card); color: var(--text); border-color: var(--card-border-2); }
.icon-btn svg { width: 15px; height: 15px; }

/* Segmented control */
.seg {
    height: 26px;
    padding: 0 var(--space-3);
    border-radius: var(--r-sm);
    border: 1px solid var(--card-border);
    background: transparent;
    color: var(--text-soft);
    font-size: var(--fs-meta);
    font-weight: 600;
    cursor: pointer;
}
.seg:hover { background: var(--card); color: var(--text); }
.seg.active, .seg[aria-pressed="true"] {
    background: var(--card);
    border-color: var(--card-border-2);
    color: var(--text);
}
.view-switch, .seg.small {
    display: inline-flex;
    gap: var(--space-0);
    padding: var(--space-1);
    border-radius: var(--r-sm);
    background: var(--card-border);
}
.view-switch .seg, .seg.small .seg { border-color: transparent; background: var(--bg); }

/* Filter chips */
.chips { gap: var(--space-2) !important; margin-bottom: var(--gap); }
.chip {
    height: 24px;
    padding: 0 var(--space-3);
    border-radius: var(--r-xl);
    border: 1px solid var(--card-border);
    background: var(--card);
    font-size: var(--fs-meta);
    font-weight: 600;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
.chip:hover { border-color: var(--card-border-2); }
.chip.active { background: var(--surface-2); border-color: var(--card-border-2); color: var(--text); }

/* --- 23.13 Toasts ------------------------------------------------------- */
.toast {
    background: var(--card);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-lg);
    font-size: var(--fs-base);
    padding: var(--space-3) var(--space-4);
}
/* PERF-02: `.toast .ticon` was here. Toasts mark themselves with `.tdot` and
   `::before`; nothing ever emitted `.ticon`. */

/* --- 23.14 Footer: quieter ---------------------------------------------- */
footer.print-hide {
    border-top: 1px solid var(--card-border);
    padding: var(--space-3) 0 var(--space-6);
    margin-top: var(--gap);
    font-size: var(--fs-meta);
    color: var(--text-faint);
}
footer .foot-mark { display: none; }
.kbd {
    border: 1px solid var(--card-border-2);
    border-bottom-width: 2px;
    border-radius: var(--r-xs);
    background: var(--surface-2);
    color: var(--text-soft);
    font-size: var(--fs-2xs);
    padding: var(--space-0) var(--space-2);
}

/* --- 23.15 Settings additions ------------------------------------------ */
.station-select {
    height: 30px;
    width: 100%;
    max-width: 240px;
    padding: 0 var(--space-3);
    border-radius: var(--r-sm);
    border: 1px solid var(--card-border);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--fs-base);
}
.station-select:focus { border-color: var(--card-border-2); }

/* --- 23.16 Reduced motion: honoured in section 22 ------------------------
   This block used to repeat the blanket reset a second time, later in source
   and with the same specificity, which is why the explicit "stop the loops"
   list it was shadowing did not always win. There is now exactly one
   @media (prefers-reduced-motion: reduce) block in the file, in section 22. */

/* --- 23.17 Print: force the light sheet so ink is actually available --- */
/* --- 23.18 App-bar clocks and status ------------------------------------- */
/* Two clocks a crew compares constantly: Zulu (the company's operational
   reference) and station local (what the shuttle and duty desk run on).
   Both tabular so digits never shift as they tick. */
.clock-pair {
    display: flex;
    align-items: stretch;
    gap: var(--space-0);
    flex: none;
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    overflow: hidden;
    background: var(--card-border);
}
.clock-cell {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--space-0);
    padding: var(--space-1) var(--space-3);
    background: var(--card);
    min-width: 74px;
}
.clock-cell b {
    font-size: var(--fs-base);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: 0.02em;
    font-variant-numeric: var(--tnum);
}
.clock-cell small {
    font-size: var(--fs-3xs);
    font-weight: 600;
    letter-spacing: 0.7px;
    text-transform: uppercase;
    color: var(--text-faint);
    line-height: 1;
}
.clock-cell.is-utc b { color: var(--text); }
.clock-cell.is-local b { color: var(--flight-ink); }

.pill-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 28px;
    padding: 0 var(--space-3);
    border-radius: var(--r-xl);
    border: 1px solid var(--card-border);
    background: var(--card);
    font-size: var(--fs-meta);
    font-weight: 600;
    color: var(--text-soft);
    white-space: nowrap;
    flex: none;
}
.pill-status .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--r-xl);
    background: var(--hotel);
    flex: none;
}
.pill-status.is-offline .dot { background: var(--standby); }
.pill-status.is-offline { color: var(--text-faint); }

@media (max-width: 900px) {
    .clock-cell.is-local { display: none; }
    .hero .hero-sub { display: none; }
}
@media (max-width: 640px) {
    .clock-pair { display: none; }
}
/* --- 23.19 Map layout: full-bleed canvas + docked summary ---------------- */
.map-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: var(--gap);
    align-items: start;
}
.map-layout .map-summary { margin-bottom: 0; }
@media (max-width: 1080px) {
    .map-layout { grid-template-columns: minmax(0, 1fr); }
}

/* SVG map: matte canvas, subtle graticule, faint land mass. */
#route-map .map-ocean { fill: var(--surface-2); }
#route-map .map-grid { stroke: var(--card-border); stroke-width: 0.5; }
#route-map .map-land {
    fill: var(--card);
    stroke: var(--card-border);
    stroke-width: 0.5;
    opacity: 0.5;
}
#route-map .map-dot { fill: var(--card); stroke: var(--flight-ink); stroke-width: 1; }
#route-map .map-dot.base-hub { stroke: var(--accent-ink); stroke-width: 1.4; }
#route-map .map-hub-ring { fill: none; stroke: var(--accent-ink); stroke-width: 1; }
#route-map .map-route { stroke: var(--flight); opacity: 0.42; transition: opacity var(--dur-fast) var(--ease); }
#route-map .map-route:hover,
#route-map .map-route.highlighted { opacity: 0.95; }
#route-map .map-label { fill: var(--text-soft); font-size: var(--fs-3xs); font-weight: 600; letter-spacing: 0.4px; }
/* QA 360 Mo3: hovering a label now shows the same tooltip the disk does, and
   the label has to acknowledge it the way the disk does -- otherwise the hover
   works and nothing on screen says which airport is being described. */
#route-map .map-label.highlighted { fill: var(--accent-ink); font-weight: 800; }
#route-map .map-label.base-hub { fill: var(--accent-ink); }
/* The label is given `.is-hub`, not `.base-hub` -- that class is on the marker --
   so the rule above matched nothing and the base's code was never picked out
   from the rest. It is the one label on the map a crew member looks for first.
   Brighter ink and a heavier weight rather than a hue change: the hub is already
   the largest dot and carries the pulsing ring, so this reinforces rather than
   introduces a second cue. */
#route-map .map-label.is-hub { fill: var(--text); font-weight: 800; }
#route-map text { font-variant-numeric: var(--tnum); }
/* --- 23.20 Neutralise remaining aurora components -----------------------
   Sections 05/12/16 predate the cockpit pass and still paint gradient
   hero-ish surfaces. Rather than rewrite each in place, flatten them here.
   Same class names, matte surfaces, 1px borders, no glow, no lift.

   The tab bar used to be in this list, flattening `.tab-btn.active` to a plate
   and killing the red underline. It is not here any more: section 06 now paints
   the active tab as a *label* -- brighter ink, a full-strength icon, no fill --
   and the moving 2px cursor under the bar carries "you are here". Flattening it
   here would put a plate back under the cursor, which is the two-signal version
   this pass set out to remove. */

/* Live banner: flat card, single left rule, no radial wash, no glow. */
.dash-live-banner {
    padding: var(--space-3) var(--space-4) var(--space-3) var(--space-4);
    border-radius: var(--r-md);
    background: var(--card);
    border: 1px solid var(--card-border);
}
.dash-live-banner::before {
    width: 2px;
    background: var(--live-color, var(--flight));
    box-shadow: none;
}
.dash-live-banner:hover {
    transform: none;
    box-shadow: none;
    border-color: var(--card-border-2);
}
.dash-live-banner .live-title { font-size: var(--fs-md); }
.dash-live-banner .live-sub { color: var(--text-soft); }

/* Rest-day / off card: neutral chip, not an emerald wash. */
.rest-day-card {
    padding: var(--space-3) var(--space-3);
    gap: var(--space-3);
    border-radius: var(--r-md);
    background: var(--off-soft);
    border: 1px solid var(--off-line);
}
.rest-day-card .rest-icon {
    width: 30px;
    height: 30px;
    font-size: var(--fs-lg);
    border-radius: var(--r-sm);
    background: var(--card);
    border: 1px solid var(--off-line);
}
.rest-day-card .rest-title { font-size: var(--fs-base); color: var(--off-ink); }
.rest-day-card .rest-sub { font-size: var(--fs-meta); color: var(--text-faint); }

/* Accent button: tinted fill, no gradient, no lift. */
.btn-accent {
    background: color-mix(in srgb, var(--accent) 16%, var(--card)) !important;
    border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
    color: var(--text) !important;
    box-shadow: none !important;
    text-shadow: none;
}
.btn-accent:hover {
    background: color-mix(in srgb, var(--accent) 24%, var(--card));
    transform: none;
}

/* Ops-check rows: flat list lines. `.ops-item` is what renderOps() builds
   (app.js:3534, 3567); `.ops-row` is emitted by nothing. */
.ops-item {
    border-radius: var(--r-sm);
    background: var(--surface-2);
    border: 1px solid var(--card-border);
}

/* Next-7-days rows: dense tabular lines. The widget renders `.week-mini`
   (app.js:3404); `.wk-row` and `.dv-row` are emitted by nothing, so this
   rule matched nothing and the min-height was never applied. */
.week-mini { min-height: var(--row-h); }
/* --- 23.21 Trip list container -----------------------------------------
   .trip-list was a column of .trip-card elements with 14px gaps and hover
   lift. The rotation cards are .rot, so the container still applies but the
   gap is tightened and the old hover no longer exists. */
.trip-list { gap: var(--gap); }
/* --- 23.22 Remaining saturated fills -------------------------------------
   Stat-tile top rule, chart bars and the live bar still use gradient fills
   from the old design. Flatten them to solid semantic tones. */
.stat-card::before { display: none; }

.chart .bar, .chart .bar.multi {
    background: var(--flight) !important;
    opacity: 0.9;
}
/* Audit 2026-10-14 UI-01 : `.bar.multi` redondant prouvé : même valeur sans
   flag (spécificité supérieure conservée par le sélecteur lui-même). */
.chart .bar.multi {
    background: var(--flight);
}
.chart .bar:hover { filter: none; transform: none; opacity: 1; }

/* Live-status bar */
.live-bar-fill {
    background: var(--live-color, var(--flight)) !important;
    box-shadow: none !important;
}

/* Duty-mix bars take the type token rather than the accent.
   Only the track carries a colour; the fill is the svg rect the row builds with
   an `sfill-*` class (app-cal.js), so the per-type overrides below were
   redundant for the fill and unreachable for two reasons: `.mix-bar-fill` is
   emitted by nothing, and app.js sets no `data-t` attribute on `.mix-row`
   (only `dataset.type` on a filter chip, at 3948), so all eight `[data-t=...]`
   selectors could never match. */
.mix-bar {
    background: var(--card-border) !important;
    box-shadow: none !important;
}

/* Legend dots and time-breakdown keys follow the same tokens.
   PERF-02: `.tb-row .dot` shares this selector and nothing ever emitted `.tb-row`,
   but section 23's selectors are frozen by test_css_overrides.py -- the half
   stays, unmatched, rather than the guard being edited to bless its removal. */
.mix-row .dot, .tb-row .dot { box-shadow: none !important; }
/* Audit 2026-10-14 UI-01 : `.tb-row .dot` redondant prouvé : même valeur sans flag. */
.tb-row .dot { box-shadow: none; }

/* "Today" markers: a 2px rule, not a coloured wash across the cell. */
.cal-cell.today {
    background: var(--card) !important;
    box-shadow: inset 0 0 0 1px var(--card-border-2);
}
.cal-cell.today::after { display: none; }
.cal-cell.today .cal-num {
    background: var(--card) !important;
    color: var(--accent-ink) !important;
    box-shadow: inset 0 0 0 1px var(--accent) !important;
}
.wk-head.today {
    background: var(--card) !important;
    border-color: var(--accent);
}

/* Filter chips: neutral, with the type dot carrying the colour. */
.chip {
    background: var(--card);
    border-color: var(--card-border);
    box-shadow: none !important;
}
.chip:hover { transform: none; }
.chip .dot { box-shadow: none !important; }
.chip[aria-pressed="true"] {
    background: var(--surface-2) !important;
    border-color: var(--card-border-2) !important;
    /* L75: `color` was missing here, so the flat override changed the fill and
       left the text white from the accent rule above it. Dark mode did not
       notice -- white on `--surface-2` is fine on a dark slate -- but the light
       theme's `--surface-2` is #f4f4f5, so the *selected* filter chip rendered
       white on near-white at 1.1:1 and "All" was simply not there to be read.
       A selected control that cannot be seen is not selected as far as anyone
       using it is concerned. This is the same pairing `.chip.active` already
       uses at 5992, so the pressed chip and the active chip now read alike. */
    color: var(--text) !important;
}

/* Stat tiles: no lift. */
.stat-card:hover { transform: none; }

/* Day cards in week/day views. */
.day-card:hover { transform: none; box-shadow: none; border-color: var(--card-border-2); }
/* PERF-02: `.day-card .day-bar` was here. Day cards render rail, number, body
   and actions; nothing ever emitted `.day-bar`. */
/* --- 23.23 Portal dashboard panel -----------------------------------------
   Notification ribbons, the METAR/TAF strip for the next duty airports, and
   open schedule bids. Sits under the live banner and above the duty cards.
   Matte and flat like the rest of section 23: 1px borders, tokens only, no
   gradients or glow. Hidden entirely when a sync produced no extras, so the
   dashboard is unchanged for anyone who never sees a notice. */
.dash-portal {
    display: grid;
    gap: var(--gap);
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    margin-bottom: var(--gap);
}
.dash-portal.hidden { display: none; }

.portal-block {
    background: var(--surface);
    border: 1px solid var(--card-border);
    border-radius: var(--r-md, 10px);
    padding: var(--pad);
    min-width: 0;
}

.portal-h {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: 0 0 var(--space-3);
    font-size: var(--fs-label);
    font-weight: 650;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-soft);
}
.portal-h svg { width: 15px; height: 15px; flex: none; }

/* --- notices: the severity is carried by the icon and the ink, not colour
   alone, so it survives a monochrome print and colour-blind viewing. --- */
.portal-notes, .portal-bids, .portal-wx-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-3);
}

.portal-note {
    display: flex;
    gap: var(--space-3);
    align-items: flex-start;
    font-size: var(--fs-meta);
    line-height: 1.45;
}
.portal-note-ico { flex: none; width: 15px; height: 15px; margin-top: var(--space-0); }
.portal-note-ico svg { width: 100%; height: 100%; }
.portal-note--warn .portal-note-ico { color: var(--warning); }
.portal-note--info .portal-note-ico { color: var(--text-faint); }
.portal-note-txt { display: grid; gap: var(--space-0); min-width: 0; }
.portal-note-txt b { font-weight: 600; color: var(--text); }
.portal-note--warn .portal-note-txt b { color: var(--warning); }
.portal-note-sub { color: var(--text-faint); }

/* --- weather: code, temperature, wind in a tight tabular row. The raw
   METAR/TAF rides along in the title attribute rather than on screen. --- */
.portal-wx {
    display: grid;
    grid-template-columns: 3.6rem 1fr auto auto;
    align-items: baseline;
    gap: var(--space-3);
    font-variant-numeric: var(--tnum);
    cursor: help;
}
.portal-wx-code {
    font-weight: 650;
    color: var(--text);
    letter-spacing: 0.02em;
}
.portal-wx-temp { color: var(--text-soft); }
.portal-wx-wind { color: var(--text-faint); font-size: var(--fs-label); }
/* The day's range and the rain chance. Both are what turn "17°C" into a
   decision about the walk to the gate; wind alone does not. */
.portal-wx-range { color: var(--text-faint); font-size: var(--fs-label); }
.portal-wx-rain {
    color: var(--sky);
    font-size: var(--fs-label);
    font-weight: 700;
}

/* --- open bidding --- */
.portal-bid { display: grid; gap: var(--space-0); font-size: var(--fs-meta); }
.portal-bid b { font-weight: 600; color: var(--text); }
.portal-bid span { color: var(--text-faint); font-size: var(--fs-label); }

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

/* ============================ 22b ┬─ CONTRASTE ET POINTEUR =====================

   Three separate things, kept apart because they are asked for by different
   people and fail in different ways.

   --- M44 : la taille des cibles, selon le pointeur -----------------------

   WCAG 2.2 SC 2.5.8 (AA) asks for 24x24. This file's controls were section-23
   shrink-downs of larger section-05 originals -- `.icon-btn` 40 -> 28,
   `.modal-x` 32 -> 26 -- and several had fallen below even the AA floor:
   `.chip` 24 was exactly on it, `.today-link` was 17, `.day-actions .mini` was
   33x33. All of it is below what the platform HIG asks for (44x44), and this is
   explicitly a phone surface: `.focus-sheet`, `#focus-btn`. The tap flash is
   also suppressed globally (`-webkit-tap-highlight-color: transparent`), so
   there is no visual feedback to fall back on either.

   Two steps, because 44 everywhere would destroy the compact density the whole
   layout is built on and would be its own regression:

   1. Nothing interactive goes below 24x24, on any device. `.today-link` is a
      text link, so it gets padding rather than a fixed height, which keeps the
      text at 11-12px and grows the hit area instead.
   2. On a coarse pointer -- a finger, not a mouse -- the standalone controls go
      to 44. Not every control: inline chips inside a sentence of text stay
      small, because a 44px chip in a dense list is worse than the tap error it
      fixes.

   --- M45 : le mode contraste force de Windows ----------------------------

   Three failures, and only the third of them was still open:

   * The focus ring. Already fixed: `:focus-visible` carries
     `outline: 2px solid var(--accent-ink)` as well as the soft halo, precisely
     because forced-colors drops box-shadows. Reasserted here anyway, because
     forced-colors also replaces author colours and a rule that merely survives
     is not the same as one that is checked.
   * The five gradient headings -- `.stat-num`, `.dash-big`, `.modal-head h2/h3`,
     `.detail-hero .flight-num` and `.focus-flt` -- all paint text with
     `color: transparent` and a `background-clip: text` gradient. Forced-colors
     suppresses background images, so the text becomes transparent over whatever
     is behind it: invisible. `forced-color-adjust: none` plus a real `color` is
     the fix, per element.
   * `backdrop-filter` surfaces -- the header, tooltips, toasts -- collapse onto
     the system canvas, so they lose all separation from the page behind.
     `CanvasText` borders put it back.

   --- M46 : contraste augmente -------------------------------------------

   The one rule that existed used `@media (prefers-color-contrast: more)`, and
   `prefers-color-contrast` is **not** the feature name. The standard is
   `prefers-contrast`. An unknown feature name makes the whole query invalid, so
   that block has never applied in any browser; it is gone, and this is its
   replacement.

   What it has to fix is measured, not aesthetic: `--card-border` is
   `rgba(255,255,255,.08)` in dark and `rgba(9,9,11,.10)` in light, and
   `--card-border-2` is `.14` / `.16`. `--card-border` is the *only* border on
   `.btn-ghost` and `.icon-btn`, so an unbordered-looking button is actually a
   1.23:1 outline -- which is why the buttons vanish for anyone who needs the
   edge to find them. SC 1.4.11 wants 3:1 for a boundary that carries meaning.
   Both tokens are raised to at least 3:1 against their own surface here, in
   both themes.

   `prefers-color-scheme` is deliberately still not queried: the crew member
   picks their theme in Settings and it is remembered, so following the OS would
   silently override a choice they made. */

@media (forced-colors: active) {
    /* The ring is reasserted, not left to survive. */
    :focus-visible {
        outline: 2px solid CanvasText !important;
        outline-offset: 2px;
    }

    /* Gradient headings: stop painting text with a background. */
    /* Exactly the five that paint text with a background. Verified by looking
       for `color: transparent` together with `background-clip: text`, not by
       taking a name from a report: the first list I wrote here included
       `.cal-summary-cell b`, which finding M41 had just proved dead, and
       `.strip-flight-num`, which has no rule in this sheet (UI-06: comment
       corrected, nothing to delete). */
    .stat-num,
    .dash-big,
    .modal-head h2, .modal-head h3,
    .detail-hero .flight-num,
    .focus-flt {
        forced-color-adjust: none;
        color: CanvasText;
        background: none;
    }

    /* Surfaces that relied on a blur for separation.
       PERF-02: `.app-header` was in this list. Nothing ever emitted it. */
    .modal,
    .tooltip,
    .toast,
    .account-menu,
    .focus-sheet,
    .day-sheet {
        forced-color-adjust: none;
        background: Canvas;
        border-color: CanvasText;
    }

    /* Anything whose only boundary is a low-alpha token. */
    .btn,
    .btn-ghost,
    .icon-btn,
    .chip,
    .seg,
    input,
    select,
    textarea {
        forced-color-adjust: none;
        border-color: CanvasText;
    }
}

@media (prefers-contrast: more) {
    :root {
        /* 3.4:1 and 3.6:1 on --surface and --card. */
        --card-border: rgba(255, 255, 255, 0.34);
        --card-border-2: rgba(255, 255, 255, 0.52);
    }
    [data-theme='light'] {
        --card-border: rgba(9, 9, 11, 0.46);
        --card-border-2: rgba(9, 9, 11, 0.62);
    }
    .btn-ghost,
    .icon-btn {
        /* Audit 2026-10-14 A11Y-03 : en contraste élevé, la bordure est la
           couleur du texte, pas une variation de bordure -- 21:1 au lieu de
           ~3.4:1. Réservé à ce bloc : aucun pixel standard ne bouge. */
        border-color: var(--text);
    }
    /* the one pill the old invalid block was reaching for */
    #portal-badge { border-width: 2px; }
}

/* M44 step 1: the AA floor, on every device. */
.today-link {
    /* Padding on the scale, not hand-picked. My first pass wrote `4px 6px` and
       `test_visual_tokens.py` was right to refuse it: 6px is not a step on this
       scale (2 / 4 / 8 / 12 / 16 / 20 / 24), so the 8px step is what "a little
       more than the 4px one" means here. The height below is left as a raw value
       on purpose -- 24px is a platform minimum, not a spacing choice, and the
       check rightly does not look at `min-height`. */
    padding: var(--space-2) var(--space-3);
    min-height: 24px;
    display: inline-flex;
    align-items: center;
}
.day-actions .mini { min-width: 24px; min-height: 24px; }
.modal-x { min-width: 26px; min-height: 26px; }
.toast-action { min-height: 24px; }

/* M44 step 2: a finger gets 44. */
@media (pointer: coarse) {
    /* `button` and not only the named classes: several controls in this file are
       bare <button> -- a `.seg` segment measured 26px and my first version of
       this rule listed `.seg button` without ever checking that the element the
       browser renders is matched by it. Measured at 26px, still under 44. */
    button,
    .btn,
    .gate-submit,
    .icon-btn,
    .modal-x,
    .seg button,
    .day-actions .mini,
    .toast-action {
        min-height: 44px;
    }
    /* ...except the ones that live inside running text or a dense list. */
    .chip { min-height: 32px; }
    /* QA 360, Mo2. `.cal-chip` is neither: it is the *tap target for a duty*,
       measured at 47 x 17 px at 390 px wide -- under the 24 px WCAG floor and a
       quarter of the 44 px a finger needs. The `.chip` exception above is
       because chips live inside running text; a calendar cell does not, and
       there is room because the grid row grows with the chip rather than
       clipping it. Scoped to `(pointer: coarse)`: a 44 px chip in a month grid
       on a 1440 px screen would triple its height for a mouse that does not
       need it. */
    .cal-chip { min-height: 44px; }
    .icon-btn,
    .modal-x,
    .day-actions .mini { min-width: 44px; }
    .seg button { min-width: 44px; }
    /* Chips stay compact: they sit inside running text and lists, and a 44px
       chip in a dense roster is its own legibility problem. The 32px above is
       the deliberate exception; everything else here is 44. */
    .today-link { min-height: 44px; }
}

/* Visually hidden, still announced. Used by #announcer (index.html): a live region
   that never changes when a toast is removed, so nothing spurious is read out. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* --- The encrypted-roster unlock (app-vault.js) ---------------------------------
   A `<dialog>`, so focus trapping, Escape and Enter come from the browser rather
   than from a hand-rolled overlay. Sized to the form inside it and nothing else;
   the backdrop is the token surface, not a hard-coded black. */
.vault-dialog {
    border: 1px solid var(--card-border);
    border-radius: var(--r-lg);
    background: var(--card);
    color: var(--text);
    padding: var(--space-5);
    max-width: 420px;
    width: calc(100% - 2 * var(--space-4));
    box-shadow: var(--shadow-lg);
}
.vault-dialog::backdrop {
    background: color-mix(in srgb, var(--bg) 72%, transparent);
    backdrop-filter: blur(3px);
}
.vault-form { display: flex; flex-direction: column; gap: var(--space-3); }
.vault-h { margin: 0; font-size: var(--fs-lg); font-weight: 800; }
.vault-sub { margin: 0; color: var(--text-soft); font-size: var(--fs-sm); line-height: 1.45; }
.vault-label { font-size: var(--fs-meta); font-weight: 700; color: var(--text-soft); }
.vault-input {
    width: 100%;
    padding: var(--space-3);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--fs-sm);
}
.vault-input:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
.vault-err { margin: 0; color: var(--danger); font-size: var(--fs-meta); font-weight: 600; }
.vault-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }
/* ============================================================================
   app-views.js -- the planning model's render layer.

   Appended rather than interleaved: the sheet, the rotation timeline, the
   alert centre, the limits panel, the period bar and the grouped search
   results are one family of surface, and keeping them together is what
   makes the shared vocabulary (a `sheet-cell` is a cell wherever it
   appears) readable.

   Everything here resolves through the tokens declared at the top of this
   file. No literal colour, no literal radius, no literal spacing step: a
   rule that hard-codes a value here is a rule that does not follow the
   theme or the density switch, and the rest of this stylesheet has spent
   a long time not doing that.
   ============================================================================ */

/* --- The shared vocabulary ---------------------------------------------
   `.sheet-*` is the vocabulary every model-backed surface speaks. The
   flight sheet, the rotation sheet and the limits panel all use it, so a
   value the portal did not send reads the same in all three. */

.sheet-block {
    padding: var(--space-4) 0;
    border-top: 1px solid var(--card-border);
}
.sheet-block:first-child { border-top: 0; }

.sheet-h {
    margin: 0 0 var(--space-3);
    font-size: var(--fs-label);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-soft);
}

/* The fact grid. `minmax(0, 1fr)` rather than `1fr` so a long value can
   shrink and wrap instead of forcing the whole row wider than the modal. */
.sheet-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 148px), 1fr));
    gap: var(--space-3);
}
.sheet-cell {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    padding: var(--space-3);
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
}
.sheet-k {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.sheet-v {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text);
    overflow-wrap: anywhere;
}
/* The single most important rule in this block. A value the portal did not
   send is marked, in place, rather than left blank or drawn as a zero. */
.sheet-na {
    font-style: italic;
    font-weight: 500;
    color: var(--text-faint);
}
.sheet-cell-na { opacity: 0.9; }
.sheet-hint {
    margin: var(--space-3) 0 0;
    font-size: var(--fs-2xs);
    line-height: 1.5;
    color: var(--text-faint);
}
/* A second-level label inside one block, for when a block carries two things
   worth naming (the rotation, and the chronology of its sectors). */
.sheet-k-sub {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--card-border);
}
.sheet-warn {
    margin: var(--space-3) 0 0;
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: var(--warning);
}
.sheet-manual {
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-3) var(--space-4);
    background: var(--gray-soft);
}
.manual-list {
    list-style: none;
    margin: var(--space-2) 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.manual-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
    flex-wrap: wrap;
}
.manual-key {
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.manual-val {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text);
    overflow-wrap: anywhere;
    min-width: 0;
    flex: 1;
}
.manual-restore,
.manual-restore-all {
    flex: none;
}

/* --- The flight sheet ---------------------------------------------------- */

.sheet-hero { margin-bottom: var(--space-2); }

.sheet-route {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}
.sheet-apt-box {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    padding: var(--space-3);
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
}
.sheet-iata {
    font-size: var(--fs-xl);
    font-weight: 800;
    letter-spacing: 0.02em;
    color: var(--text);
}
.sheet-apt { font-size: var(--fs-xs); color: var(--text-soft); }
.sheet-city { font-size: var(--fs-2xs); color: var(--text-faint); }
.sheet-arrow { flex: none; color: var(--text-faint); font-size: var(--fs-lg); }

/* Report / departure / arrival as a three-column table: civil time, civil
   date, UTC. The civil date is its own column because an overnight duty's
   arrival falls on a different day from its departure, and putting the two
   in one cell is how that fact gets lost. */
.sheet-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
}
.sheet-table th {
    padding: var(--space-2) var(--space-2);
    text-align: left;
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
    border-bottom: 1px solid var(--card-border);
}
.sheet-table td {
    padding: var(--space-2);
    border-bottom: 1px solid var(--card-border);
    color: var(--text);
}
.sheet-table tr:last-child td { border-bottom: 0; }
.sheet-civil { font-weight: 700; font-variant-numeric: var(--tnum); }
.sheet-utc { color: var(--text-faint); font-variant-numeric: var(--tnum); }
.sheet-zone {
    margin-top: var(--space-2);
    font-size: var(--fs-2xs);
    color: var(--text-faint);
}

.sheet-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.sheet-pos {
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--text-soft);
    font-variant-numeric: var(--tnum);
}
.sheet-round {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    width: 100%;
    margin-top: var(--space-2);
    padding: var(--space-3);
    text-align: start;
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.sheet-round:hover { border-color: var(--accent); }
.sheet-round-route { font-size: var(--fs-sm); font-weight: 700; color: var(--text); }
.sheet-round-meta { font-size: var(--fs-2xs); color: var(--text-faint); }
.sheet-foot { margin-top: var(--space-4); }
.sheet-note { margin-top: var(--space-4); }

/* The rotation as a chronology on the duty sheet. The sector being read is
   marked `is-here` and is not a button -- it is where the reader already is,
   and offering it as a destination that does nothing is worse than not
   offering it. Every other stop is a door. */
.timeline {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
}
.timeline-stop {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-inline-start: 2px solid var(--card-border);
}
.timeline-stop.is-here {
    /* `--accent-ink`, not `--accent`: this rule is a 2px bar and not text,
       but it reads as a marker beside type, and the design system reserves
       `--accent` for fills the palette owns. The check in
       `test_visual_tokens.py` only excludes the `border-` prefixes, so
       `border-inline-start-color` with `--accent` is flagged; using the ink
       token is both correct and passes. */
    border-inline-start-color: var(--accent-ink);
    background: var(--gray-soft);
    border-radius: var(--r-sm);
}
.timeline-n {
    display: grid;
    place-items: center;
    width: 18px;
    height: 18px;
    font-size: var(--fs-3xs);
    font-weight: 800;
    color: var(--text-faint);
    background: var(--surface-2);
    border-radius: var(--r-full);
}
.timeline-stop.is-here .timeline-n { color: var(--accent-ink); }
.timeline-apt {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text);
    background: none;
    border: 0;
    padding: 0;
    text-align: start;
    cursor: pointer;
}
.timeline-stop.is-here .timeline-apt { cursor: default; }
.timeline-flt {
    font-size: var(--fs-2xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}

/* --- The rotation -------------------------------------------------------- */

.round-head { padding-bottom: var(--space-3); }
.round-route {
    display: block;
    font-size: var(--fs-lg);
    font-weight: 800;
    color: var(--text);
}
.round-dates { font-size: var(--fs-sm); color: var(--text-soft); }
.round-flags { display: flex; gap: var(--space-2); margin: var(--space-2) 0; }
.round-flag {
    padding: 2px var(--space-2);
    font-size: var(--fs-3xs);
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    border-radius: var(--r-xs);
}
.round-flag.is-ok {
    color: var(--green);
    background: var(--green-soft);
}
.round-flag.is-warn {
    color: var(--warning);
    background: color-mix(in srgb, var(--warning) 14%, transparent);
}
/* The grouping rule, printed on every rotation. A reader who disagrees
   with how these sectors were grouped needs to see the threshold that
   produced the answer, not a claim that it is correct. */
.round-rule {
    font-size: var(--fs-2xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}
.round-facts { margin-bottom: var(--space-4); }
.round-block { padding: var(--space-4) 0; border-top: 1px solid var(--card-border); }

.round-timeline { display: flex; flex-direction: column; gap: var(--space-1); }
.round-sector {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-3);
    text-align: start;
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-inline-start: 3px solid var(--accent);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.round-sector:hover { background: var(--card-border); }
.round-n {
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    font-size: var(--fs-3xs);
    font-weight: 800;
    color: var(--accent-ink);
    background: var(--surface-2);
    border-radius: var(--r-full);
}
.round-flight { font-size: var(--fs-sm); font-weight: 700; color: var(--text); }
.round-apt, .round-time {
    font-size: var(--fs-2xs);
    color: var(--text-soft);
    font-variant-numeric: var(--tnum);
}
/* Ground time between sectors. It sits between the two rows it separates,
   so the reading order of the timeline is the chronology. */
.round-gap {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    font-size: var(--fs-2xs);
    color: var(--text-faint);
}
.round-reason { flex: 1; font-style: italic; }
.round-why {
    margin: var(--space-3) 0 0;
    padding-inline-start: var(--space-4);
    font-size: var(--fs-2xs);
    color: var(--text-soft);
}
.round-hotels { display: flex; flex-direction: column; gap: var(--space-2); }
.round-hotel {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: var(--space-3);
    padding: var(--space-3);
    text-align: start;
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-inline-start: 3px solid var(--hotel-line, var(--hotel));
    border-radius: var(--r-sm);
    cursor: pointer;
}
.round-hotel-name { font-size: var(--fs-sm); font-weight: 700; color: var(--text); }
.round-hotel-time, .round-hotel-len {
    font-size: var(--fs-2xs);
    color: var(--text-soft);
    font-variant-numeric: var(--tnum);
}

/* The compact rotation list on the dashboard. */
.rounds-list { display: flex; flex-direction: column; gap: var(--space-2); }
.round-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    width: 100%;
    padding: var(--space-3);
    text-align: start;
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.round-row:hover { border-color: var(--accent); }
.round-row-dates {
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--accent-ink);
    font-variant-numeric: var(--tnum);
}
.round-row-route { font-size: var(--fs-sm); font-weight: 700; color: var(--text); }
.round-row-meta { font-size: var(--fs-2xs); color: var(--text-faint); }

/* --- The dashboard situation card ---------------------------------------- */

.sit-now { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.sit-badge {
    padding: 2px var(--space-2);
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--accent-ink);
    background: var(--accent-soft);
    border-radius: var(--r-xs);
}
.sit-what { font-size: var(--fs-md); font-weight: 800; color: var(--text); }
.sit-date { font-size: var(--fs-sm); color: var(--text-soft); }
.sit-tag {
    margin-inline-end: var(--space-2);
    padding: 1px var(--space-2);
    font-size: var(--fs-3xs);
    color: var(--text-soft);
    background: var(--gray-soft);
    border-radius: var(--r-xs);
}
.sit-next { margin-top: var(--space-2); font-size: var(--fs-sm); color: var(--text-soft); }
.sit-open { margin-top: var(--space-3); }
/* Report and departure, as two rows. Never merged into one "time": they are
   two instants, and the distance between them is the briefing. */
.sit-when {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-1) 0;
}
.sit-countdown {
    margin: var(--space-2) 0;
    font-size: var(--fs-lg);
    font-weight: 800;
    color: var(--accent-ink);
    font-variant-numeric: var(--tnum);
}

/* --- The alert centre -----------------------------------------------------
   PERF-02: `.alert-group` and `.alert-group-h` were here -- the wrappers 544996d
   unwired when the template stopped grouping. The list and the rows below are
   live. */
.alert-list { display: flex; flex-direction: column; gap: var(--space-2); }
.alert-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding: var(--space-3);
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-inline-start: 3px solid var(--text-faint);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.alert-row.is-err { border-inline-start-color: var(--danger); }
.alert-row.is-warn { border-inline-start-color: var(--warning); }
.alert-row.is-info { border-inline-start-color: var(--sky); }
.alert-row:hover { background: var(--card-border); }
.alert-kind {
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
}
.alert-text { flex: 1; min-width: 0; font-size: var(--fs-sm); color: var(--text); }
/* "Why?" is a per-row affordance rather than a link to somewhere else: the
   explanation is a sentence about this row, and the row is the thing that
   opens the underlying duty. */
.alert-why {
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--accent-ink);
    text-decoration: underline dotted;
    cursor: help;
}

/* --- Freshness, on the pill itself ------------------------------------------
   `data-state` comes from `syncProvenance()`. Three states rather than two:
   "not updated today" and "possibly out of date" are different warnings, and
   the old single threshold could only say the second -- so a roster imported
   yesterday and one imported last month looked alike until you read the date. */
.status-pill[data-state="stale"] { color: var(--warning); border-color: var(--warning); }
.status-pill[data-state="ageing"] { color: var(--text-soft); }
.status-pill[data-state="fresh"] { color: var(--text-soft); }
.status-pill[data-state="never-synced"] { color: var(--danger); border-color: var(--danger); }

.sync-provenance {
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--card-border);
}

/* --- The alert centre, by category -----------------------------------------
   Five sections instead of one flat severity-ordered list. The flat list is
   what put a rest shortfall from two years ago at the top of the card; the
   sections are what puts it under "Historical results", collapsed, where it is
   still readable and no longer reads as tonight's problem. */
.alert-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.alert-filters label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
}
.alert-filters select {
    font: inherit;
    font-size: var(--fs-xs);
    text-transform: none;
    letter-spacing: 0;
    font-weight: 600;
    color: var(--text);
    background: var(--card);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-1) var(--space-2);
    max-width: 15rem;
}
.alert-filters-n {
    font-size: var(--fs-2xs);
    color: var(--text-soft);
}
.alert-cat {
    border-top: 1px solid var(--card-border);
    padding: var(--space-3) 0;
}
.alert-cat:first-of-type { border-top: 0; }
.alert-cat > summary,
.alert-cat-h {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    cursor: pointer;
    list-style: none;
}
.alert-cat > summary::-webkit-details-marker { display: none; }
.alert-cat-h { cursor: default; }
/* `is-open` marks the two categories that are never collapsed: the distinction
   is in the markup as well as in the rules, so a stylesheet that drops one of
   them does not silently make history look active again. */
.alert-cat.is-open > .alert-cat-h { cursor: default; }
.alert-cat-name {
    font-size: var(--fs-sm);
    font-weight: 800;
    color: var(--text);
}
.alert-cat-count {
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--text-soft);
    white-space: nowrap;
}
.alert-cat-note { margin: var(--space-2) 0 var(--space-3); }
.alert-when {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-1);
    font-size: var(--fs-3xs);
    color: var(--text-faint);
    white-space: nowrap;
}
.alert-when.is-unknown { font-style: italic; }
.alert-state {
    padding: 0 var(--space-1);
    border-radius: var(--r-xs);
    background: var(--card-border);
    font-weight: 700;
}
.alert-state[data-state="future"] { background: var(--sky-soft); color: var(--sky-ink); }
.alert-state[data-state="today"] { background: var(--amber-soft); color: var(--amber-ink); }
.alert-flag {
    font-size: var(--fs-3xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
    border: 1px dashed var(--card-border);
    border-radius: var(--r-xs);
    padding: 0 var(--space-1);
}

/* --- The two flight-time scopes, side by side ------------------------------
   639 h 21 and 684 h 08 were both correct and both on one screen with nothing
   between them. Each scope is a row that says what it counts, so the difference
   is a stated rule rather than an apparent contradiction. */
.limit-scopes {
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
.limit-scope {
    display: grid;
    grid-template-columns: 1fr auto auto auto;
    align-items: baseline;
    gap: var(--space-3);
    padding: var(--space-2) 0;
}
.limit-scope-name {
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--text);
}
.limit-scope-val {
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
}
.limit-scope-pct,
.limit-scope-rest {
    font-size: var(--fs-2xs);
    color: var(--text-soft);
    font-variant-numeric: tabular-nums;
}
@media (max-width: 560px) {
    .limit-scope { grid-template-columns: 1fr auto; }
    .limit-scope-pct, .limit-scope-rest { grid-column: 2; }
}

/* --- Limits --------------------------------------------------------------- */

.limits-head { margin-bottom: var(--space-3); }
.limit-row {
    padding: var(--space-4) 0;
    border-top: 1px solid var(--card-border);
}
.limit-row.is-over .limit-name { color: var(--danger); }
.limit-name {
    margin-bottom: var(--space-2);
    font-size: var(--fs-sm);
    font-weight: 800;
    color: var(--text);
}
.limit-na {
    padding: var(--space-3);
    font-size: var(--fs-sm);
    font-style: italic;
    color: var(--text-faint);
    background: var(--gray-soft);
    border-radius: var(--r-sm);
}
.limit-bar {
    width: 100%;
    height: 8px;
    margin-top: var(--space-3);
    background: var(--gray-soft);
    border-radius: var(--r-xl);
    overflow: hidden;
}
.limit-bar .bar-fill {
    display: block;
    height: 100%;
    border-radius: var(--r-xl);
    overflow: hidden;
}
.limit-bar .bar-fill rect {
    fill: var(--accent);
}
.limit-row.is-over .limit-bar .bar-fill rect { fill: var(--danger); }
.limit-verdict {
    margin-top: var(--space-2);
    font-size: var(--fs-sm);
    font-weight: 700;
    color: var(--text);
}
.limit-proj {
    padding: var(--space-4) 0;
    border-top: 1px solid var(--card-border);
}
/* The band rules. Separated from the cumulative ones by a rule and a heading,
   because they are a different kind of answer -- a count of events and a
   minimum, not a total -- and reading them in the same column without a
   divider invites exactly the mistake the panel exists to prevent. */
.limit-bands {
    padding-top: var(--space-4);
    border-top: 2px solid var(--card-border);
}
.limit-bands > .limit-name {
    margin-bottom: var(--space-3);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: var(--fs-2xs);
    color: var(--text-faint);
}
/* The row where a projection crosses the threshold. Marked in place so the
   reader sees which rotation did it rather than only that one did. */
.limit-proj tr.is-cross td { color: var(--warning); font-weight: 800; }

/* --- The period bar and the cross-filter --------------------------------- */

.period-bar { margin-bottom: var(--space-4); }
.period-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin-bottom: var(--space-2);
}
.period-chip {
    padding: 3px var(--space-3);
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--text-soft);
    background: var(--gray-soft);
    border: 1px solid var(--card-border);
    border-radius: var(--r-full);
    cursor: pointer;
}
.period-chip.is-on {
    color: var(--accent-ink);
    border-color: var(--accent);
    background: var(--accent-soft);
}
.period-select {
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-sm);
    color: var(--text);
    background: var(--input);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
}
.stats-dest { margin-top: var(--space-4); }
.dest-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 2fr auto;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    text-align: start;
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    cursor: pointer;
}
.dest-row:hover { background: var(--gray-soft); }
.dest-code { font-weight: 800; color: var(--text); }
.dest-name {
    font-size: var(--fs-xs);
    color: var(--text-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.dest-n {
    font-size: var(--fs-2xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}
.stats-compare { margin-top: var(--space-4); }
.stats-compare th { text-align: start; }
.stats-compare .is-up { color: var(--green); font-weight: 700; }
.stats-compare .is-down { color: var(--warning); font-weight: 700; }
.stats-compare .is-flat { color: var(--text-faint); }
/* The two containers exist so the stats pane can hold the period analysis
   and its comparison independently: emptying one must not blank the other. */
.stats-metrics { margin-bottom: var(--space-4); }
.stats-compare-box:empty { display: none; }
.stats-doors { display: flex; gap: var(--space-2); margin-top: var(--space-4); }
.plan-stats { display: flex; flex-direction: column; }

/* --- The sync view -------------------------------------------------------- */

.sync-head { margin-bottom: var(--space-3); }
.sync-counts { margin-bottom: var(--space-4); }
.sync-months {
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--card-border);
}
/* The rule that grouped this rotation, under a label that says so. */
.round-k {
    margin-top: var(--space-2);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
}
.sync-actions { margin-top: var(--space-4); }
.modal-wide { max-width: 720px; }

/* --- The grouped search results ------------------------------------------ */

.search-group { margin-bottom: var(--space-4); }
.search-group-h {
    margin: 0 0 var(--space-2);
    font-size: var(--fs-2xs);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-faint);
}
/* What this group of results can answer, so a reader knows whether to keep
   typing before concluding the roster does not hold what they want. */
.search-hint {
    margin-inline-start: var(--space-2);
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
    color: var(--text-faint);
    opacity: 0.85;
}
.search-list { display: flex; flex-direction: column; gap: var(--space-1); }
.search-total {
    margin-bottom: var(--space-2);
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}
.search-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    text-align: start;
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    cursor: pointer;
}
.search-row:hover { background: var(--gray-soft); }
/* The rotations that went through the selected airport. Under the chips, so a
   reader who has just clicked a map pin reads the answer in the same place
   they clicked rather than having to find it. */
.map-airport-rounds:empty { display: none; }
.map-airport-rounds { margin-top: var(--space-3); }
.map-airport-rounds .search-list { max-height: 168px; overflow-y: auto; }
.search-title { font-weight: 700; color: var(--text); }
.search-sub {
    font-size: var(--fs-2xs);
    color: var(--text-faint);
    font-variant-numeric: var(--tnum);
}

/* --- Narrow screens -------------------------------------------------------
   The rotation timeline is a three-column grid and the sheet's fact grid
   is a two-column one; below the phone breakpoint both collapse to a
   single column and the labels move above their values rather than
   disappearing, because a rotation you cannot read on a phone is not a
   rotation you can check you are on. */
@media (max-width: 640px) {
    .sheet-route { flex-direction: column; align-items: stretch; }
    .sheet-arrow { transform: rotate(90deg); align-self: center; }
    .sheet-grid { grid-template-columns: minmax(0, 1fr); }
    .round-sector { grid-template-columns: auto 1fr; }
    .round-apt, .round-time { grid-column: 2; }
    .round-hotel { grid-template-columns: 1fr; }
    .dest-row { grid-template-columns: auto 1fr; }
    .dest-freq-bar, .dest-n { grid-column: 2; }
}

/* --- Print ----------------------------------------------------------------
   A rotation sheet printed for a briefing carries the timeline and the
   numbers; the doors and the chips are interaction, and a dead control on
   paper is noise. */

/* === 24 · COCKPIT GLASS & OLED PRECISION — refonte Flighty Pro ============
   Carte blanche v2026.10.14. Additive only: every rule below refines the same
   DOM, ids and JS hooks as sections 01-23. No !important anywhere in this
   section (test_css_overrides only classifies !important past the COCKPIT
   COMPONENTS marker, so this section is invisible to it by construction).
   No new assets, no external fetch: offline-first and CSP-safe.
   Structure:
     24.0 Tokens (OLED night + Alabaster day)
     24.1 Boarding-pass hero
     24.2 Bento KPI
     24.3 Calendar pills & cells
     24.4 Day timeline & hand badge
     24.5 FTL cockpit gauges
     24.6 Route-map ND & chips
     24.7 Dialogs (14 modales)
     24.8 Responsive, motion, overflow guards
   ========================================================================== */

/* --- 24.0 Tokens ------------------------------------------------------------
   Dark "OLED Cockpit Night": deeper ground than --bg so the hero ticket can
   glow against it. Light "Daylight Alabaster": warm luminous ground, pure
   white cards, pastel badges whose *text* reuses the measured -ink tokens
   (all already WCAG AA on white), so no new contrast debt is introduced. */
:root {
    --oled-0: #07090e;
    --oled-1: #0c1017;
    --glass-blur: blur(20px) saturate(180%);
    --ftl-ok: #10b981;
    --ftl-near: #f59e0b;
    --ftl-over: #f26262;
    --bp-glow: 0 0 24px -6px rgba(56, 189, 248, 0.45);
}
[data-theme="light"] {
    --oled-0: #f4f6f9;
    --oled-1: #eef1f6;
    --ftl-ok: #047857;
    --ftl-near: #92400e;
    --ftl-over: #b91c1c;
    --bp-glow: 0 12px 32px -12px rgba(2, 132, 199, 0.35);
}

/* Instrumental figures: strictly tabular, everywhere counts are read. */
.mono,
.strip-dep-in,
.strip-block,
.strip-local,
.bp-tz,
.bp-wx-temp,
.limit-pct,
.rot-leg-time,
.cal-evt-time,
.stay-count,
.op-countdown {
    font-variant-numeric: tabular-nums;
}

/* --- 24.1 Boarding-pass hero ------------------------------------------------
   .flight-strip keeps its contract (role=button, .strip-head/.strip-route /
   .strip-times/.strip-rest/.strip-foot all still present). The .bp-* classes
   dress it as a ticket: airline badge, live status pill, wx pill, giant IATA
   pair over an animated SVG arc, instrument band below. */
.bento-hero .flight-strip {
    position: relative;
    overflow: hidden;
    border-radius: var(--r-xl);
    border: 1px solid var(--card-border-2);
    background:
        radial-gradient(120% 90% at 50% -20%, rgba(56, 189, 248, 0.14), transparent 60%),
        radial-gradient(100% 80% at 90% 110%, rgba(139, 92, 246, 0.12), transparent 55%),
        linear-gradient(180deg, var(--oled-1), var(--oled-0));
    box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.07);
    padding: var(--space-5) var(--space-5) var(--space-4);
    cursor: pointer;
    transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease);
}
[data-theme="light"] .bento-hero .flight-strip {
    background:
        radial-gradient(120% 90% at 50% -20%, rgba(2, 132, 199, 0.10), transparent 60%),
        linear-gradient(180deg, #ffffff, #f4f6f9);
    box-shadow: 0 1px 2px rgba(9, 9, 11, 0.06), 0 12px 32px -12px rgba(9, 9, 11, 0.16),
        inset 0 1px 0 #ffffff;
}
.bento-hero .flight-strip:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-xl), var(--bp-glow), inset 0 1px 0 rgba(255, 255, 255, 0.09);
}
/* Perforation: the ticket tear line above the footer. */
.bento-hero .flight-strip::after {
    /* La section 23 posait ici un pointillé en débord (`left/right: -8px`)
       qui élargissait la zone de défilement du billet ; la perforation du
       billet (.strip-foot ci-dessous) le remplace, DOM inchangé. */
    content: none;
}
.bento-hero .strip-foot {
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px dashed color-mix(in srgb, var(--text-faint) 45%, transparent);
    font-size: var(--fs-xs);
    color: var(--text-faint);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.bento-hero .bp-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.bento-hero .bp-airline {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 34px;
    height: 34px;
    padding: 0 var(--space-3);
    border-radius: var(--r-md);
    font-size: var(--fs-sm);
    font-weight: 800;
    letter-spacing: 0.04em;
    color: #fff;
    background: linear-gradient(160deg, #0369a1, #0c4a6e);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 4px 12px -4px rgba(2, 132, 199, 0.7);
}
.bento-hero .bp-airline svg {
    width: 18px;
    height: 18px;
    flex: none;
}
.bento-hero .bp-airline-name {
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 0.06em;
    white-space: nowrap;
}
.bento-hero .strip-flt {
    font-size: var(--fs-lg);
    font-weight: 800;
    letter-spacing: 0.01em;
}
.bento-hero .bp-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-left: auto;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-full);
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border: 1px solid var(--card-border);
    background: color-mix(in srgb, var(--sky) 14%, transparent);
    color: var(--sky-ink);
}
.bento-hero .bp-status.is-live {
    background: color-mix(in srgb, var(--green) 16%, transparent);
    color: var(--green-ink);
    border-color: color-mix(in srgb, var(--green) 45%, transparent);
}
.bento-hero .bp-status.is-brief {
    background: color-mix(in srgb, var(--amber) 16%, transparent);
    color: var(--amber-ink);
    border-color: color-mix(in srgb, var(--amber) 45%, transparent);
}
.bento-hero .bp-pulse {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent);
    animation: bp-pulse 2s var(--ease) infinite;
}
@keyframes bp-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.5; transform: scale(0.82); }
}
.bento-hero .bp-wx {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-full);
    border: 1px solid var(--card-border);
    background: var(--glass-tint);
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--text-soft);
}
.bento-hero .bp-wx svg { width: 14px; height: 14px; }
/* Trajectoire: heroic IATA pair, arc SVG animé, durée de bloc au-dessus. */
.bento-hero .bp-route {
    align-items: end;
    margin-top: var(--space-2);
}
.bento-hero .bp-from b,
.bento-hero .bp-to b {
    font-size: var(--fs-3xl);
    font-weight: 800;
    letter-spacing: 0.01em;
    line-height: 1;
}
.bento-hero .bp-from span,
.bento-hero .bp-to span {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.bento-hero .bp-to { text-align: right; }
.bento-hero .bp-tz {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--fs-2xs);
    color: var(--text-faint);
}
.bento-hero .bp-leg { position: relative; padding-bottom: 62px; display: flex; flex-direction: column; align-items: center; gap: var(--space-1); }
/* La durée de bloc survit au-dessus de l'arc ; les deux tirets hérités du
   strip doublonnaient la trajectoire, ils s'effacent ici (DOM inchangé). */
.bento-hero .bp-leg .strip-leg-line { display: none; }
.bento-hero .bp-leg .strip-block {
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 0.05em;
    color: var(--text-soft);
    background: color-mix(in srgb, var(--input) 70%, transparent);
    border: 1px solid var(--card-border);
    border-radius: var(--r-full);
    padding: var(--space-1) var(--space-3);
}
.bento-hero .bp-meta {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}
/* Pack excellence §2: full stop chain under the arc for multi-leg duties
   (ALG → ELU → TMR → ELU → ALG), plus the sector-count badge. */
.bento-hero .bp-chain {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--text-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}
.bento-hero .bp-n {
    font-size: var(--fs-2xs);
    font-weight: 800;
    color: var(--sky-ink);
    border: 1px solid var(--card-border-2);
    border-radius: var(--r-full);
    padding: 0 var(--space-2);
    margin-left: var(--space-1);
    white-space: nowrap;
}
.bento-hero .bp-arc {
    position: absolute;
    left: 4%;
    right: 4%;
    bottom: 16px;
    width: 92%;
    height: 44px;
    /* Hidden, not visible: le halo drop-shadow de l'avion élargirait sinon la
       zone de défilement du billet (scrollWidth +8 px) sans rien peindre hors
       carte — tout tient déjà dans le viewBox. */
    overflow: hidden;
}
.bento-hero .bp-arc-path {
    fill: none;
    stroke: var(--sky-ink);
    stroke-width: 1.6;
    stroke-dasharray: 5 4;
    opacity: 0.85;
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 70%, transparent));
    animation: bp-dash 2.6s linear infinite;
}
@keyframes bp-dash { to { stroke-dashoffset: -18; } }
.bento-hero .bp-arc-dot { fill: var(--sky-ink); opacity: 0.9; }
.bento-hero .bp-plane path { fill: var(--text); }
.bento-hero .bp-plane {
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--sky) 80%, transparent));
}
/* Bandeau d'instrumentation: 5 cadrans lisibles, seuils nets. */
.bento-hero .bp-instruments {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--space-2);
    margin-top: var(--space-4);
    padding: var(--space-3);
    border-radius: var(--r-lg);
    border: 1px solid var(--card-border);
    background: color-mix(in srgb, var(--input) 55%, transparent);
}
.bento-hero .strip-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}
.bento-hero .strip-key {
    font-size: var(--fs-3xs);
    font-weight: 800;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.bento-hero .strip-col b {
    font-size: var(--fs-md);
    font-variant-numeric: tabular-nums;
}
[data-theme="light"] .bento-hero .bp-instruments {
    background: rgba(255, 255, 255, 0.72);
}
[data-theme="light"] .bento-hero .bp-leg .strip-block {
    background: #fff;
}
/* Jauge de repos pré-vol: seuil opérateur matérialisé par le tick. */
.bento-hero .strip-rest {
    margin-top: var(--space-3);
}
.bento-hero .strip-rest-bar {
    position: relative;
    display: block;
    height: 8px;
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--text-faint) 22%, transparent);
    overflow: hidden;
}
.bento-hero .strip-rest-bar .bar-fill { display: block; height: 100%; }
.bento-hero .strip-rest.ok .bar-fill rect { fill: var(--ftl-ok); }
.bento-hero .strip-rest.bad .bar-fill rect { fill: var(--ftl-over); }
.bento-hero .strip-rest-txt {
    display: block;
    margin-top: var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-soft);
}

/* --- 24.2 Bento KPI -----------------------------------------------------------
   Grille respirante, icônes dédiées, survol fluide. Les cartes existent déjà
   (.stat-card/.kpi) : on leur donne la profondeur verre + l'ascenseur commun. */
.bento {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}
.bento-hero { grid-column: span 8; }
.bento-stay { grid-column: span 4; }
.dash-card,
.kpi,
.stat-card,
.chart-card {
    border: 1px solid var(--card-border);
    box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.05);
    transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease);
}
[data-theme="light"] .dash-card,
[data-theme="light"] .kpi,
[data-theme="light"] .stat-card,
[data-theme="light"] .chart-card {
    box-shadow: 0 1px 2px rgba(9, 9, 11, 0.05), 0 10px 28px -14px rgba(9, 9, 11, 0.18),
        inset 0 1px 0 #ffffff;
}
.dash-card:hover,
.kpi:hover,
.stat-card:hover {
    transform: translateY(-3px);
}
.dash-title {
    font-size: var(--fs-xs);
    font-weight: 800;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.dash-title .dicon {
    color: var(--sky-ink);
}

/* --- 24.3 Calendrier ------------------------------------------------------------
   Cases élégantes, anneau lumineux sur aujourd'hui, pilules structurées
   (avion + vol + heure), ruban d'hôtel continu, tons standby/training/off. */
.cal-cell {
    min-width: 0;
    border-radius: var(--r-sm);
    transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.cal-cell:hover { background: var(--gray-soft); }
/* UI-01: §24.3 .cal-cell.today ring removed -- dead by construction: the
   cockpit rule above wins on !important at equal specificity, so this never
   painted. The effective today marker stays the sober accent-ink pastille.
   No new !important added. */
.cal-evt .cal-evt-ico, .cal-lane .cal-evt-ico { width: 12px; height: 12px; flex: none; }
.cal-evt-dest {
    display: none; flex: none; font-weight: 800; letter-spacing: 0.04em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A lazy section's placeholder (QA 360 Mo1). Its height comes from `--lazy-h`,
   set through the CSSOM: an inline style attribute would be a CSP violation on
   every Stats render, since this app ships `style-src 'self'` with no
   `unsafe-inline`. It carries nothing else -- a placeholder that looked like
   content would be content. */
.lazy-ph {
    display: block;
    width: 100%;
    /* Declared here as well as being set per-instance: the scale check reads
       every `var(--x)` in this file and requires `--x` to be defined somewhere,
       and a property that only ever arrives from JavaScript would otherwise
       read as a typo. */
    --lazy-h: 120px;
    min-height: var(--lazy-h);
}

.cal-evt-n {
    flex: none;
    font-weight: 800;
    color: var(--sky-ink);
    background: color-mix(in srgb, var(--sky) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--sky) 40%, transparent);
    border-radius: var(--r-full);
    padding: 0 var(--space-2);
}

/* Below 620px a month column is 52px wide. Measured there: the chip needs 80px of
   content for a 45px box, so a crossing rectangle cannot be drawn at all -- there
   is no room for it to reach into a neighbour that is 52px away. What is kept is
   the information set the app already used at this width (number, departure,
   arrival code) and the crossing is carried by a `+1j` on the departure instead.
   The cost is stated rather than hidden: on a phone the month is a list of facts,
   not a picture of spans. */
@media (max-width: 620px) {
    .cal-evt.cal-cross { width: auto; max-width: 100%; }
    .cal-lane { display: none; }
    .cal-evt .cal-tarr { display: none; }
    .cal-evt .cal-evt-l1 .cal-evt-dest { display: inline; font-size: var(--fs-2xs); }
    .cal-xmark { display: inline; }
    .cal-evt-dest i { font-style: normal; }
    .cal-evt-dest .loop { display: none; }
    .cal-evt.t-flight .cal-evt-dest .loop,
    .cal-evt.t-deadhead .cal-evt-dest .loop { display: inline; }
    .cal-evt.t-flight .cal-evt-dest .full,
    .cal-evt.t-deadhead .cal-evt-dest .full { display: none; }
}

/* The flight number, visible in every calendar sub-view (QA 360 M9).

   Placed before `.chip-txt` and given `flex: none` so the ROUTE is what the
   ellipsis eats on a narrow cell -- the number is the token a crew member
   quotes and must never be the part that disappears. On the narrowest widths
   the route is replaced anyway by `.cal-evt-dest` (the arrival code), which
   frees the space for it. */
.cal-evt-flt {
    flex: none;
    font-weight: 800;
    letter-spacing: 0.02em;
    color: var(--text-soft);
}
@media (max-width: 620px) {
    .cal-evt.t-flight .cal-evt-dest,
    .cal-evt.t-deadhead .cal-evt-dest {
        display: inline;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .cal-cell .cal-evt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
    /* Out of base keeps its diamond wherever a chip can hold a glyph. In a 47px
       chip it cost the ARRIVAL CODE its room -- the thing that says where the
       day ends -- so below 620 it goes, as the flight number already does. */
    .cal-obmark { display: none; }
    /* The duty marker in a 52px column: three lines of prose wrapped to six and
       cut "Service" in half, so below 620 it is one line and the departure it
       points at stays in the tooltip and in the next day's chip. */
    /* A 52px column keeps the NUMBERS and loses the words: `report 15:25 - fin
       06:42 le 12` measured `rep...` there. What survives is `15h17` and `60 h`. */
    .cal-foot-when, .cal-hotel-name { display: none; }
    /* two selectors: the base `flex: none` sits 700 lines BELOW and loses. */
    .cal-cell .cal-hotel-n { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
    .cal-start-ar, .cal-start small b { display: none; }
    .cal-start small:nth-of-type(2) { display: none; }
    .cal-start small { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
.cal-role-standby {
    border: 1px dashed color-mix(in srgb, var(--standby) 60%, transparent);
}
.cal-role-training {
    border: 1px dashed color-mix(in srgb, var(--training) 60%, transparent);
}
.cal-role-hotel {
    background: linear-gradient(90deg, var(--hotel-soft), transparent);
    border-radius: var(--r-xs);
}
/* Un coucher s'étale sur les jours qu'il couvre (coversDay dans app-cal.js) :
   les jours de suite aplatissent le bord d'attaque pour se lire comme le même
   séjour plutôt qu'une nouvelle escale. */
.cal-chip.cal-cont {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    border-left-style: dashed;
}
.cal-offbar {
    height: 4px;
    border-radius: var(--r-full);
    background: linear-gradient(90deg, color-mix(in srgb, var(--text-faint) 55%, transparent), transparent);
    margin-top: var(--space-1);
}

/* --- 24.4 Timeline du jour & badge main ----------------------------------------
   Épine verticale chronologique ; pastille « corrigé main » avec écart nommé. */
.day-timeline .rot-card,
.rot-timeline .rot-card {
    border: 1px solid var(--card-border);
    box-shadow: var(--shadow-sm);
}
.day-sheet .rot-node { position: relative; }
.hand-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-full);
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.03em;
    color: var(--amber-ink);
    background: var(--amber-soft);
    border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent);
}
.hand-badge svg { width: 12px; height: 12px; }

/* --- 24.5 Cockpit FTL -------------------------------------------------------------
   Cartes d'instruments: jauge HD avec graduation (tick à 80 %), % tabulaire,
   marge restante, code couleur Émeraude -> Ambre -> Corail. */
.ftl-gauge {
    border: 1px solid var(--card-border);
    border-radius: var(--r-lg);
    padding: var(--space-4);
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--surface-2) 55%, transparent), transparent 70%);
}
.ftl-gauge .limit-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
}
.ftl-gauge .limit-pct {
    font-size: var(--fs-lg);
    font-weight: 800;
    color: var(--ftl-ok);
}
.ftl-gauge.is-near .limit-pct { color: var(--ftl-near); }
.ftl-gauge.is-over .limit-pct { color: var(--ftl-over); }
.ftl-gauge .limit-bar {
    position: relative;
    width: 100%;
    height: 10px;
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--text-faint) 22%, transparent);
    overflow: hidden;
    margin-top: var(--space-3);
}
.ftl-gauge .limit-bar .bar-fill { display: block; height: 100%; }
.ftl-gauge .limit-bar rect { fill: var(--ftl-ok); }
.ftl-gauge.is-near .limit-bar rect { fill: var(--ftl-near); }
.ftl-gauge.is-over .limit-bar rect { fill: var(--ftl-over); }
.ftl-gauge .limit-tick {
    position: absolute;
    top: -2px;
    bottom: -2px;
    left: 80%;
    width: 2px;
    border-radius: var(--r-xs);
    background: var(--text);
    opacity: 0.75;
}
.ftl-gauge.is-over {
    border-color: color-mix(in srgb, var(--ftl-over) 55%, transparent);
    box-shadow: 0 0 18px -6px color-mix(in srgb, var(--ftl-over) 60%, transparent);
}
.ftl-gauge.is-near {
    border-color: color-mix(in srgb, var(--ftl-near) 50%, transparent);
}

/* --- 24.6 Carte ND & chips ----------------------------------------------------------
   Écran de navigation: océan profond, arcs lumineux, halos, chips cockpit. */
.map-card {
    overflow: hidden;
}
#route-map {
    border-radius: var(--r-lg);
    background: radial-gradient(120% 100% at 50% 0%, #101b30 0%, #07090e 70%);
}
[data-theme="light"] #route-map {
    background: radial-gradient(120% 100% at 50% 0%, #e8f1fa 0%, #f4f6f9 70%);
}
#route-map .map-ocean { fill: var(--surface-2); fill: url(#nd-ocean-grad); }
[data-theme="light"] #route-map .map-ocean { fill: #e2eaf3; }
#route-map .map-grid {
    stroke: rgba(148, 163, 184, 0.14);
    stroke-width: 0.6;
}
#route-map .map-land {
    fill: rgba(148, 163, 184, 0.16);
    stroke: rgba(148, 163, 184, 0.3);
    stroke-width: 0.6;
}
[data-theme="light"] #route-map .map-land { fill: rgba(100, 116, 139, 0.18); }
#route-map .map-route {
    fill: none;
    stroke: #38bdf8;
    opacity: 0.75;
    filter: drop-shadow(0 0 4px rgba(56, 189, 248, 0.7));
    transition: opacity var(--dur-fast) var(--ease);
}
#route-map .map-route.highlighted {
    opacity: 1;
    stroke: #7dd3fc;
    filter: drop-shadow(0 0 8px rgba(125, 211, 252, 0.9));
}
#route-map .map-dot {
    fill: #0c1017;
    stroke: #38bdf8;
    stroke-width: 1.6;
    filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.8));
}
#route-map .map-dot.base-hub {
    fill: #38bdf8;
    stroke: #e0f2fe;
}
#route-map .map-hub-ring {
    fill: none;
    stroke: rgba(56, 189, 248, 0.55);
    stroke-width: 1.2;
    animation: bp-pulse 2.6s var(--ease) infinite;
}
#route-map .map-label {
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    fill: #cbd5e1;
    paint-order: stroke;
    stroke: rgba(7, 9, 14, 0.85);
    stroke-width: 2.5px;
}
[data-theme="light"] #route-map .map-label { fill: #334155; stroke: rgba(255, 255, 255, 0.9); }
#map-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}
#map-chips .map-chip {
    font-variant-numeric: tabular-nums;
    font-weight: 800;
    font-size: var(--fs-xs);
    letter-spacing: 0.04em;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--r-full);
    border: 1px solid var(--card-border-2);
    background: color-mix(in srgb, var(--input) 70%, transparent);
    color: var(--text-soft);
    cursor: pointer;
    transition: transform var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
        border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
#map-chips .map-chip:hover {
    transform: translateY(-2px);
    color: var(--text);
    box-shadow: var(--shadow-md);
}
#map-chips .map-chip.active {
    color: #fff;
    background: linear-gradient(180deg, #0284c7, #0c4a6e);
    border-color: rgba(125, 211, 252, 0.6);
    box-shadow: 0 0 14px -2px rgba(56, 189, 248, 0.7);
}

/* --- 24.7 Dialogues ------------------------------------------------------------------
   14 role="dialog": en-tête épuré, actions primaires/secondaires, champs
   modernes, anneau :focus-visible net, ouverture 180 ms ease-out. */
.modal-root {
    animation: modal-fade 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.modal-root .modal {
    animation: modal-pop 180ms cubic-bezier(0.16, 1, 0.3, 1);
    border: 1px solid var(--card-border-2);
    box-shadow: var(--shadow-xl), inset 0 1px 0 rgba(255, 255, 255, 0.07);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
}
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-pop {
    from { opacity: 0; transform: scale(0.96) translateY(6px); }
    to { opacity: 1; transform: none; }
}
.modal-head {
    border-bottom: 1px solid var(--card-border);
}
.modal-head h2 {
    font-size: var(--fs-md);
    letter-spacing: 0;
}
.modal .field label {
    font-size: var(--fs-2xs);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.modal input:not([type="checkbox"]):not([type="radio"]),
.modal select,
.modal textarea {
    border-radius: var(--r-md);
    border: 1px solid var(--card-border-2);
    background: var(--input);
    padding: var(--space-3) var(--space-4);
    transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.modal input:not([type="checkbox"]):not([type="radio"]):focus-visible,
.modal select:focus-visible,
.modal textarea:focus-visible {
    outline: 2px solid var(--sky);
    outline-offset: 1px;
    border-color: var(--sky);
}
/* Daylight: les champs héritent de --input, resté ardoise sombre dans le
   thème clair (jeton non redéfini). Sur une modale blanche, un champ sombre
   à texte sombre est illisible : fond blanc, encre ardoise, deux valeurs
   déjà mesurées AA sur blanc. */
[data-theme="light"] .modal input:not([type="checkbox"]):not([type="radio"]),
[data-theme="light"] .modal select,
[data-theme="light"] .modal textarea {
    background: #fff;
    color: #18181b;
}
/* Même dette --input sur le sélecteur « Comparer » (period-select), seul
   contrôle hors modale à fond --input : carte blanche en clair. */
[data-theme="light"] .period-select {
    background: #fff;
    color: #18181b;
}
.modal-actions .btn-accent {
    box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--accent) 70%, transparent),
        inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

/* --- 24.8 Responsive, motion, anti-débordement ------------------------------------------
   1440 / 768 / 390 (+ paysage 844x390) : jamais de scroll horizontal,
   IATA héroïques contenus, instruments 2x2 sur téléphone. */
main, .hero, .tabs, .bento, .dash-grid, .calendar-view, .map-layout {
    min-width: 0;
    max-width: 100%;
}
/* La grille ne peut rétrécir ses pistes que si les items l'acceptent : le
   minimum automatique (min-content du billet, IATA héroïques) forçait 6 px
   de débordement sous 1100 px. Zéro minimum sur les items, le billet ondule
   en dedans (overflow hidden + ellipsis déjà en place). */
.bento > *,
.bento > section,
.bento-hero,
.bento-stay,
.dash-grid > .dash-card {
    min-width: 0;
    max-width: 100%;
}
.bento {
    padding: 0;
    margin-left: 0;
    margin-right: 0;
    overflow-x: clip;
}
.bento-hero .flight-strip {
    min-width: 0;
    max-width: 100%;
}
.bento-hero .bp-head,
.bento-hero .bp-route,
.bento-hero .bp-instruments,
.strip-head,
.strip-route,
.strip-times {
    min-width: 0;
    max-width: 100%;
}
.strip-end b,
.cal-evt,
.chip-txt,
.stay-val,
.portal-msg-txt {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
@media (max-width: 1100px) {
    .bento-hero, .bento-stay { grid-column: 1 / -1; }
}
@media (max-width: 768px) {
    .bento-hero .bp-from b,
    .bento-hero .bp-from b,
    .bento-hero .bp-to b { font-size: var(--fs-2xl); }
}
@media (max-width: 620px) {
    .bento { gap: var(--space-3); }
    .bento-hero .flight-strip { padding: var(--space-4); }
    .bento-hero .bp-instruments { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .bento-hero .bp-status { margin-left: 0; }
    .bento-hero .bp-leg { padding-bottom: 40px; }
    .cal-cell { min-height: 64px; }
    .modal-root .modal { margin: var(--space-3); max-width: calc(100vw - 24px); }
}
@media (max-height: 500px) {
    .bento-hero .bp-leg { padding-bottom: 34px; }
    .bento-hero .bp-arc { height: 34px; }
}
/* Reduced motion lives in the single section-22 block (M47): the refonte
   loops (.bp-arc-path dash, .bp-pulse, .map-hub-ring) are listed there by
   name, and the hover lifts below are transform-only so the blanket
   transition-duration rule already stills them. */

/* --- 24.9 Médaillons vectoriels repos & escale --------------------------------
   Les emojis système (palmier, hôtel) deviennent des icônes SVG logées dans un
   médaillon dégradé : lune/étoiles pour le repos, bâtiment pour l'escale. Le
   texte traduit reste intact à côté du médaillon. */
.live-title {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}
.live-ico-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 26px;
    height: 26px;
    border-radius: var(--r-full);
    border: 1px solid;
}
.live-ico-badge svg { width: 15px; height: 15px; }
.live-ico-badge.is-rest {
    color: var(--green-ink);
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--green) 26%, transparent),
        color-mix(in srgb, var(--green) 8%, transparent));
    border-color: color-mix(in srgb, var(--green) 45%, transparent);
    box-shadow: 0 0 10px -2px color-mix(in srgb, var(--green) 60%, transparent);
}
.live-ico-badge.is-hotel {
    color: var(--purple);
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--purple) 26%, transparent),
        color-mix(in srgb, var(--purple) 8%, transparent));
    border-color: color-mix(in srgb, var(--purple) 45%, transparent);
    box-shadow: 0 0 10px -2px color-mix(in srgb, var(--purple) 60%, transparent);
}
.rest-day-card .rest-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    color: var(--green-ink);
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--green) 24%, transparent),
        color-mix(in srgb, var(--green) 7%, transparent));
    border: 1px solid color-mix(in srgb, var(--green) 40%, transparent);
    box-shadow: 0 4px 12px -4px color-mix(in srgb, var(--green) 55%, transparent);
}
.rest-day-card .rest-icon svg { width: 19px; height: 19px; }

/* --- 24.10 Boarding pass Flighty Pro : double arc, pistes, perforation ------
   L'arc SVG est bi-couche (piste pointillée + arc actif lumineux de progression
   + halo), les aéroports sont des anneaux (r=6) à point central (r=2.8), et la
   jonction route/instruments devient une ligne de découpe de billet avec
   oeillets latéraux internes (jamais en débord) et reflet métallique sur
   l'en-tête. */
.bento-hero .bp-arc-track {
    fill: none;
    stroke: var(--text-faint);
    stroke-width: 1.2;
    stroke-dasharray: 4 4;
    opacity: 0.65;
    filter: none;
}
.bento-hero .bp-arc-active {
    fill: none;
    stroke: var(--sky);
    stroke-width: 2.2;
    stroke-linecap: round;
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 75%, transparent));
}
.bento-hero .bp-arc-halo { fill: var(--sky); opacity: 0.14; }
.bento-hero .bp-arc-ring {
    fill: var(--card);
    stroke: var(--sky-ink);
    stroke-width: 1.8;
    opacity: 0.95;
}
.bento-hero .bp-arc-dot { fill: var(--sky-ink); opacity: 0.95; }
.bento-hero .bp-plane-halo { fill: var(--sky); opacity: 0.18; }
.bento-hero .bp-plane-medal {
    fill: var(--card);
    stroke: var(--sky-ink);
    stroke-width: 1.2;
    opacity: 0.96;
}
.bento-hero .bp-plane.is-parked { opacity: 0.55; }
.bento-hero .bp-head { position: relative; }
.bento-hero .bp-head::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: var(--r-sm);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent 60%);
    pointer-events: none;
}
.bento-hero .bp-instruments {
    position: relative;
    border-top: 1px dashed color-mix(in srgb, var(--text-faint) 45%, transparent);
}
/* Oeillets de perforation retirés : deux cercles gris parasites flottaient
   dans les coins supérieurs internes de la boîte des 5 instruments. La ligne
   de découpe (border-top dashed) suffit ; aucune encoche ne déborde. */
.bento-hero .bp-instruments::before,
.bento-hero .bp-instruments::after {
    content: none;
    display: none;
}

/* --- 24.11 Écran radar ND & instruments Stats --------------------------------
   Anneaux de portée et axes de visée autour du hub, halos sous les routes,
   donut FTL 36 unités, spectre Duty Mix segmenté, histogramme mensuel poli. */
#route-map .map-nd-ring {
    fill: none;
    stroke: color-mix(in srgb, var(--sky) 30%, transparent);
    stroke-width: 0.8;
}
#route-map .map-nd-cross {
    stroke: color-mix(in srgb, var(--sky) 14%, transparent);
    stroke-width: 0.8;
}
#route-map .map-route-glow {
    fill: none;
    stroke: color-mix(in srgb, var(--sky) 55%, transparent);
    stroke-linecap: round;
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease);
    pointer-events: none;
}
#route-map .map-route-g:hover .map-route-glow { opacity: 0.5; }
#route-map .map-route-g:has(.map-route.highlighted) .map-route-glow { opacity: 0.6; }
#route-map .map-route-g:has(.map-route.is-preview) .map-route-glow { opacity: 0.6; }
#route-map .map-route-glow.is-preview { opacity: 0.6; }
.mix-spectrum {
    display: block;
    width: 100%;
    height: 10px;
    border-radius: var(--r-full);
    overflow: hidden;
    background: var(--gray-soft);
    margin-bottom: var(--space-3);
    box-shadow: inset 0 1px 2px rgba(11, 25, 45, 0.10);
}
.mix-spectrum rect { fill: var(--off-ink); }
.mix-spectrum rect.sfill-flight { fill: var(--flight); }
.mix-spectrum rect.sfill-hotel { fill: var(--hotel); }
.mix-spectrum rect.sfill-standby { fill: var(--standby); }
.mix-spectrum rect.sfill-training { fill: var(--training); }
.mix-spectrum rect.sfill-reserve { fill: var(--reserve); }
.mix-row .dot {
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 15%, transparent),
        0 0 6px -1px currentColor;
}
.mtr-defs { display: block; width: 0; height: 0; overflow: hidden; }
.mtr-grad-a { stop-color: var(--sky); }
.mtr-grad-b { stop-color: var(--flight-ink); }
.mtr-bar .bar-fill {
    filter: drop-shadow(0 0 3px color-mix(in srgb, var(--flight) 55%, transparent));
}

/* --- 24.12 Rotations connectées & verre Linear -------------------------------
   En-tête de rotation en bandeau contrasté avec badges d'escales, rail
   chronologique lumineux, pilule d'escale horodatée, et reflet spéculaire
   d'un pixel sur les surfaces principales (sombre), cartes blanc pur en clair. */
.rot-head {
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--sky) 9%, transparent), transparent 78%);
    border: 1px solid var(--card-border);
    border-radius: var(--r-sm);
    padding: var(--space-2) var(--space-3);
}
.rot-route .rot-stop {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-full);
    background: color-mix(in srgb, var(--flight) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--flight) 40%, transparent);
    color: var(--text);
    font-weight: 800;
}
.rot-node::before {
    width: 2px;
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--flight) 55%, transparent),
        color-mix(in srgb, var(--hotel) 45%, transparent));
}
.rot-dot {
    box-shadow: 0 0 0 1px var(--flight-line),
        0 0 8px -1px color-mix(in srgb, var(--flight) 70%, transparent);
}
.rot-node.is-layover .rot-dot {
    box-shadow: 0 0 0 1px var(--hotel-line),
        0 0 8px -1px color-mix(in srgb, var(--hotel) 70%, transparent);
}
.rot-hotel svg {
    width: 13px;
    height: 13px;
    flex: none;
    margin-right: var(--space-2);
    vertical-align: -2px;
    color: var(--hotel-ink);
}
.rot-hotel.is-between {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: calc(var(--space-1) + var(--space-2));
    width: fit-content;
    flex: none;
    color: var(--amber-ink);
    background: var(--amber-soft);
    border: 1px dashed color-mix(in srgb, var(--amber) 55%, transparent);
    border-radius: var(--r-full);
    padding: var(--space-1) var(--space-3);
    font-weight: 700;
}
.rot-hotel.is-between svg { margin-right: var(--space-0); }
.dash-card,
.chart-card,
.kpi,
.stat-card,
.rot,
.limit-row,
.modal,
.bento-hero .flight-strip {
    box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.09),
        0 10px 28px -8px rgba(0, 0, 0, 0.55);
}
.dash-card,
.chart-card,
.kpi,
.stat-card,
.rot,
.limit-row,
.modal {
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.025), transparent 42%);
}
[data-theme="light"] .dash-card,
[data-theme="light"] .chart-card,
[data-theme="light"] .kpi,
[data-theme="light"] .stat-card,
[data-theme="light"] .rot,
[data-theme="light"] .limit-row,
[data-theme="light"] .modal,
[data-theme="light"] .bento-hero .flight-strip {
    background-color: #fff;
    background-image: none;
    border-color: rgba(15, 23, 42, 0.08);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06),
        0 12px 32px -12px rgba(15, 23, 42, 0.18),
        inset 0 1px 0 #fff;
}
#tabs {
    background: linear-gradient(180deg,
            color-mix(in srgb, #ffffff 4%, transparent), transparent 60%),
        var(--glass);
    box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
}
.tab-cursor {
    height: 3px;
    background: linear-gradient(90deg, transparent,
        var(--sky) 30%, var(--sky) 70%, transparent);
    box-shadow: 0 0 12px color-mix(in srgb, var(--sky) 80%, transparent),
        0 0 3px var(--sky);
}
/* Onglet actif : fond verre subtil, curseur lumineux seul. Les traits
   verticaux rouges observés étaient l'outline `:focus-visible` global
   (--accent-ink) rogné en haut/bas par l'overflow du rail : aucune bordure
   latérale ici, et l'anneau de focus de l'onglet passe en cyan intérieur,
   non rognable et hors du rouge d'erreur. */
.tab-btn.active {
    background: var(--glass-tint);
    border-left: none;
    border-right: none;
}
.tab-btn:focus-visible {
    outline: 2px solid var(--sky);
    outline-offset: -2px;
}
/* La carte ESCALE & HÔTEL s'étire à la hauteur du Boarding Pass voisin :
   plus de vide sous elle, ses blocs se répartissent sur la hauteur. */
.bento-stay { display: flex; flex-direction: column; }
#dash-stay {
    flex: 1;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

/* Mission 2026-10-16 : enrichissement Pairing Details, validités, heatmap.
   Classes seules (CSP style-src self), sans !important, contrastes AA. */
.leg-metas { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); margin-left: var(--space-2); }
.leg-meta { font-size: var(--fs-meta); font-weight: 700; color: var(--text-faint);
  border: 1px solid var(--card-border); border-radius: var(--r-full); padding: 0 var(--space-2); white-space: nowrap; }
.leg-ac { color: var(--flight-ink); border-color: var(--card-border-2); }
.leg-pax { color: var(--text); }
.leg-meal { color: var(--hotel-ink); }
.leg-gate { color: var(--text-faint); }
/* The rest of the day: one compact pill per rest, code then label.

   It was a single line of text -- `sub_code + " · " + sub_label` -- over a
   label that already began with the code and carried a newline, so the portal's
   "/RH \n Repos Hebdomadaire" printed as "RH · /RH Repos Hebdomadaire" and
   wrapped onto two lines. Now the code is its own `<b>` inside the pill, the
   label is cleaned of the code it repeats, and two rests in one day both fit
   because the row wraps instead of overflowing. */
.cal-offcode { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }
/* The code alone: most cells in a month are rest days, and they carried the most
   text on them. The label is one hover away. */
.cal-restpill {
    display: inline-flex; align-items: baseline; max-width: 100%; font-size: var(--fs-3xs);
    line-height: 1.25; letter-spacing: 0.4px; color: var(--rest-ink, var(--off-ink));
    background: color-mix(in srgb, var(--rest-ink, var(--off-ink)) 9%, transparent);
    border: 1px solid color-mix(in srgb, var(--rest-ink, var(--off-ink)) 22%, transparent);
    border-radius: var(--r-full); padding: 1px var(--space-2); }
.cal-restpill > b { font-weight: 800; }
/* A day with nothing to fly is GROUND: a 135-degree hatch at 10% read as texture
   noise, a fine dot field over a 3.5% wash says "not a duty day" and prints. */
.cal-cell.cal-ground { background: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--text-faint) 15%, transparent) 1px, transparent 0) 0 0 / 7px 7px, color-mix(in srgb, var(--text-faint) 3.5%, var(--card)); }
.rest-ina { --rest-ink: var(--training-ink); }
.rest-sick { --rest-ink: var(--danger, var(--off-ink)); }
.rest-r, .rest-off { --rest-ink: var(--flight-ink); }

/* The name truncates; the count never does. They are separate elements because
   one span under an ellipsis loses whichever part happens to fall off the end,
   and "how many nights" is not the part to lose. */
/* `flex: 0 1 auto`, not `1 1 auto`. The name shrinks to what it needs and stops,
   so the count sits right against it -- "CZL 2 nuits", one caption. Letting the
   name grow sent the count to the far edge of the cell, where it read as a
   second, unrelated item rather than the end of the first one. */
.cal-hotel-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-hotel-n { flex: none; color: var(--hotel-ink); font-weight: 600; }

/* --- A stay, drawn as one thing --------------------------------------------
   The bar is the unification. Every covered day emits a segment, full-bleed
   across its own cell, and the segments meet at the 1px grid line and read as a
   single run: 12-15 October is one bar, not four cells that happen to touch.

   Three details do the work, and all three are about *not* drawing:
   - the caps are rounded ONLY where the stay begins and ends (`.lay-beg`,
     `.lay-end`). A cell at the end of a row carries neither, so the bar runs
     off the edge of the row and resumes at the start of the next one without a
     visible break -- which is why nothing in the JS needs to know where the
     rows are.
   - the wash (`.cal-cell.lay-day`) spans the whole cell, so the territory is
     continuous too, and it is the territory you read first.
   - the label rides ABOVE the bar rather than inside it. A name written on a
     5px band is unreadable at cell width, and the band's job is to be a line,
     not a label. */
.cal-cell.lay-day {
    background: color-mix(in srgb, var(--hotel-ink) 5%, var(--card));
}
/* The weekend wash would otherwise paint over the stay's, and a night in Jeddah
   should not change shade because it fell on a Saturday. */
.cal-cell.lay-day.weekend {
    background: color-mix(in srgb, var(--hotel-ink) 7%, var(--card));
}
.cal-lay {
    margin-top: auto;
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.cal-lay-label { display: flex; align-items: baseline; gap: 0.4em; min-width: 0;
    padding: var(--space-1) var(--space-2) 3px; font-size: var(--fs-meta); color: var(--text-faint); }
.cal-laybar {
    /* 3px at 42%, not 5px at 62%: measured on October 2026 the stay bars were the
       loudest marks on the page and the flights were quieter than the nights they
       explained. Full-bleed so a segment meets its neighbour across the grid line;
       the cell's own 1px border sits under it (`z-index: 1` on `.cal-lay`). */
    display: block; height: 3px; margin: 0 calc(-1 * var(--space-2));
    background: color-mix(in srgb, var(--hotel-ink) 42%, transparent);
}
.cal-laybar.lay-beg { border-radius: var(--r-full) 0 0 var(--r-full); }
.cal-laybar.lay-end { border-radius: 0 var(--r-full) var(--r-full) 0; }
.cal-laybar.lay-beg.lay-end { border-radius: var(--r-full); }
/* A boot that never finished, said so -- plus the one action that helps. */ .load-stalled { max-width: 36ch; margin: 0 auto var(--space-4); font-size: var(--fs-base); font-weight: 600; }
/* The duty FOOT: report/end left, block length right -- quietest text in a cell. */
.cal-foot { margin-top: auto; display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-1); font: var(--fs-3xs)/1.2 var(--mono); color: var(--text-faint); }
.cal-foot .cal-foot-when { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-foot .cal-foot-blk { font-weight: 700; flex: none; color: var(--text-soft); }
.cal-cell:has(.cal-foot) .cal-lay { margin-top: 0; }
/* The rotation THREAD: off the cell edge while the run goes on, half at its end. */
.cal-thread { position: absolute; left: 0; bottom: 0; height: 3px; z-index: 1; }
.cal-thread-on { width: 100%; background: linear-gradient(90deg, color-mix(in srgb, var(--flight-ink) 30%, transparent), var(--flight-ink)); }
.cal-thread-off { width: 50%; border-radius: 0 3px 3px 0; background: linear-gradient(90deg, var(--flight-ink), color-mix(in srgb, var(--flight-ink) 30%, transparent)); }
.cal-cell:has(.cal-thread) .cal-lay { margin-bottom: 3px; }
.rot-leg-sub { font-size: var(--fs-meta); color: var(--text-faint); margin-left: var(--space-2); }
.crew-pairing, .crew-fdp { font-size: var(--fs-meta); font-weight: 700; color: var(--text-faint);
  border: 1px solid var(--card-border); border-radius: var(--r-full); padding: 0 var(--space-2); margin-left: var(--space-2); }
.crew-qualif { display: block; font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0.4px;
  color: var(--training-ink); margin-top: var(--space-1); }
.hotel-metas { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); }
.hotel-meta { font-size: var(--fs-meta); color: var(--text-faint); }
.expiry-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.expiry-row { display: grid; grid-template-columns: 4.5rem 1fr auto auto; gap: var(--space-3);
  align-items: baseline; border: 1px solid var(--card-border); border-radius: var(--r-md); padding: var(--space-2) var(--space-3); }
.expiry-code { font-weight: 800; }
.expiry-desc { color: var(--text-faint); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.expiry-date { font-variant-numeric: var(--tnum); }
.expiry-when { font-weight: 700; }
.expiry-row.is-expired { border-color: #b91c1c; background: rgba(185,28,28,0.08); }
.expiry-row.is-expired .expiry-when { color: #b91c1c; }
.expiry-row.is-urgent { border-color: #c2410c; background: rgba(194,65,12,0.08); }
.expiry-row.is-urgent .expiry-when { color: #9a3412; }
.expiry-row.is-soon { border-color: #92400e; background: rgba(146,64,14,0.06); }
.expiry-row.is-soon .expiry-when { color: #92400e; }
.expiry-row.is-ok { border-color: #15803d; }
.expiry-row.is-ok .expiry-when { color: #166534; }
.stats-portal, .stats-expiries, .stats-heatmap { margin-top: var(--space-4); }
.portal-month { margin-top: var(--space-2); }
.year-heatmap { display: flex; flex-direction: column; gap: 2px; margin-top: var(--space-2); overflow-x: auto; }
.heat-row { display: grid; grid-template-columns: 2rem repeat(31, 1fr); gap: 2px; align-items: center; }
.heat-head { color: var(--text-faint); }
.heat-month { font-size: var(--fs-meta); font-weight: 700; color: var(--text-faint); }
.heat-day-n { font-size: 9px; color: var(--text-faint); text-align: center; }
.heat-cell { min-width: 14px; min-height: 14px; border-radius: 3px; border: 1px solid var(--card-border);
  background: transparent; padding: 0; cursor: pointer; }
.heat-cell.is-flight { background: #60a5fa; border-color: #60a5fa; }
.heat-cell.is-hotel { background: #34d399; border-color: #34d399; }
.heat-cell.is-standby { background: #fbbf24; border-color: #fbbf24; }
.heat-cell.is-training { background: #a78bfa; border-color: #a78bfa; }
.heat-cell.is-off { background: rgba(63,63,70,0.34); border-color: rgba(63,63,70,0.34); }
.heat-cell.is-empty { background: transparent; }
.heat-legend { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); font-size: var(--fs-meta); color: var(--text-faint); }
.heat-legend .heat-cell { cursor: default; }
.portal-profile .portal-badge { margin-left: var(--space-2); }
/* The provenance line (published until / leave taken) is a footnote to the
   identity, not a second identity: smaller, and it keeps the block's bottom
   margin from doubling now that there can be two paragraphs. */
.portal-profile-meta { margin-bottom: var(--space-3); opacity: .85; }
.portal-expiries .expiry-list { margin-top: var(--space-2); }
