/* Wings dashboard - visual identity
   Grounded in the actual subject: a business-jet MRO shop floor and its
   instrument panels, not a generic SaaS dashboard. Dark anti-glare panel
   background (shop-floor/hangar-TV friendly), an ICAO-style three-colour
   annunciator system (green/amber/red) for schedule status, and monospace
   for every numeric readout so columns of figures actually line up like a
   real instrument display. Dark is still the default and the only theme
   this panel auto-adopts - it doesn't switch to "light mode" for daytime
   use or follow OS `prefers-color-scheme`. A light variant exists only as
   a manual, opt-in preference (toggle at the bottom of the sidebar),
   persisted client-side, for whoever wants it. */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/jetbrains-mono-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

:root {
  color-scheme: dark;

  --panel-0: #10151a;   /* page background - anti-glare instrument charcoal */
  --panel-1: #1a2029;   /* card / plate surface */
  --panel-2: #212a33;   /* nested surface - gauge tracks, tiles */
  --hairline: #2a343d;  /* rivet-seam dividers */
  --border: rgba(255, 255, 255, 0.08);

  --text-primary: #eef3f1;
  --text-secondary: #93a5ac;
  --text-muted: #84939b;  /* bumped from #5b6b73 - that was ~2.6:1 on --panel-2, below WCAG AA for the 10-11px uppercase labels it's used for */

  --series-routine: #4fb3ff;  /* planned track - ADI sky blue */
  --series-nrc: #f2954f;      /* found in teardown - warm amber-orange */
  /* Blocked by another card - the fourth blocker hue on the big-screen
     board. Named rather than a one-off hex so both themes define it and it
     sits with the blues instead of fighting them. */
  --series-dependency: #8b7df0;

  --status-good: #33d17a;     /* annunciator green - on schedule */
  --status-warning: #f2b134;  /* annunciator amber - caution / provisional */
  --status-critical: #e2483d; /* annunciator red - behind schedule */
  /* The same red where it has to be READ, not just seen (design pass t436,
     17.09.2026). --status-critical is an annunciator fill: on this theme's
     --panel-2 it measures 3.62:1 and on --panel-1 4.08:1, under the 4.5:1 AA
     asks of body text - fine for a rule or a dot, not for a sentence or a
     count. This is the same hue lifted to 5.33:1 / 6.00:1. Green and amber
     need no such pair (7.3-8.7:1 already), and the light theme already
     picks its status hues as ink, so there the two are the same value. Use
     it for critical TEXT and glyphs; --status-critical stays for fills,
     rules and borders. */
  --status-critical-ink: #f4776b;

  --card-shadow: 0 1px 0 rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.03);
  --on-accent-text: #0b0f13;  /* text sat on top of a --series-routine fill (login/submit buttons) */

  /* Translucency system (the "Sunset veil / glass" rules further down) -
     both themes share the same selectors, each supplies its own fills.
     veil-* are flat alpha (free); glass-* sit under a backdrop blur and
     are reserved for small frame elements and the short blend-page list. */
  --band-fill: rgba(16, 21, 26, 0.7);        /* .page::before field around the cards */
  --veil-card: rgba(26, 32, 41, 0.9);        /* .card - flat, no blur */
  --veil-row: rgba(26, 32, 41, 0.88);        /* list rows (History) */
  --veil-row-hover: rgba(33, 42, 51, 0.95);
  --glass-fill: rgba(33, 42, 51, 0.55);      /* frame chrome: whoami bar, nav buttons */
  --glass-card: rgba(26, 32, 41, 0.62);      /* blend-page cards, project header placard */
  --glass-card-hover: rgba(33, 42, 51, 0.78);
  --glass-tile: rgba(33, 42, 51, 0.5);       /* KPI tiles on the blend page */
  --glass-edge: rgba(255, 255, 255, 0.08);
  --glass-fallback: rgba(26, 32, 41, 0.95);  /* engines without backdrop-filter */

  --font-body: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* Shape and motion tokens. --accent-brand is the one "signature" colour a
     theme may use for the active nav item, primary buttons and focus - both
     themes just point it at the planned-track blue.

     The corners were 4px here ("instrument panel") until t398 took the shell
     to floating panels: 14px frame corners with 4px plates inside them read
     as two unrelated designs sharing a screen, so cards, fields and tables
     went to 10/8 - one step under the frame, which is what the spec asked
     for. */
  --accent-brand: var(--series-routine);
  --radius-card: 10px;
  --radius-control: 8px;
  --radius-pill: 999px;
  --dur-fast: .15s;
  --dur-base: .22s;
  --ease-out: cubic-bezier(.2, .7, .2, 1);

  /* ---- The frame (t398, 15.09.2026) ----------------------------------

     The shell is three panels floating on the page background - the drawer,
     the top strip and the work area - held apart by one gap and cornered by
     one radius, so the three can never drift by a pixel. Both step down on a
     phone (see the end of this file), which is why every offset in this file
     is written against them and not against a literal 8px.

     --frame-bg is the page itself: what is left between and around the three
     panels. It is a flat colour from the theme's own palette, because the
     backdrop photo sits INSIDE the work area (--app-photo below) - the
     gutters show the page, never a strip of photograph.

     --frame-shadow is shared by all three panels, for the same reason as the
     radius: one shadow rather than three that nearly agree. Deliberately
     faint - the panels lie on the page, they do not hover over it. */
  --frame-gap: 8px;
  --frame-radius: 14px;
  --frame-bg: var(--panel-0);
  --frame-edge: rgba(255, 255, 255, 0.08);
  --frame-shadow: 0 1px 2px rgba(0, 0, 0, 0.16), 0 6px 16px rgba(0, 0, 0, 0.14);
}

/* Light variant - "Sunset Airfield" (2026-09-01), built from the user's
   style sheet: airy near-white surfaces around #F5F7FA, deep navy #1B2740
   as the primary ink, brand blue #3577E3 as the accent, and the
   sunset-apron photo behind the app shell (see body.app-bg.app-main
   below). Fill-strength colors come straight from the sheet; every hue
   that is used AS TEXT is the same hue darkened just enough to clear
   WCAG AA on panel-1/panel-2 - the sheet's raw #22C55E / #F59E0B /
   #EF4444 / #FF8A65 sit at ~2-3:1 on white, which is fine for a swatch
   or a button fill and unreadable as a 12px label. */
:root[data-theme="light"] {
  color-scheme: light;

  --panel-0: #f5f7fa;   /* page frame - the sheet's Background */
  --panel-1: #ffffff;   /* card - white plates, as in the sheet's UI components */
  --panel-2: #eef2f7;   /* nested - gauge tracks, headers, tiles */
  --hairline: #dfe5ec;
  --border: rgba(27, 39, 64, 0.12);

  --text-primary: #1b2740;   /* sheet Primary */
  --text-secondary: #475569; /* sheet body-text grey */
  --text-muted: #5b6878;     /* sheet's #64748B nudged darker - 4.5:1+ on panel-2
                                for the 10-11px uppercase labels it carries */

  --series-routine: #2e63d4; /* Accent Blue #3577E3 as ink (AA on all panels) */
  --series-nrc: #b3480d;     /* Accent Orange #FF8A65 as ink */
  --series-dependency: #5b46c9; /* dependency violet as ink */

  --status-good: #0e7a38;     /* Success #22C55E as ink */
  --status-warning: #9a4a04;  /* Warning #F59E0B as ink */
  --status-critical: #c22a20; /* Danger #EF4444 as ink */
  /* Already chosen as ink (5.12:1 on --panel-2): the pair is the same value. */
  --status-critical-ink: #c22a20;

  /* The sheet's soft lifted-card shadow, not the dark theme's flat edge. */
  --card-shadow: 0 1px 2px rgba(27, 39, 64, 0.05), 0 4px 14px rgba(27, 39, 64, 0.06);
  --on-accent-text: #ffffff;

  /* The frame (t398): the page around the panels is the sheet's one step
     below Background - #f5f7fa panels on a #f5f7fa page would have needed a
     border to be seen at all, and the spec asked for a neutral light
     grey-blue, which is exactly what --panel-2 is here. The shadow is navy
     at low alpha like every other lifted surface in this theme, never
     black. */
  --frame-bg: var(--panel-2);
  --frame-shadow: 0 1px 2px rgba(27, 39, 64, 0.06), 0 6px 16px rgba(27, 39, 64, 0.08);

  /* Translucency fills - light twins of the dark set in :root.
     --band-fill stays translucent on purpose (2026-09-07): it is the margin
     wash around the sheet, not a working surface, and the user asked to
     keep the backdrop photo visible there. Every fill that a working
     surface sits on (cards, rows, frame chrome, KPI tiles) went opaque the
     same day - a default photo must never show through data. */
  --band-fill: rgba(245, 247, 250, 0.68);
  --veil-card: #ffffff;
  --veil-row: #ffffff;
  --veil-row-hover: #eef2f7;
  --glass-fill: #ffffff;
  --glass-card: #ffffff;
  --glass-card-hover: #eef2f7;
  --glass-tile: #eef2f7;
  --glass-edge: rgba(27, 39, 64, 0.12);
  --glass-fallback: #ffffff;
}

/* .cat-chemical's accent is a one-off hex, not a shared status/series
   variable (see the cat-* rules below) - the dark-mode purple fails
   contrast directly on a light panel-0, so it gets its own light-mode
   override rather than a new shared variable for a single use site. */
:root[data-theme="light"] .material-category-header.cat-chemical { --accent: #6b46c1; }

/* The mock-data banner is intentionally decoupled from --status-warning:
   that variable's light-mode value is tuned for use as *text* elsewhere
   (lamp-caution, cat-service), which is too dark to also work as a bright
   attention-grabbing banner fill. The banner needs its own loud backdrop
   regardless of theme, so it keeps its own colors rather than inheriting
   a value optimized for a different job. */
:root[data-theme="light"] .mock-banner { background: #f2b134; color: #1a1300; }

* { box-sizing: border-box; }

/* Keyboard focus - none of the interactive elements below (nav links, tab
   buttons, refresh button, <summary> disclosure triggers) had any focus
   styling at all, so a keyboard user had no way to tell what's selected. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent-brand);
  outline-offset: 2px;
  border-radius: 2px;
}

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--panel-0);
  color: var(--text-primary);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

/* Hangar backdrop - the shop-floor photo, fixed to the viewport; the panels
   on top of it stay fully opaque (see .sidebar / .whoami-bar below), so it
   only ever shows through in the true gaps around content, never behind
   text. Each theme gets its own tonally-matched shot (dark moody hangar vs.
   a washed-out near-white version) plus a matching overlay tint, so neither
   variant is left with a photo fighting its own palette.

   Each shot is versioned in its filename (-v2) rather than overwritten:
   nothing here is cache-busted (templates call url_for('static', ...) with no
   version), so reusing a filename would leave every already-open browser -
   and the office mini-server after update.bat - showing the previous art.

   Which shot applies is carried by --app-photo rather than written straight
   onto `background-image` (t398): inside the app shell the photo is the
   WORK AREA's backdrop, not the window's - the frame's gutters are a flat
   colour and the photo is clipped by the work area's rounded corners -
   while the standalone login pages, which have no shell, still wear it
   full-bleed. One declaration per surface, one variable to pick the shot, so
   the two can never disagree about which photo this theme uses.

   Restored 21.09.2026: aff28f75 had dropped the theme's own default
   backdrop along with the personal picker it also removed, but the user's
   call was narrower - only the personal choice (dashboard/backgrounds.py,
   the 44-photo catalog and the /account picker) was meant to go; the
   default hangar/sunset shot under each theme stays. The personal photo
   override that used to sit on top of this variable with an `!important`
   is what is genuinely gone, not this rule. */
body.app-bg {
  --app-photo: linear-gradient(180deg, rgba(16, 21, 26, 0.55), rgba(16, 21, 26, 0.8)), url('images/background/gallery/hangar-bg-v2.jpg');
  --app-photo-pos: center;

  background-image: var(--app-photo);
  background-size: cover;
  background-position: var(--app-photo-pos, center);
  background-repeat: no-repeat;
  background-attachment: fixed;
}
:root[data-theme="light"] body.app-bg {
  /* Login pages (standalone documents without .app-main) keep the washed
     hangar shot - the sunset backdrop is an app-shell look by request. */
  --app-photo: linear-gradient(180deg, rgba(245, 247, 250, 0.66), rgba(245, 247, 250, 0.88)), url('images/background/gallery/hangar-jet-light.jpg');
}
/* Sunset Airfield backdrop - app shell only. Per the style sheet: the photo
   keeps its own proportions (cover, anchored right/bottom where the aircraft
   sits), and readability comes from a left-to-right overlay - near-opaque
   under the sidebar and content column, clearing toward the right so the
   jet and the mountains actually read. The residual 18% wash at the right
   edge is the sheet's "drop contrast/saturation 15-20%" step. */
:root[data-theme="light"] body.app-bg.app-main {
  --app-photo:
    linear-gradient(90deg, rgba(245, 247, 250, 0.95) 0%, rgba(245, 247, 250, 0.7) 40%, rgba(245, 247, 250, 0.3) 75%, rgba(245, 247, 250, 0.18) 100%),
    url('images/background/gallery/sunset-airfield-v1.jpg');
  --app-photo-pos: right bottom;
}
/* Inside the shell the window itself carries no photograph any more (t398):
   it is the page the three panels float on, in the theme's own frame colour.
   The photo is on .main-content .page further down, where the work area's
   rounded corners clip it. Standalone .app-bg pages (login) are untouched -
   this rule needs .app-main, and outranks the bare body.app-bg above. */
body.app-bg.app-main {
  background-image: none;
  background-color: var(--frame-bg);
}
/* ...and the work area wears it instead. Two things make this the same
   picture as before rather than a smaller copy of it:

   - `background-attachment: fixed` keeps the positioning area the viewport,
     so the shot is still sized and anchored to the window (the sunset one is
     anchored right/bottom, where the aircraft sits) and still does not move
     when the page scrolls. The work area simply shows the part of it that
     falls inside its own box.
   - an element's own background is always clipped by its border-radius, so
     the corners come free - no `overflow: hidden` anywhere near
     .main-content, which is the one thing this frame must not do (see the
     note on .main-content's overflow-x). */
body.app-bg.app-main .main-content .page {
  background-image: var(--app-photo);
  background-size: cover;
  background-position: var(--app-photo-pos, center);
  background-repeat: no-repeat;
  background-attachment: fixed;
}

.page {
  max-width: 1280px;
  margin: 0 auto;
  padding: 24px;
}

a { color: var(--series-routine); }

/* ---- Mock-data banner - shown on every page (see templates/_mock_banner.html)
   whenever WINGS_MOCK=1, so synthetic demo data is never mistaken for a live
   Wings status by anyone glancing at a screen. Deliberately loud (amber,
   full-width) - the one place this app breaks its own "no glow, no pill
   fill" restraint (style.css intro), because a wrong reading here is worse
   than an ugly banner. ---- */
.mock-banner {
  background: var(--status-warning);
  color: #1a1300;
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  padding: 6px 10px;
  flex: none;
}

/* ---- Header placard ---- */
.header-widget {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 20px;
  padding: 18px 22px;
  background: var(--panel-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--series-routine);
  border-radius: var(--radius-card);
  box-shadow: var(--card-shadow);
  margin-bottom: 20px;
}
.header-widget h1 {
  font-family: var(--font-mono);
  font-size: 21px;
  font-weight: 700;
  letter-spacing: 0.01em;
  margin: 0;
}
.header-widget .meta { color: var(--text-secondary); font-size: 13px; }
.header-widget .lamp { margin-left: auto; }

/* ---- Annunciator lamp - the quiet status device used everywhere a
   badge/pill used to be (schedule state, record status). Deliberately
   plain: a coloured dot + uppercase mono label, no glow, no pill fill -
   the tape gauge below is the one bold element on this page. ---- */
.lamp {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
}
.lamp-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); flex: none; }
.lamp-good { color: var(--status-good); }
.lamp-good .lamp-dot { background: var(--status-good); }
.lamp-caution { color: var(--status-warning); }
.lamp-caution .lamp-dot { background: var(--status-warning); }
.lamp-critical { color: var(--status-critical); }
.lamp-critical .lamp-dot { background: var(--status-critical); }
.lamp-neutral { color: var(--text-secondary); }
.lamp-neutral .lamp-dot { background: var(--text-muted); }

/* ---- Grid / cards ---- */
.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 16px;
}
.span-6 { grid-column: span 6; }
.span-4 { grid-column: span 4; }
.span-8 { grid-column: span 8; }
.span-12 { grid-column: span 12; }
@media (max-width: 900px) {
  .span-6, .span-4, .span-8 { grid-column: span 12; }
}

.card {
  background: var(--panel-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 16px 18px;
  box-shadow: var(--card-shadow);
}
.card h2 {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 0 0 14px 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--hairline);
  font-weight: 600;
}
.card .footnote { font-size: 11px; color: var(--text-muted); margin-top: 10px; }

/* ---- Hint tooltip component (static/app.js initHints()) - replaces
   footnotes and gives per-row context on hover/focus without an extra
   round-trip. Two variants sharing one popup mechanism:
   .hint       - a quiet "i" icon next to a heading, explains a static concept
   .hint-text  - dotted-underline inline text, explains per-row data ---- */
.hint {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 6px; border-radius: 50%;
  border: 1px solid var(--text-muted); color: var(--text-muted);
  font-size: 10px; font-style: normal; line-height: 1; cursor: help;
  vertical-align: middle;
}
.hint:hover, .hint:focus-visible { color: var(--text-primary); border-color: var(--text-primary); }
.hint-text {
  border-bottom: 1px dotted var(--text-muted); cursor: help;
}
/* ---- Toast (static/toast.js) - the app's one transient message ----
   It replaced three per-module copies, two of which sat at the BOTTOM of the
   screen in 13px grey. That is how "record the result first" went unread
   every time someone ticked a task done in the list: the answer arrived
   below the fold, in the colour of a footnote. So: top of the viewport,
   under the identity bar (toast.js measures it), 15px, and errors carry the
   critical colour instead of looking like every other notice. ---- */
.ui-toast {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(-8px);
  max-width: min(460px, calc(100vw - 24px)); box-sizing: border-box;
  padding: 13px 18px; border-radius: var(--radius-card);
  background: var(--panel-2); border: 1px solid var(--border); color: var(--text-primary);
  font-size: 15px; line-height: 1.4; text-align: center; overflow-wrap: anywhere;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .35);
  z-index: 300; cursor: pointer;
  opacity: 0; transition: opacity .16s ease, transform .16s ease;
}
.ui-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.ui-toast--error {
  border-color: var(--status-critical);
  background: color-mix(in srgb, var(--status-critical) 14%, var(--panel-2));
  font-weight: 600;
}
.ui-toast--error::before {
  content: "!"; display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-right: 9px; border-radius: 50%; vertical-align: -3px;
  /* The glyph takes the panel colour, not white: a theme's critical colour
     can be a pale ink, where white on it is barely a mark. */
  background: var(--status-critical); color: var(--panel-2); font-size: 12px; font-weight: 700;
}
.ui-toast--ok { border-color: color-mix(in srgb, var(--status-good) 55%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .ui-toast { transition: none; transform: translateX(-50%); }
  .ui-toast.show { transform: translateX(-50%); }
}

.hint-popup {
  position: fixed; z-index: 200; max-width: 280px;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-control);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  padding: 8px 10px; font-size: 11.5px; line-height: 1.4; color: var(--text-secondary);
  pointer-events: none;
}

/* ---- Active Labor multi-card disclosure (static/app.js) - clicking an
   employee's Multi-card cell reveals their other simultaneously open
   cards as sub-rows right below. Click instead of hover (unlike .hint
   above) since the TV view has no pointer to hover with. Same disclosure
   arrow as .card-drilldown for visual consistency. ---- */
.multi-card-toggle {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: var(--text-primary); border-bottom: 1px dotted var(--text-muted);
  display: inline-flex; align-items: center; gap: 4px;
}
.multi-card-toggle:hover, .multi-card-toggle:focus-visible { color: var(--series-routine); }
.multi-card-toggle::after { content: "▸"; color: var(--text-muted); display: inline-block; transition: transform 0.15s ease; }
.multi-card-toggle[aria-expanded="true"]::after { transform: rotate(90deg); }
.labor-subrow td { color: var(--text-secondary); background: var(--panel-2); }
.labor-subrow-indent { text-align: center; color: var(--text-muted); }

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.cache-note { font-family: var(--font-mono); color: var(--text-secondary); font-size: 12px; }

.refresh-btn {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--text-secondary);
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 3px;
  padding: 4px 10px; cursor: pointer;
}
.refresh-btn:hover { color: var(--text-primary); border-color: var(--series-routine); }
.refresh-btn:disabled { cursor: default; opacity: 0.6; }

.back-btn {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--text-secondary);
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 3px;
  padding: 4px 10px; text-decoration: none; order: -1;
}
.back-btn:hover { color: var(--text-primary); border-color: var(--series-routine); }

/* ---- Card drill-down (Widget 2) - expand a forecast group to see its
   individual open cards, closed count alongside for context. Native
   <details>/<summary> - no JS needed for something this simple. ---- */
.card-drilldown { margin-top: 14px; border-top: 1px solid var(--hairline); padding-top: 10px; }
.card-drilldown summary {
  cursor: pointer; font-size: 12px; color: var(--text-secondary);
  list-style: none; display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap;
  border-radius: var(--radius-control); margin: 0 -6px; padding: 2px 6px;
  transition: background-color 0.15s ease, color 0.15s ease;
  overflow-wrap: break-word;
  word-break: break-word;
}
.card-drilldown summary::-webkit-details-marker { display: none; }
/* The ▸ marker is the only visual cue that a row is expandable - kept at
   full text-secondary weight (not the fainter text-muted) plus a bold
   weight of its own so it doesn't read as decoration, and a hover/focus
   state on the whole row (background tint, arrow + text switch to the
   series-routine accent) makes "you can click here" obvious before the
   click, not just after. */
.card-drilldown summary::before {
  content: "▸"; color: var(--text-secondary); font-weight: 700;
  display: inline-block; transition: transform 0.15s ease, color 0.15s ease;
}
.card-drilldown summary:hover, .card-drilldown summary:focus-visible {
  background: var(--panel-2); color: var(--text-primary);
}
.card-drilldown summary:hover::before, .card-drilldown summary:focus-visible::before {
  color: var(--series-routine);
}
.card-drilldown[open] summary::before { transform: rotate(90deg); }
.card-drilldown summary b { color: var(--text-primary); font-family: var(--font-mono); }
.card-drilldown table { margin-top: 10px; }
.card-drilldown .blocker-note { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }

/* Widget 2's open-cards list can show as one flat table (badge per row) or
   split into a per-category accordion (Material/Tool/Child Cards/Not
   Started/In Progress, each its own drill-down) - the checkbox flips which
   of the two pre-rendered blocks is visible. Pure CSS (sibling combinator),
   no JS, same "no JS needed for something this simple" reasoning as the
   details/summary disclosure above - the checkbox is a sibling of both
   view blocks (not nested inside the label) so this selector reaches them. */
.cards-grouped-view { display: none; }
.cards-view-toggle:checked ~ .cards-grouped-view { display: block; }
.cards-view-toggle:checked ~ .cards-flat-view { display: none; }
.category-summary-row {
  display: flex; align-items: center; justify-content: space-between; flex: 1; font-size: 12.5px;
}
.category-summary-row .count { font-variant-numeric: tabular-nums; font-family: var(--font-mono); color: var(--text-secondary); }

/* ---- "Why this number?" explain disclosure (_widget_overview.html) - a
   quieter cousin of .card-drilldown (smaller text, no bold summary term)
   since this is prose, not a table of records to scan. ---- */
.explain-drilldown summary { font-size: 11.5px; }
.explain-body { margin-top: 8px; }
.explain-body p { margin: 0 0 6px; font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
.explain-body p:last-child { margin-bottom: 0; }
.explain-body p.footnote { color: var(--text-muted); font-style: italic; }
.explain-scenarios { width: 100%; border-collapse: collapse; margin: 4px 0 8px; }
.explain-scenarios td {
  padding: 5px 0; font-size: 12px; color: var(--text-secondary); line-height: 1.4;
  border-bottom: 1px dotted var(--hairline); vertical-align: baseline;
}
.explain-scenarios tr:last-child td { border-bottom: none; }
.explain-scenarios .scenario-date {
  font-family: var(--font-mono); color: var(--text-primary); font-weight: 600;
  white-space: nowrap; text-align: right; padding-left: 12px; width: 1%;
}
.explain-scenarios .scenario-headline td { color: var(--text-primary); }
.explain-scenarios .scenario-tag {
  display: inline-block; margin-left: 6px; padding: 0 5px; border-radius: 3px;
  font-size: 10px; letter-spacing: 0.02em; color: var(--text-muted);
  border: 1px solid var(--hairline); white-space: nowrap;
}

.project-forecast-summary {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 14px;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-control);
}
.project-forecast-summary .project-forecast-label {
  font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;
}
.project-forecast-summary .project-forecast-date {
  font-family: var(--font-mono); font-size: 19px; font-weight: 700; margin-left: auto;
}

.forecast-cards { display: flex; gap: 16px; flex-wrap: wrap; }
.forecast-card { flex: 1; min-width: 260px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 14px; }
.forecast-card.routine { border-top: 3px solid var(--series-routine); }
.forecast-card.nrc { border-top: 3px solid var(--series-nrc); }
.forecast-card-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.forecast-card-head { align-items: center; margin-bottom: 8px; }
.forecast-card .group-name {
  font-family: var(--font-mono); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 12px; color: var(--text-secondary);
}
.forecast-card .big-date, .forecast-card .big-pct { font-family: var(--font-mono); font-size: 25px; font-weight: 700; margin: 8px 0 4px; white-space: nowrap; }
.forecast-card .counts { color: var(--text-secondary); font-size: 12.5px; margin-bottom: 12px; }
.forecast-card .counts b { color: var(--text-primary); font-family: var(--font-mono); }

.sparkline { display: block; margin-top: 10px; }
.sparkline-hit { cursor: help; }
.sparkline-hit:focus-visible { outline: 1px solid var(--text-primary); outline-offset: 1px; }

/* Not four equal columns. Cards Closed carries a 92px ring plus a legend of
   five label/count pairs; Closed Yesterday carries one number. Equal quarters
   gave the ring's legend about 115px, which is narrower than the word
   "PERFORMED" - so the label overflowed its own cell and printed on top of
   the count. Weighting the columns is what makes the legend fit inline. */
.kpi-row { display: grid; grid-template-columns: 1.5fr 1fr 1.25fr 1.25fr; gap: 12px; }
.kpi-tile { background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 12px; text-align: left; }
.kpi-tile .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.kpi-tile .value { font-family: var(--font-mono); font-size: 22px; font-weight: 700; }
.kpi-tile .sub { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
/* The dashboard's one completion ring (templates/_macros_gauges.html). Its
   geometry and every colour are arguments to the macro, on purpose - only
   what CSS is actually better at lives here: not shrinking inside a flex
   row, and the sweep when a percentage changes under a live update. Global
   rather than per-screen because the same macro draws the KPI tile, the two
   rings on a task page and the closeout widget. */
.gauge-ring { flex: none; }
.gauge-ring circle { transition: stroke-dasharray var(--dur-base) var(--ease-out); }
/* Wraps rather than crushes: below about 250px of tile the legend drops
   under the ring instead of being squeezed into a column too narrow for the
   word "PERFORMED" - which is what put the labels underneath the numbers
   ("PE55ORME") on the hangar tablet. */
.kpi-tile-donut-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.kpi-tile-donut-row svg { flex: none; }
.kpi-tile-wide { grid-column: span 2; }
/* Cards Closed and Routine Pace sit in the same grid row as the taller
   Hours Worked/Plan tile (which carries an open breakdown table), so the
   grid's default row-stretch already gives them extra height - center
   their content in it instead of leaving it stranded at the top. */
.kpi-tile-donut, .kpi-tile-pace, .kpi-tile-yesterday, .kpi-tile-today { display: flex; flex-direction: column; justify-content: center; }
.kpi-tile-donut .value { font-size: 30px; }
.kpi-tile-donut .sub { font-size: 13px; }

/* ---- Cards Closed legend - the numbers beside the completion ring. A
   two-column <dl> so the counts right-align into a column that reads as a
   sum: the dotted rows below Total always add up to it. Labels reuse the
   .lamp dot device rather than inventing a second coloured-dot style, and
   each dot's colour is the same token as its arc in the ring. ---- */
.donut-legend {
  display: grid;
  /* minmax(0, 1fr), not auto: an `auto` label column cannot shrink below its
     longest word, so at tablet width the label overflowed its own cell and
     printed underneath the count. The label column now yields, the counts
     keep their natural width, and nothing overlaps at any width. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px 12px;
  margin: 0;
  /* No grow. With minmax(0, 1fr) above, a legend allowed to fill a wide tile
     hands the entire surplus to the label column: at a 617px tile the labels
     took 452px and the counts ended up stranded against the far edge, a gap
     wider than the table itself. Sized to content the pairs stay together,
     and the basis is still "PERFORMED plus its dot plus the counts", so the
     legend drops below the ring exactly when it no longer fits beside it. */
  flex: 0 1 auto;
  font-family: var(--font-mono);
  font-size: 13px;
}
/* Last-resort safeguard under ~140px of legend: wrap the word rather than
   clip it. A clipped label is silently wrong; a wrapped one is only ugly. */
.donut-legend dt { min-width: 0; overflow-wrap: anywhere; }
.donut-legend dt .lamp { min-width: 0; white-space: normal; }
.donut-legend dd {
  margin: 0;
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
/* Total is the sum, not one of the parts - bigger, undotted, and ruled off
   from the rows that add up to it. */
.donut-legend .donut-legend-total {
  font-size: 17px;
  font-weight: 700;
  padding-bottom: 4px;
  margin-bottom: 2px;
  border-bottom: 1px solid var(--border);
}
/* Closed, the tile is a quarter of the row; open, its four paragraphs of
   explanation would wrap into a ~200px column and make the tile ~450px tall.
   Widen it to two columns while open - the row already reflows because
   Hours Worked/Plan carries an open table, so nothing else has to move. */
details.kpi-tile-donut[open] { grid-column: span 2; }
details.kpi-tile-donut[open] .kpi-tile-breakdown { max-width: 46em; }
details.kpi-tile-donut[open] .kpi-tile-breakdown p { margin: 0 0 8px; }

.donut-legend dt.donut-legend-total {
  color: var(--text-secondary);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 12px;
}
.kpi-tile-pace .value { font-size: 44px; }
.kpi-tile-pace .sub { font-size: 13px; }
/* One number, same weight as the pace figure it replaced. The date under it
   is the label of that number, not a footnote - a count with no day attached
   is the sort of thing that gets read a week later as "today". */
.kpi-tile-yesterday .value { font-size: 44px; }
.kpi-tile-yesterday .sub { font-family: var(--font-mono); font-size: 13px; white-space: nowrap; }
/* Today's tile is deliberately the same shape as yesterday's - same size,
   same split underneath - so the pair reads as one comparison rather than
   two unrelated numbers. The only difference is the accent border: this is
   the number that is still moving, and the one people look at during the
   day (production feedback item 14). */
.kpi-tile-today .value { font-size: 44px; }
.kpi-tile-today .sub { font-family: var(--font-mono); font-size: 13px; white-space: nowrap; }
.kpi-tile-today { border-left: 3px solid var(--series-routine); padding-left: 13px; }
/* The ROUTINE/NONROUTINE split under the total - the same two-column,
   right-aligned pairs as the Cards Closed legend, so the two tiles read as
   one language. Ruled off: these add up to the number above them. */
.yesterday-split {
  display: grid;
  /* Content-sized, like the Cards Closed legend beside it. `minmax(0, 1fr)`
     alone hands the whole tile to the label column and strands the counts
     against the far edge - the same defect fixed in the legend, and the two
     tiles are supposed to read as one language. The rule above the pairs
     still spans the tile: the divider is full width, the pairs are not. */
  grid-template-columns: minmax(0, max-content) max-content;
  justify-content: start;
  gap: 2px 10px;
  margin: 10px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-secondary);
}
.yesterday-split dt { min-width: 0; overflow-wrap: anywhere; }
.yesterday-split dd {
  margin: 0; text-align: right;
  font-variant-numeric: tabular-nums; color: var(--text-primary);
}
/* Yesterday's closures under the rolling rate. Ruled off and set smaller
   than the pace: it is a second, different number in a tile whose whole
   point is the one big figure, so it has to read as a footnote to it rather
   than compete with it at a glance from across the hangar. */
/* Same label / number / sub rhythm as the tile above it, stacked rather than
   set in columns: this is the narrow tile in the row, and a label-left,
   count-right line broke apart the moment the date wrapped under it. */
.pace-yesterday { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.pace-yesterday-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted);
}
/* nowrap: without it the date breaks after the month ("2026-08-" / "31"),
   which reads as two numbers rather than one date. */
.pace-yesterday-date { font-family: var(--font-mono); text-transform: none; letter-spacing: 0; white-space: nowrap; }
.pace-yesterday-count {
  font-family: var(--font-mono); font-size: 22px; font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1.2;
}
.pace-yesterday-split { font-family: var(--font-mono); font-size: 11.5px; color: var(--text-secondary); }
/* Two implicit equal-width columns so Worked/Plan center inside their
   own half instead of pinning to the tile's outer edges. */
.hwp-value-row { display: grid; grid-template-columns: 1fr 1fr; align-items: center; }
.hwp-stat { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.hwp-stat .hwp-number { font-family: var(--font-mono); font-size: 22px; font-weight: 700; }
.hwp-stat .hwp-tag {
  font-size: 10.5px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted);
}
/* ---- Under the forecast date: what the number assumes. The headline is a
   best case, so it is labelled, and the realistic trailing-window date sits
   beside it - printed alone, a best case is how a plan gets built on the one
   day everything went right. ---- */
.forecast-basis {
  display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline;
  margin-top: 2px; font-size: 11.5px; line-height: 1.35;
}
.forecast-basis-label { color: var(--text-secondary); }

/* Tablet. Four equal columns give the ring's legend and the Hours Worked
   table about 180px each, which is where both stopped being readable. The
   shop floor reads Cards Closed and Hours Worked; Closed Yesterday is a
   single number and can afford to be the narrow one - so at tablet widths
   the row is two columns weighted 1.6 : 1, and Hours Worked takes the full
   width below with its breakdown table intact. */
@media (min-width: 701px) and (max-width: 1200px) {
  .kpi-row { grid-template-columns: 1.6fr 1fr; }
  .kpi-tile-wide, details.kpi-tile-donut[open] { grid-column: 1 / -1; }
}
@media (max-width: 700px) {
  .kpi-row { grid-template-columns: repeat(2, 1fr); }
  /* One number per tile at phone width - the legend beside a 92px ring in
     half a phone is the same squeeze as the tablet, one breakpoint down.
     The [open] selector is listed too: it carries the same grid-column at a
     higher specificity, so without it an opened Cards Closed tile keeps half
     the row here and its explanation wraps into ~150px. */
  .kpi-tile-donut, details.kpi-tile-donut[open], .kpi-tile-wide { grid-column: 1 / -1; }
}

/* ---- Fleet summary strip (project_list.html) - reuses .kpi-tile, just
   tints the value to match the same lamp-good/lamp-critical semantics used
   everywhere else instead of inventing a third color language. ---- */
.fleet-summary-card { margin-bottom: 20px; }
.kpi-tile-good .value { color: var(--status-good); }
.kpi-tile-critical .value { color: var(--status-critical); }

.kpi-tile-expandable summary { cursor: pointer; list-style: none; }
.kpi-tile-expandable summary::-webkit-details-marker { display: none; }
.kpi-tile-breakdown { margin-top: 4px; }
.kpi-tile-breakdown table { font-size: 12px; }
.kpi-tile-breakdown th, .kpi-tile-breakdown td { padding: 5px 4px; }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid var(--hairline); }
/* Contrast, 06.09.2026 - same pass as static/invoice.css, and this is the
   rule it applies to most screens at once: a column header names the data
   under it, so it reads as content, not as a decorative caption. */
th { color: var(--text-secondary); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-family: var(--font-mono); }
td.num, th.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: none; }

/* Grouped column headers (t181, 15.09.2026): a second <tr> above a wide
   table's real header, one <th colspan> per group naming the columns under
   it (money together, dates together, etc). Centered and non-wrapping so
   the group label reads as a span across several columns rather than as a
   left-aligned caption that could wrap to a second line above an already
   two-row thead - shared across invoice/hangar/workforce, one rule instead
   of a copy per stylesheet. */
.thgroup { text-align: center; white-space: nowrap; }

/* ---- Tape gauge - THE signature element. Modelled on a PFD tape
   instrument (airspeed/altitude tape): a tick-marked track with a boxed
   digital readout riding at the current value, instead of a generic
   filled pill bar. Rendered server-side by app.py's tape_gauge() helper. ---- */
.tape { position: relative; padding-top: 22px; min-width: 90px; }
.tape-track {
  position: relative; height: 12px;
  background: var(--panel-0); border: 1px solid var(--hairline); border-radius: 2px;
  overflow: hidden;
}
.tape-fill { position: absolute; inset: 0 auto 0 0; background: var(--tape-color, var(--series-routine)); }
.tape-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(0, 0, 0, 0.4); }
.tape-flag {
  position: absolute; top: 0; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  background: var(--panel-0); color: var(--tape-color, var(--series-routine));
  border: 1px solid var(--tape-color, var(--series-routine));
  border-radius: 3px; padding: 1px 5px; line-height: 1.5; white-space: nowrap;
}
.tape-flag::after {
  content: ""; position: absolute; left: 50%; bottom: -5px; transform: translateX(-50%);
  width: 1px; height: 5px; background: var(--tape-color, var(--series-routine));
}

.priority-aog, .over-hours { color: var(--status-critical); font-weight: 700; }

.blocker-bars { display: flex; flex-direction: column; gap: 2px; }
.blocker-row { display: grid; grid-template-columns: 120px 1fr 40px; align-items: center; gap: 10px; font-size: 12.5px; flex: 1; }
.blocker-row .bar { height: 12px; border-radius: 2px; background: var(--panel-0); border: 1px solid var(--hairline); position: relative; overflow: hidden; }
.blocker-row .bar > span { position: absolute; inset: 0; border-radius: 2px; }
.blocker-row .count { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); color: var(--text-secondary); }

/* Each blocker category is itself a drill-down - click a row to see the
   specific cards behind that count, same disclosure pattern as Widget 2's
   .card-drilldown. */
.blocker-drilldown summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px;
  padding: 6px 6px; margin: 0 -6px; border-radius: var(--radius-control);
  transition: background-color 0.15s ease;
}
.blocker-drilldown summary::-webkit-details-marker { display: none; }
/* Same "make the clickability obvious before the click" treatment as
   .card-drilldown above - full-weight arrow, hover/focus highlight. */
.blocker-drilldown summary::before {
  content: "▸"; color: var(--text-secondary); font-weight: 700;
  flex: none; transition: transform 0.15s ease, color 0.15s ease;
}
.blocker-drilldown summary:hover, .blocker-drilldown summary:focus-visible { background: var(--panel-2); }
.blocker-drilldown summary:hover::before, .blocker-drilldown summary:focus-visible::before {
  color: var(--series-routine);
}
.blocker-drilldown[open] summary::before { transform: rotate(90deg); }
.blocker-drilldown .drilldown-body { margin: 0 0 10px 22px; }
.blocker-drilldown .drilldown-body table { font-size: 12px; }

.material-group-toggle-label {
  display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-secondary);
  margin-bottom: 8px; cursor: pointer; user-select: none;
}
/* Each material category gets its own accent color (left rail + text),
   not just a shared muted divider - the whole point is that CHEMICAL,
   CONSUMABLE, ROTABLE etc. shouldn't blend into one grey band when you're
   scanning the table quickly. */
/* table-layout: fixed, one width per named column (see the th classes in
   _widget_materials.html) - Card/Part get the room, everything else is a
   short fixed value, so Status and (especially) Due can never be pushed
   past the panel's right edge by a long detail cell. Category/Price are
   conditional columns; when absent the browser just scales the remaining
   percentages up to fill the row, which is fine - they are relative
   weights, not literal pixels. */
/* table-layout: fixed with a PX width per short, fixed-vocabulary column
   (group, PO, qty, price, status, due) - a percentage of the panel isn't
   enough room for "AWAITING"/"PENDING 2026-09-15" once the panel is a
   realistic width, and wrapping them mid-word or letting them run into the
   next cell both read as a rendering bug, not a status. Card/Part/Category
   (the free-text columns) take the width left over - fixed layout gives
   any column without an explicit width an equal share of the remainder, so
   they simply get more room when the conditional Category/Price columns
   are absent. */
.material-table { table-layout: fixed; }
.material-table th { white-space: nowrap; }
.material-table td.col-group, .material-table td.col-po, .material-table td.col-qty,
.material-table td.col-price, .material-table td.col-status, .material-table td.col-due { white-space: nowrap; }
.material-table td.col-card, .material-table td.col-part, .material-table td.col-category { overflow-wrap: break-word; }
.material-table .col-group { width: 66px; }
.material-table .col-category { width: 96px; }
.material-table .col-po { width: 80px; }
.material-table .col-qty { width: 44px; }
.material-table .col-price { width: 76px; }
.material-table .col-status { width: 92px; }
.material-table .col-due { width: 84px; }
.material-category-header td {
  font-family: var(--font-mono); font-size: 12.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.1em; text-align: center;
  /* A full-width tint, not just the left rule below - at 9 columns wide the
     rule alone read as unpainted by the time the eye reached Status/Due. */
  background: color-mix(in srgb, var(--accent, var(--text-muted)) 10%, var(--panel-0));
  padding: 8px; border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
  border-left: 4px solid var(--accent, var(--text-muted));
  color: var(--accent, var(--text-secondary));
}
.material-category-header.cat-rotable { --accent: var(--series-routine); }
.material-category-header.cat-consumable { --accent: var(--series-nrc); }
.material-category-header.cat-chemical { --accent: #a78bfa; }
.material-category-header.cat-tool { --accent: var(--status-good); }
.material-category-header.cat-service { --accent: var(--status-warning); }
.material-category-header.cat-other { --accent: var(--text-muted); }
/* CREATED means the requisition only exists inside Wings - it hasn't
   actually been placed with a vendor yet, so it's invisible to whoever
   tracks incoming parts until someone notices it here and pushes it
   forward. Darkened row + red warning icon so it can't be skimmed past. */
.material-row-created { background: var(--panel-0); }
.status-created { color: var(--status-critical); font-weight: 700; }
/* AOG (aircraft-on-ground) requisitions - the highest-priority material
   blocker there is - get the same darkened-row treatment as CREATED above,
   plus the inline priority-aog badge next to the part number. */
.material-row-aog { background: var(--panel-0); }
/* Who owns the part (dashboard/material_owner.py): the customer's own part
   is never priced, so its row gets a grey left rule; a unit taken off
   another aircraft ("donor") a violet rule and badge. Both are facts about
   the line rather than problems, hence no red. The filter checkboxes above
   the table hide/show rows by their data-owner-kind (static/app.js). */
.material-row-customer td:first-child { box-shadow: inset 3px 0 0 var(--text-muted); }
.material-row-donor td:first-child { box-shadow: inset 3px 0 0 #7c5cd6; }
.material-owner-tag { display: inline-block; font-size: 11.5px; color: var(--text-secondary); padding: 0 5px;
  border: 1px solid var(--hairline); border-radius: 9px; white-space: nowrap; vertical-align: 1px; }
.material-owner-tag.is-donor { color: #7c5cd6; border-color: #7c5cd6; }
.material-owner-code { font-family: var(--font-mono); font-size: 11.5px; }
/* A repair trip with a cost column nobody filled in (calc.repair_summary):
   the total is shown as far as it goes, in red, so the gap is visible
   before it reaches an invoice. */
.material-repair { font-size: 11px; color: var(--text-secondary); }
.material-repair.is-incomplete { color: var(--status-critical); }
.material-filters { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 6px; font-size: 12px; color: var(--text-muted); }
.material-filters label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
/* Requisition positions (Widget 5 drill-down). A requisition here can
   carry anything from 1 to 60+ lines, so the card row stays a one-line
   summary and the positions open underneath it on demand rather than
   turning the blocker table into a wall of parts. */
.material-lines-toggle {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 4px; padding: 1px 6px 1px 4px;
  font-family: inherit; font-size: 11px; color: var(--text-secondary);
  background: var(--panel-0); border: 1px solid var(--hairline); border-radius: 10px; cursor: pointer;
}
.material-lines-toggle:hover { color: var(--text-primary); border-color: var(--text-muted); }
.material-lines-caret { font-size: 9px; line-height: 1; }
.material-lines-row > td { padding: 0 0 0 24px; background: var(--panel-0); }
.material-lines-table { width: 100%; font-size: 11.5px; margin: 2px 0 6px 0; }
.material-lines-table th {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary);
  font-weight: 600; padding: 4px 8px;
}
.material-lines-table td { padding: 3px 8px; border-bottom: 1px solid var(--hairline); }
.material-lines-table tfoot td {
  border-bottom: none; border-top: 1px solid var(--hairline);
  font-weight: 700; color: var(--text-secondary);
}
.material-line-desc { color: var(--text-secondary); margin-left: 8px; }
/* The price is always inferred - from a quote, or from what the part cost
   last time it was bought - so the source rides along with the number in
   muted small caps instead of leaving a bare figure that reads as fact. */
.material-price-source { font-size: 11.5px; color: var(--text-secondary); font-weight: 400; }
.material-price-date { margin-left: 4px; font-family: var(--font-mono); }

.empty-state { color: var(--text-secondary); font-size: 13px; padding: 8px 0; }

.tail-group-header {
  font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-muted); font-weight: 700; margin: 22px 0 8px 0;
}
.tail-group-header:first-of-type { margin-top: 12px; }

.project-list { list-style: none; padding: 0; margin: 0 0 4px 0; display: grid; gap: 10px; }
.project-list a {
  display: block; background: var(--panel-1); border: 1px solid var(--border);
  border-left: 3px solid var(--hairline);
  border-radius: var(--radius-control); padding: 14px 16px; text-decoration: none; color: var(--text-primary);
  box-shadow: var(--card-shadow);
  transition: border-left-color 0.15s ease;
}
.project-list a:hover { border-left-color: var(--series-routine); }
.project-list .pnum { font-family: var(--font-mono); font-weight: 700; }
.project-list .pmeta { color: var(--text-secondary); font-size: 12.5px; margin-top: 2px; }

/* ---- /history's invoice-amount column (templates/history.html) ---- */
/* The row holds two links side by side - the project and, for whoever may
   see invoices, its invoice (t137). The second one cannot live inside the
   first, so the <li> is the flex row and both links are its children. */
.project-list.with-invoice li { display: flex; align-items: stretch; gap: 8px; }
.project-list.with-invoice li > a:first-child { flex: 1 1 auto; min-width: 0; }
.project-list.with-invoice a { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.project-list.with-invoice .pinfo { min-width: 0; }
.project-list .pinvoice-link {
  flex: 0 0 auto; justify-content: center; white-space: nowrap;
  font-size: 13px; color: var(--text-secondary);
}
.project-list .pinvoice-link:hover { color: var(--text-primary); }
.project-list .pinvoice { flex: 0 0 auto; text-align: right; white-space: nowrap; }
.project-list .pinvoice-total { font-family: var(--font-mono); font-weight: 700; font-size: 14px; }
.project-list .pinvoice-none { color: var(--text-secondary); }

.diagnostic-tag {
  font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); border: 1px solid var(--hairline); border-radius: 3px; padding: 1px 6px; margin-left: 6px;
}

.labor-forecast-row { display: flex; gap: 24px; flex-wrap: wrap; align-items: stretch; }
.cpi-tile { background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 14px 20px; text-align: center; min-width: 180px; }
.cpi-tile .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.cpi-tile .value { font-family: var(--font-mono); font-size: 32px; font-weight: 700; }
.cpi-tile .sub { font-size: 11px; color: var(--text-secondary); margin-top: 4px; max-width: 180px; }
.cpi-good { color: var(--status-good); }
.cpi-warning { color: var(--status-warning); }
.cpi-critical { color: var(--status-critical); }

/* ---- CPI gauge + percent-vs-estimate readout - a second, faster-to-read
   view of the same CPI value as the big number above (see cpi-tile),
   added because the raw ratio alone reads as ambiguous/backwards to
   anyone expecting textbook EVM's CPI (where >1 is good, not over). ---- */
.cpi-gauge { margin-top: 10px; cursor: help; }
.cpi-gauge-track { position: relative; height: 8px; border-radius: 2px; overflow: hidden; }
.cpi-gauge-marker {
  position: absolute; top: -2px; bottom: -2px; width: 2px;
  background: var(--text-primary); transform: translateX(-1px);
}
.cpi-gauge-scale {
  display: flex; justify-content: space-between; font-family: var(--font-mono);
  font-size: 9px; color: var(--text-muted); margin-top: 3px;
}
.cpi-delta { font-family: var(--font-mono); font-size: 13px; font-weight: 700; margin-top: 8px; cursor: help; }
.labor-forecast-details { flex: 1; min-width: 260px; }
.labor-forecast-details .big-date { font-family: var(--font-mono); font-size: 22px; font-weight: 700; margin: 6px 0 4px; }

/* Applied/earned/remaining/adjusted/burn-rate as a row of labeled stats
   (same number-over-label shape as .kpi-tile) instead of one run-on
   sentence - that sentence line-wrapped mid-clause on real widths and
   left the space below it empty. */
.labor-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: 14px 18px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--hairline);
}
.labor-stat-value { font-family: var(--font-mono); font-size: 17px; font-weight: 700; color: var(--text-primary); }
.labor-stat-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-top: 3px; }

/* ---- Crew output on this aircraft (dashboard.html Labor tab, test builds
   only). The card is built from /workforce/shifts' own .sh-* classes
   (workforce.css) so the two screens read as one feature; only this period
   switcher is local - that page renders periods as links in its toolbar. ---- */
.crew-output-controls { display: flex; align-items: center; gap: 6px; margin-bottom: 14px; }
.crew-period {
  font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary);
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 3px;
  padding: 3px 10px; cursor: pointer;
}
.crew-period:hover { color: var(--text-primary); }
.crew-period.active { color: var(--text-primary); border-color: var(--text-muted); }
.crew-open-all { margin-left: auto; font-size: 12px; color: var(--text-secondary); text-decoration: none; }
.crew-open-all:hover { color: var(--text-primary); }
/* ---- Pricing widget (templates/_widget_pricing.html) - same tile/detail
   shape as the CPI row above, kept as its own classes rather than reusing
   .cpi-tile/.labor-forecast-* so this doesn't read as labor-specific. ---- */
.pricing-row { display: flex; gap: 24px; flex-wrap: wrap; align-items: stretch; }
.pricing-tile { background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 14px 20px; text-align: center; min-width: 160px; }
.pricing-tile .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.pricing-tile .value { font-family: var(--font-mono); font-size: 32px; font-weight: 700; }
/* flex-direction: column (revision 13, money design pass 15.09.2026): this
   was a single-line row, `align-items: center` on the cross axis only. A
   second line - _widget_pricing's "Wings priced this invoice without
   billing groups" under the labor/materials/other counts, for the no-
   billing-groups case - became a flex item too and sat beside the first
   line instead of under it, on one squeezed row. `.footnote` below (the
   pulled-at line, pre-existing in _widget_invoice.html) had the same
   defect already, just never a two-line case that made it visible. */
.pricing-details { flex: 1; min-width: 200px; display: flex; flex-direction: column;
  justify-content: center; align-items: flex-start; gap: 3px; }
.pricing-details .counts { color: var(--text-secondary); font-size: 12.5px; }
.pricing-details .counts b { color: var(--text-primary); font-family: var(--font-mono); }

/* ---- TV / mobile portfolio overview ---- */
.tv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 20px; }
.tv-card {
  display: block; color: inherit; text-decoration: none;
  background: var(--panel-1); border: 1px solid var(--border); border-left: 3px solid var(--hairline);
  border-radius: var(--radius-control); padding: 18px 20px; box-shadow: var(--card-shadow);
  transition: border-left-color 0.15s ease;
}
.tv-card:hover { border-left-color: var(--series-routine); }
.tv-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 6px; }
.tv-tail { font-family: var(--font-mono); font-size: 19px; font-weight: 700; letter-spacing: 0.02em; }
.tv-meta { color: var(--text-secondary); font-size: 12.5px; margin-bottom: 14px; }
.tv-forecast-row { margin-top: 10px; font-size: 12.5px; color: var(--text-secondary); }
.tv-forecast-row .mono { font-family: var(--font-mono); color: var(--text-primary); }

/* Large-viewport (TV) scale-up - same markup, bigger everything for
   across-the-room legibility instead of a separate mobile-sized layout. */
@media (min-width: 1400px) {
  body.tv-mode { font-size: 20px; }
  body.tv-mode .page { max-width: 1800px; padding: 40px; }
  /* auto-fit (not the base rule's auto-fill): with only a handful of open
     aircraft, empty tracks collapse and the real cards stretch to fill the
     row instead of sitting small on the left of a big screen. */
  body.tv-mode .tv-grid { gap: 24px; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
  body.tv-mode .tv-tail { font-size: 32px; }
  body.tv-mode .tv-meta { font-size: 16px; }
  body.tv-mode .tv-forecast-row { font-size: 18px; }
  body.tv-mode .lamp { font-size: 15px; }
  body.tv-mode .lamp-dot { width: 10px; height: 10px; }
  body.tv-mode .tape { padding-top: 30px; }
  body.tv-mode .tape-track { height: 18px; }
  body.tv-mode .tape-flag { font-size: 15px; padding: 3px 8px; }

  /* Widget-level scale-up for the single-page /tv/project/<id> view - same
     markup/partials as the leads dashboard, just bigger so a dense one-scroll
     page stays legible at TV viewing distance instead of shrinking to fit. */
  body.tv-mode .header-widget h1 { font-size: 30px; }
  body.tv-mode .header-widget .meta { font-size: 16px; }
  body.tv-mode .card { padding: 24px 28px; }
  body.tv-mode .card h2 { font-size: 18px; }
  body.tv-mode .card .footnote { font-size: 13px; }
  body.tv-mode table { font-size: 16px; }
  body.tv-mode th { font-size: 12.5px; }
  body.tv-mode th, body.tv-mode td { padding: 12px 10px; }
  body.tv-mode .forecast-card .group-name { font-size: 15px; }
  body.tv-mode .forecast-card .big-date, body.tv-mode .forecast-card .big-pct { font-size: 30px; }
  body.tv-mode .forecast-card .counts { font-size: 15px; }
  body.tv-mode .kpi-tile .label { font-size: 13px; }
  body.tv-mode .kpi-tile .value { font-size: 28px; }
  body.tv-mode .kpi-tile .sub { font-size: 14px; }
  body.tv-mode .cpi-tile .label { font-size: 13px; }
  body.tv-mode .cpi-tile .value { font-size: 40px; }
  body.tv-mode .cpi-tile .sub { font-size: 13px; }
  body.tv-mode .labor-forecast-details .big-date { font-size: 28px; }
  body.tv-mode .labor-forecast-details .counts { font-size: 15px; }
  body.tv-mode .blocker-row { font-size: 15px; grid-template-columns: 160px 1fr 50px; }
  body.tv-mode .blocker-row .bar { height: 16px; }
  body.tv-mode .card-drilldown summary,
  body.tv-mode .blocker-drilldown summary { font-size: 15px; }
  body.tv-mode .empty-state { font-size: 16px; }
}

/* ---- TV single-project view: fit-to-screen layout ----
   Unlike body.tv-mode above (a tall scrolling page, scaled up for legibility),
   this is a fixed-height dashboard meant to show everything at a glance on a
   TV with no scrolling of the page itself. That goal trades against large
   type - the 7 widget cards are packed into a 4-column grid sized to
   viewport height. Any one card whose data outgrows its cell (a long supply
   or active-labor list) scrolls internally instead of pushing the page. */
body.tv-fit {
  height: 100vh;
  overflow: hidden;
  font-size: 13.5px;
}
/* Fills the viewport at any screen size instead of a fixed 1920x1080 box
   scaled by JS. Two things make that work: the grid below fills the width
   with `fr` columns and the height with `fr` rows, and every font here is
   sized off --u, one knob tied to viewport HEIGHT (the binding axis for a
   landscape dashboard). So a 4K panel shows the same layout with larger,
   still-proportional type - and no letterbox bars on a non-16:9 screen,
   which the old transform: scale left behind. --u ~= the old 11.5px base at
   1080p (1.05vh), clamped so a small dev window stays legible and a very
   tall panel doesn't turn cartoonish. (There is no @media enlargement for
   tv-fit - the removed transform was the only thing scaling this view, so
   --u now carries it alone.) */
.tv-fit-page {
  width: 100%;
  height: 100vh;
  margin: 0;
  padding: 1vh 1vw;
  display: flex;
  flex-direction: column;
  gap: 0.9vh;
  --u: clamp(9px, 1.05vh, 24px);
  font-size: var(--u);
}
.tv-fit-topbar { flex: none; font-size: 1.04em; color: var(--text-secondary); }
.tv-fit-header {
  flex: none;
  margin-bottom: 0;
  padding: 0.9vh 0.95vw;
}
.tv-fit-header h1 { font-size: 1.65em; }
.tv-fit-header .meta { font-size: 1.04em; }

/* ---- Visit stage strip (dashboard/visit_stage.py, tv_project.html) ----
   Sized in em, not px: this strip is a direct child of .tv-fit-page, which
   sets font-size: var(--u), so 1em here IS the one viewport-height knob the
   rest of this view scales by. Left in px it stayed 21/13px while every
   number around it grew on a 1440p+ panel - the verdict would have become
   the smallest thing on the line it is meant to lead. The 1px hairline
   border stays px on purpose; only the left accent scales.
   The verdict on the left at a size that reads across a desk, its evidence
   as chips beside it. Chips are colored by tone, not by category: on this
   line "bad" means "this is why the stage is what it is", and that is the
   only distinction worth a color here. Single row, no wrap - a visit with
   enough problems to overflow it would push the grid, and the grid is the
   fixed-height part of this page. */
.stage-strip {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 0.9em;
  padding: 0.7em 1.6em;
  border-radius: 0.7em;
  border: 1px solid var(--border);
  border-left-width: 0.45em;
  background: var(--panel-1);
  overflow: hidden;
  white-space: nowrap;
}
.stage-strip .stage-name {
  font-size: 1.85em;
  font-weight: 700;
  letter-spacing: 0.01em;
  flex: none;
}
.stage-strip .stage-fact {
  font-size: 1.15em;
  color: var(--text-secondary);
  flex: none;
}
.stage-strip .stage-fact + .stage-fact::before {
  content: "·";
  /* 0.8em of the fact's own 1.15em, i.e. ~0.9 of --u - matching the
     strip's gap rather than reading wider than it. */
  margin-right: 0.8em;
  color: var(--text-muted);
}
.stage-strip .tone-bad { color: var(--status-critical); font-weight: 600; }
.stage-strip .tone-good { color: var(--status-good); }
.stage-critical { border-left-color: var(--status-critical); }
.stage-critical .stage-name { color: var(--status-critical); }
.stage-warning { border-left-color: var(--status-warning); }
.stage-warning .stage-name { color: var(--status-warning); }
.stage-good { border-left-color: var(--status-good); }
.stage-good .stage-name { color: var(--status-good); }
.stage-neutral { border-left-color: var(--text-muted); }

.tv-fit-grid {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1.35fr 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 10px;
}
.tv-fit-grid > .card {
  min-height: 0;
  min-width: 0;
  overflow-y: auto;
  padding: 10px 14px;
}
/* Deterministic DOM order from tv_project.html's three includes: forecast,
   KPI, active labor (_widget_overview.html - personnel is merged in there
   too, Killer Card folded into Active Labor's own Over column) - blockers
   (_widget_materials.html) - CPI (_widget_labor.html). 5 cards, grouped by
   theme into the 4x2 grid: col1 forecast alone (tall); col4 KPI+CPI (compact
   stat panels, narrow); active labor and blockers each get a double-width
   column (cols 2-3) since both are wide, variable-length tables - active
   labor especially, now that Killer Card's Accumulated/Over columns live
   there too. */
.tv-fit-grid > .card:nth-child(1) { grid-column: 1; grid-row: 1 / span 2; }
.tv-fit-grid > .card:nth-child(2) { grid-column: 4; grid-row: 1; }
.tv-fit-grid > .card:nth-child(3) { grid-column: 2 / 4; grid-row: 1; }
.tv-fit-grid > .card:nth-child(4) { grid-column: 2 / 4; grid-row: 2; }
.tv-fit-grid > .card:nth-child(5) { grid-column: 4; grid-row: 2; }

.tv-fit-grid .card h2 { font-size: 1em; margin-bottom: 8px; padding-bottom: 6px; }
.tv-fit-grid .card .footnote { display: none; }
.tv-fit-grid table { font-size: 1em; table-layout: fixed; width: 100%; }
.tv-fit-grid th, .tv-fit-grid td { padding: 5px 4px; overflow-wrap: break-word; }

/* Active Labor table (_widget_personnel.html) at tv-fit's fixed-height,
   fixed-width columns: with every column the same width, the free-text
   Description column wrapped to 4-5 lines and blew out row height,
   defeating the "read the whole board at a glance" point of this view. Give
   it most of the row's width and clamp to 2 lines instead of letting it
   wrap freely. Columns: Employee, Card, Description, Approved, Accumulated,
   Over, Logged in since, Multi-card. */
.tv-fit-grid #labor-widget table th:nth-child(1), .tv-fit-grid #labor-widget table td:nth-child(1) { width: 15%; }
.tv-fit-grid #labor-widget table th:nth-child(2), .tv-fit-grid #labor-widget table td:nth-child(2) { width: 9%; }
.tv-fit-grid #labor-widget table th:nth-child(3), .tv-fit-grid #labor-widget table td:nth-child(3) { width: 26%; }
.tv-fit-grid #labor-widget table th:nth-child(4), .tv-fit-grid #labor-widget table td:nth-child(4) { width: 8%; }
.tv-fit-grid #labor-widget table th:nth-child(5), .tv-fit-grid #labor-widget table td:nth-child(5) { width: 9%; }
.tv-fit-grid #labor-widget table th:nth-child(6), .tv-fit-grid #labor-widget table td:nth-child(6) { width: 8%; }
.tv-fit-grid #labor-widget table th:nth-child(7), .tv-fit-grid #labor-widget table td:nth-child(7) { width: 13%; }
.tv-fit-grid #labor-widget table th:nth-child(8), .tv-fit-grid #labor-widget table td:nth-child(8) { width: 12%; }

.tv-fit-grid #labor-widget table td:nth-child(3) {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* t181, 15.09.2026: .labor-table further down this file makes every
   non-Description cell `white-space: nowrap` so a card id or a two-word
   header never breaks at a desk width - see that rule's comment. This
   fixed-percentage TV grid was already tuned to wrap/break-word within its
   narrow columns (the block above), so nowrap here would just push text
   past the edge of a 8-9%-wide column instead. Restore normal wrapping for
   this one context; higher specificity than the generic rule so it wins
   regardless of the two rules' order in the file. */
.tv-fit-grid .labor-table th, .tv-fit-grid .labor-table td { white-space: normal; }
.tv-fit-grid .kpi-row { grid-template-columns: repeat(2, 1fr); gap: 8px; }
.tv-fit-grid .kpi-tile { padding: 8px 10px; }
.tv-fit-grid .kpi-tile .value { font-size: 1.57em; }
.tv-fit-grid .kpi-tile .label { font-size: 0.87em; }
.tv-fit-grid .kpi-tile .sub { font-size: 0.91em; }
/* Fill the forecast column's height. It spans both grid rows (tall), but its
   content is often shorter than that - which used to leave a black void below
   it. Make the card a flex column so the two forecast panels stretch to fill
   the column; a panel whose open-cards list overflows scrolls inside itself
   rather than pushing the page. So the column always reads as two full panels,
   not two short panels over emptiness. */
.tv-fit-grid > .card:nth-child(1) { display: flex; flex-direction: column; }
.tv-fit-grid > .card:nth-child(1) > .forecast-cards { flex: 1; min-height: 0; }
.tv-fit-grid .forecast-cards { gap: 8px; align-items: stretch; }
.tv-fit-grid .forecast-card { padding: 10px; min-width: 0; overflow-y: auto; }
.tv-fit-grid .forecast-card .big-date, .tv-fit-grid .forecast-card .big-pct { font-size: 1.48em; margin: 6px 0 3px; }
.tv-fit-grid .sparkline { margin-top: 4px; }
.tv-fit-grid .cpi-tile { padding: 10px 14px; min-width: 100px; }
.tv-fit-grid .cpi-tile .value { font-size: 2.09em; }
.tv-fit-grid .labor-forecast-row { gap: 12px; }
.tv-fit-grid .labor-forecast-details .big-date { font-size: 1.48em; }
.tv-fit-grid .blocker-row { font-size: 1em; grid-template-columns: 7.6em 1fr 2.4em; gap: 6px; }
.tv-fit-grid .blocker-row .bar { height: 9px; }
.tv-fit-grid .tape { min-width: 0; padding-top: 18px; }
.tv-fit-grid .tape-flag { font-size: 0.91em; padding: 1px 5px; }

/* ---- App shell: persistent sidebar (project_list/dashboard/history only -
   /tv and the customer view are separate standalone pages by design).

   Redesigned 2026-09-07 to a single graphite panel, identical in both
   themes - only the page behind it (cards, tables) still follows the
   active theme. The colors below are the panel's own tokens, never the
   shared --panel-* / --text-* set, precisely so a theme switch can never
   repaint it, and they are declared on .sidebar itself (not :root) so
   nothing outside the panel can be affected by a name collision. ---- */
/* NB (t398, 15.09.2026): the line above used to run the two token names
   together with a slash between them, which closes a CSS comment three lines
   early. Everything after it was parsed as a selector, and the first rule of
   this block - this one - was dropped by every browser: the
   shell has not actually been a flex container, nor 100vh tall, since the
   comment was written. It matters now, because a work area that floats has to
   reach the bottom of the window even on a short page. (The same typo two
   rules apart was swallowing .sidebar-theme-list, the theme popup in the
   drawer - see the end of this file.) */
.app-shell { display: flex; min-height: 100vh; }
/* ...and the shell is 100vh of the room LEFT, not 100vh flat: the mock-data
   banner (templates/_mock_banner.html) stands above it in the same column, so
   a flat 100vh made every page in a mock build 27px taller than its window
   and put a scrollbar on it. Scoped to .app-main - the login pages are
   standalone documents that centre their own card in the viewport. */
body.app-main { display: flex; flex-direction: column; min-height: 100vh; }
body.app-main .app-shell { flex: 1 1 auto; min-height: 0; }
.sidebar {
  --sb-bg: #172630;
  --sb-text: #e5edf2;
  --sb-muted: #a5b7c4;
  --sb-icon: #c4d2dc;
  --sb-active-bg: #30434f;
  --sb-active-text: #ffffff;
  --sb-hover-bg: #223641;
  --sb-border: #30414d;
  --sb-badge-bg: #101d26;
  --sb-badge-text: #dfe8ee;
  --sb-focus: #43bdf2;
  /* One step under the frame's own 14px, like every other plate inside a
     panel (--radius-card). 4px until t398. */
  --sb-radius: 10px;
  /* The panel's own hairline highlight - white at low alpha, because this
     plate is graphite in all three themes, so a themed --border would go
     invisible in two of them. */
  --sb-edge: rgba(255, 255, 255, 0.08);

  width: 260px; flex: none; background: var(--sb-bg); color: var(--sb-text);
  /* overflow: hidden, not auto - the panel itself no longer scrolls as one
     piece (07.09.2026, user's own call: the header and footer were
     scrolling away with a long open group). .sidebar-scroll below is the
     one part that does. */
  display: flex; flex-direction: column; overflow: hidden;
  padding: 10px 0 18px;
  /* A floating panel since t398: a gap all the way round it instead of the
     old edge-to-edge column with a border down its right side.

     The hairline is an INSET shadow, not a border: a real 1px border would
     make the panel 262px wide and move everything measured against its 260
     (the docked page's margin, the strip at the bottom, the ✕ nav.js places
     at x=207). Same reason the shadow carries it - one box, no new pixels. */
  position: sticky; top: var(--frame-gap);
  height: calc(100vh - var(--frame-gap) * 2);
  border-radius: var(--frame-radius);
  box-shadow: inset 0 0 0 1px var(--sb-edge), var(--frame-shadow);
}
/* The one signature-blue focus ring, regardless of which accent the active
   theme otherwise uses (per the design spec) - everywhere else in the app
   keeps following --accent-brand. */
.sidebar :focus-visible { outline-color: var(--sb-focus); }

/* The full lockup was removed 07.09.2026 for taking too much room, and it
   stays removed. What sits here instead (12.09.2026) is the monogram alone
   at 26px - small enough to leave the corner to the ✕ and the build badge,
   large enough to be the thing you look at to know which window this is.
   nav.js prepends the ✕ close button (.sidebar-close) into this same
   element - it is absolutely positioned, so it does not push the mark or the
   badge around. */
.sidebar-header {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  flex: none; padding: 8px 12px; border-bottom: 1px solid var(--sb-border); margin-bottom: 12px;
}
/* A column, not a row. In a row the mark and the build badge together are
   234px wide inside a 260px panel, and the ✕ nav.js puts in the top-right
   corner is absolutely positioned from x=207 - so the badge ran a clear 27px
   underneath it. Measured, not guessed. Stacked, the mark keeps the top line
   to itself and the badge gets the one below, clear of the ✕ in both. */
.sidebar-brand {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  min-width: 0; color: var(--sb-text); text-decoration: none;
}
/* Height, not width: the mark is 3.9:1, so setting a width would make its
   height depend on how the crop was taken. 40px was chosen by measuring
   against the ✕ nav.js puts at x=207, not by eye - at 40 the mark ends at
   168 and leaves 39px of air, at 46 it ends at 191 and the jet's nose all
   but touches the button. The user asked for "немного увеличить" after the
   first pass at 26px, and this is that step with room still left.
   The panel is one graphite plate in all three themes (--sb-* above), so
   there is no second file for a light background and nothing to switch. */
.sidebar-brand .brand-mark { display: block; height: 40px; width: auto; flex: none; }
/* Moved from an inline <style> in _base.html when the logo above it went
   away - same look, just no longer duplicated per test-mode render. */
.brand-test {
  display: inline-block; padding: 2px 6px; border-radius: 3px;
  background: var(--status-warning, #875c04); color: var(--on-accent-text, #fff);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .12em;
}

/* The one part of the panel that scrolls (07.09.2026) - the header above and
   the footer further down are flex: none, so neither one moves regardless
   of how far a long open group or the aircraft list is scrolled here. */
/* overflow-x: hidden - without it, an explicit overflow-y forces overflow-x
   to auto too (the CSS spec's overflow-x/-y coupling), which drew a sideways
   scrollbar along the bottom whenever anything inside was even a pixel
   wider than the panel. Nothing in here actually needs to scroll sideways.

   Scrollbar itself only shows on hover (both engines: WebKit's own
   ::-webkit-scrollbar-thumb, and Firefox's scrollbar-color) - a track that
   is always visible reads as a stray line down the panel when the list
   fits without scrolling, which it usually does with every group closed. */
.sidebar-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column;
  scrollbar-color: transparent transparent;
}
.sidebar-scroll::-webkit-scrollbar { width: 8px; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: transparent; border-radius: var(--sb-radius); }
.sidebar-scroll:hover { scrollbar-color: var(--sb-border) transparent; }
.sidebar-scroll:hover::-webkit-scrollbar-thumb { background: var(--sb-border); }

.sidebar-nav { display: flex; flex-direction: column; padding: 0 10px; margin-bottom: 16px; }
.sidebar-nav a, .sidebar-project {
  display: flex; align-items: center; gap: 12px; min-height: 40px; padding: 8px 10px; border-radius: var(--sb-radius);
  color: var(--sb-muted); text-decoration: none; font-size: 16.5px; font-weight: 400; white-space: nowrap;
}
.sidebar-nav a:hover, .sidebar-project:hover { background: var(--sb-hover-bg); color: var(--sb-text); }
.sidebar-nav a.active, .sidebar-project.active {
  background: var(--sb-active-bg); color: var(--sb-active-text); font-weight: 600;
}
.sidebar-nav a .nav-ico, .sidebar-project .nav-ico { color: var(--sb-icon); width: 20px; height: 20px; }
.sidebar-nav a:hover .nav-ico, .sidebar-project:hover .nav-ico,
.sidebar-nav a.active .nav-ico, .sidebar-project.active .nav-ico { color: var(--sb-active-text); }
.sidebar-section-label {
  font-family: var(--font-body); font-size: 16.5px; font-weight: 500;
  color: var(--sb-muted); padding: 0 16px; margin-bottom: 6px;
}
/* A collapsible group inside the nav list - the whole menu is groups now
   (Production / Workforce / My Account / Admin). Built on <details>/<summary>,
   so open/close, keyboard and screen-reader behaviour are the browser's and
   not ours. */
.sidebar-group { margin-top: 18px; display: flex; flex-direction: column; }
.sidebar-group:first-child { margin-top: 0; }   /* the first one meets the header rule */
.sidebar-group-label {
  display: flex; align-items: center; gap: 12px; min-height: 40px; padding: 8px 10px; border-radius: var(--sb-radius);
  cursor: pointer; list-style: none;              /* kill the default marker... */
  font-family: var(--font-body); font-size: 16.5px; font-weight: 500;
  color: var(--sb-muted); user-select: none;
}
.sidebar-group-label::-webkit-details-marker { display: none; }   /* ...in Safari too */
/* The caret sits at the right edge (the left slot now belongs to the group's
   icon), so a collapsed group reads as "icon · name · ›" like the mockup. */
.sidebar-group-label::after {
  content: "›"; font-family: var(--font-body); font-size: 15px; line-height: 1;
  margin-left: auto; transition: transform .15s ease;
}
.sidebar-group[open] > .sidebar-group-label::after { transform: rotate(90deg); }
.sidebar-group-label:hover { color: var(--sb-text); background: var(--sb-hover-bg); }
.sidebar-group-label .nav-ico { color: var(--sb-icon); }
/* Links inside the group sit one step in, so the group reads as a container
   rather than as more top-level items. */
.sidebar-group > a { padding-left: 22px; }
@media (prefers-reduced-motion: reduce) { .sidebar-group-label::after { transition: none; } }

/* ---- Sidebar icons - one inline <symbol> sprite per page
   (templates/_nav_icons.html), referenced with <use>, so each icon ships
   once no matter how often it appears. Stroke inherits currentColor: muted
   with the label at rest, white on hover/active. ---- */
.nav-ico {
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.sidebar-group-label .nav-ico,
.sidebar-section-label .nav-ico { width: 14px; height: 14px; }
.sidebar-section-label { display: flex; align-items: center; gap: 8px; }
.sidebar-logout { display: flex; align-items: center; gap: 12px; }

/* No longer flex:1/overflow-y of its own (07.09.2026) - .sidebar-scroll,
   wrapping this together with the nav groups above it, is the one region
   that scrolls now, so the aircraft list just flows inside that. */
.sidebar-projects { display: flex; flex-direction: column; padding: 0 10px; gap: 1px; margin-bottom: 12px; }
.sidebar-project-tail { font-family: var(--font-mono); font-weight: 600; }

/* A tail with more than one open project (normally shouldn't happen, but
   the sidebar has to cope if it does) collapses to a toggle instead of a
   direct link - expands to show that tail's actual open projects. */
.sidebar-tail-toggle {
  display: flex; align-items: center; gap: 12px; width: 100%; min-height: 40px; padding: 8px 10px; border-radius: var(--sb-radius);
  background: none; border: none; color: var(--sb-muted); font-size: 16.5px; font-family: var(--font-body);
  cursor: pointer; text-align: left;
}
.sidebar-tail-toggle:hover { background: var(--sb-hover-bg); color: var(--sb-text); }
.sidebar-tail-toggle.active { background: var(--sb-active-bg); color: var(--sb-active-text); font-weight: 600; }
.sidebar-tail-count {
  font-family: var(--font-mono); font-size: 12px; color: var(--sb-badge-text); background: var(--sb-badge-bg);
  border-radius: 8px; padding: 0 6px; margin-left: auto;
}
.sidebar-tail-caret { font-size: 10px; color: var(--sb-muted); transition: transform 0.15s ease; }
.sidebar-tail-toggle.open .sidebar-tail-caret { transform: rotate(90deg); }
.sidebar-tail-projects { display: flex; flex-direction: column; gap: 1px; padding-left: 16px; }
.sidebar-project-sub { font-size: 12px; padding: 6px 8px; }

/* Fixed at the bottom (07.09.2026) - flex:none, a sibling of .sidebar-scroll
   rather than its last item, so it never scrolls out of view with the menu
   above it. The on-track/behind/unknown lamp legend used to live here too;
   removed on the user's own call. */
.sidebar-footer { flex: none; }

/* The revision number - last in the footer, readable when looked for, never
   competing with the menu itself (moved here from just under the aircraft
   list 07.09.2026, on the user's own call). */
.sidebar-revision {
  padding: 10px 16px 0; font-size: 11px; color: var(--sb-muted);
  letter-spacing: 0.02em; font-variant-numeric: tabular-nums;
}

.sidebar-logout {
  padding: 10px 16px; font-size: 12.5px; color: var(--sb-muted); text-decoration: none;
  border-top: 1px solid var(--sb-border);
}
.sidebar-logout:hover { color: var(--sb-text); }
/* "Log out" is a POST <form> with a <button> since 10.09.2026 (a GET logout
   can be fired from any foreign page). The form is invisible layout-wise and
   the button dresses as the link it replaced. */
.logout-form { display: contents; }
button.sidebar-logout, button.account-menu-logout, button.logout-link {
  background: none; border: 0; font: inherit; cursor: pointer; text-align: left;
  width: 100%; box-sizing: border-box; color: inherit;
}
button.sidebar-logout { width: 100%; color: var(--sb-muted); font-size: 12.5px; }
button.sidebar-logout:hover { color: var(--sb-text); }
button.logout-link { width: auto; padding: 0; color: var(--accent-brand); text-decoration: underline; }

.sidebar-theme-toggle {
  display: flex; align-items: center; gap: 12px; width: 100%; margin: 0;
  padding: 10px 16px; background: none; border: none; border-top: 1px solid var(--sb-border);
  color: var(--sb-muted); font-family: var(--font-body); font-size: 13px; cursor: pointer; text-align: left;
  text-decoration: none; box-sizing: border-box;
}
.sidebar-theme-toggle:hover { background: var(--sb-hover-bg); color: var(--sb-text); }
.sidebar-theme-toggle.active { color: var(--sb-active-text); background: var(--sb-active-bg); }
.sidebar-theme-toggle svg, .sidebar-theme-toggle .nav-ico { width: 16px; height: 16px; flex: none; }
.sidebar-theme-toggle svg { fill: currentColor; }
.sidebar-theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] .sidebar-theme-toggle .icon-moon { display: none; }
:root[data-theme="light"] .sidebar-theme-toggle .icon-sun { display: block; }

/* Profile row, bottom of the panel (2026-09-07) - avatar-initials, name and
   role, opening the account area (account_profile). The account menu at the
   top right of every page keeps doing what it always did; this is a second,
   always-visible door to the same room, not a replacement for it. */
.sidebar-profile {
  display: flex; align-items: center; gap: 12px; padding: 10px 16px;
  color: var(--sb-text); text-decoration: none; border-top: 1px solid var(--sb-border);
}
.sidebar-profile:hover { background: var(--sb-hover-bg); }
.sidebar-profile-avatar {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 28px; height: 28px; border-radius: 50%; background: var(--sb-active-bg); color: var(--sb-active-text);
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 600;
}
.sidebar-profile-info { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.sidebar-profile-name { font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-profile-role { font-size: 11.5px; color: var(--sb-muted); }

.main-content {
  flex: 1; min-width: 0;
  /* `.page::before` used to bleed 160px past each edge so the content band
     dissolved into the hangar photo instead of ending on a hard line. That
     bleed used to hide under the 220px sidebar; once the sidebar became a
     drawer and the page took the full width, the right-hand 160px became
     160px of sideways scroll - the page could be dragged off-screen on a
     tablet. The band stops at the work area's own edge since t398 (the work
     area IS a panel now, with a deliberate rounded edge - there is nothing
     left to dissolve into), but this stays: it is also what keeps a wide
     table's overflow from turning the window into a scroller.

     `clip`, not `hidden`: `hidden` would make this a scroll container and
     change how the sticky "Signed in as" bar behaves. `clip` just cuts the
     bleed off and creates no scrollport. And NOT `overflow: clip` on both
     axes either - the top strip inside is sticky, and this box must never
     become the thing it sticks inside. Corners are clipped by the panels'
     own border-radius instead, which needs no overflow at all.

     The two margins are the frame (t398): the gap to the window on the right
     and at the bottom, and on the left either the same gap or - with the
     drawer docked - the drawer's width plus two gaps (the @media block
     further down). The top gap is on the strip itself, so that a page can
     scroll under it. */
  overflow-x: clip;
  display: flex; flex-direction: column;
  margin: 0 var(--frame-gap) var(--frame-gap) var(--frame-gap);
}
/* Every page takes the whole width of the window. It used to be a 1100px
   reading measure, which on the 27-34" screens this is actually used on left
   a third of the glass empty on every screen except the two that had opted
   into `page--wide` - and the pages that suffered most were exactly the ones
   made of tables (People, Employees, Users, Invoices, History), where the
   missing pixels are columns, not decoration.

   Content that genuinely wants a measure sets its own: the error card caps
   itself at 520px, the account grid flows into columns of its own. */
.main-content .page { max-width: none; position: relative; z-index: 0; }
/* The third panel (t398). `flex: 1` is what makes it a panel rather than a
   plate the size of its text: .app-shell is at least 100vh and .main-content
   stretches to it, so the work area fills whatever is left under the top
   strip even on a page with three lines on it.

   The rounded corners clip the panel's own background colour (above) - its
   border-radius is what makes the eye read that fill as "the page".

   `margin: 0` undoes the `margin: 0 auto` .page carries for the pages that
   are not in the shell (the login screens): an auto margin on a flex item
   swallows the free space instead of stretching into it, so with it the work
   area came out as wide as its widest paragraph, centred in the window.

   `box-shadow` was missing here until a design pass on t398 caught it
   (15-16.09.2026): the drawer and the top strip both carry
   `inset 0 0 0 1px var(--frame-edge), var(--frame-shadow)`, but nobody had
   written the same pair onto the third panel, so it sat flat against the
   page while the other two visibly floated - failing the spec's own "same
   shadow on all three panels" acceptance line. */
.main-content .page { flex: 1 1 auto; margin: 0; border-radius: var(--frame-radius); box-shadow: inset 0 0 0 1px var(--frame-edge), var(--frame-shadow); }
/* Each page eases in rather than appearing in one frame. Short and on the
   content only: the sidebar and the chrome are already in place before the
   first paint (see _nav_init.html and the group script in _base.html), so
   they must not move - only the thing that actually changed. */
@keyframes page-in { from { opacity: 0; } to { opacity: 1; } }
.main-content .page { animation: page-in .22s ease-out both; }
@media (prefers-reduced-motion: reduce) { .main-content .page { animation: none; } }

/* Wide, Excel-like pages (Staff Schedule, Planned Works, the task views):
   full width is now the default, so all this still carries is the tighter
   gutter - a month grid wants every pixel, where an ordinary page keeps the
   24px it has always had. A page opts in with
   {% block page_class %}page--wide{% endblock %}.

   The `.page::before` backdrop band needs no change - its 160px bleed falls
   in the strip `.main-content { overflow-x: clip }` cuts off, so the band
   reads edge to edge with no seam and no sideways scroll. */
.main-content .page.page--wide { padding: 12px 16px; }

/* Setup cards that used to sit one under the other down the left edge of a
   page whose real content is the table below them (Employees, Users). Side
   by side they cost one screenful less scrolling before the table starts,
   and they stop being a narrow column with a window's worth of nothing
   beside it. Falls back to one column under ~320px per card. */
.card-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; align-items: start; margin-bottom: 18px; }
.card-row > .card { margin-bottom: 0; }

/* A short form laid across the page instead of down it: label-above-field
   pairs flow into as many columns as fit, the submit lines up with the last
   row. Used where the form is the small half of a page whose other half is
   a full-width table (Users). */
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px 18px; align-items: end; }
.form-row > * { min-width: 0; }

/* "Signed in as NAME" - always visible, sticky so it survives scrolling on
   long pages. The one guard a person has, on a shared device, against acting
   under someone else's still-open session. */
/* Same reasoning and the same graphite as the sidebar (2026-09-07): the top
   strip is chrome, not a page surface, so it keeps one look in every theme
   instead of following --panel-2 from light to dark.
   Own tokens (--tb-*) rather than the sidebar's --sb-* because this element
   is not a descendant of .sidebar - same values, so the two bars still read
   as one continuous frame around the page. */
.whoami-bar {
  --tb-bg: #172630;
  --tb-text: #e5edf2;
  --tb-muted: #a5b7c4;
  --tb-border: #30414d;
  --tb-hover: #223641;

  /* The frame's own hairline highlight, for the same reason .sidebar carries
     one: this strip is graphite in every theme. */
  --tb-edge: rgba(255, 255, 255, 0.08);

  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  /* A floating panel since t398: the frame's gap above it and below it, the
     frame's radius, the frame's shadow - the same three values the drawer
     and the work area use. Left and right need nothing of their own: the
     strip is as wide as .main-content, which already holds the gap to the
     drawer and to the right edge of the window.

     `top: 0` and not `top: var(--frame-gap)`, deliberately. The strip stays
     INSIDE .main-content (it always has - making it a fourth child of
     .app-shell would mean giving up `sticky` for `fixed` and measuring its
     height in script, and its height varies: the task views wrap onto a
     second line under 900px). Sticking 8px down would leave an 8px slot
     above it through which the scrolling page shows - .main-content cannot
     clip that, its own top edge is above the window by then. So at rest the
     strip floats 8px down like the other two panels, and while you scroll it
     docks against the top of the window. If that asymmetry ever has to go,
     the cheap fix is `top: var(--frame-gap)` plus
     `border-top: var(--frame-gap) solid var(--frame-bg)` with
     `background-clip: padding-box` - the border then masks the slot in the
     frame's own colour - and --side-top below moves by the same 8px. */
  padding: 7px 14px; margin: var(--frame-gap) 0;
  background: var(--tb-bg);
  /* The drawer's hairline, on a real border here rather than an inset shadow:
     this strip already had a 1px border, so keeping it costs no pixels, where
     the drawer had none and a border would have made it 262px wide. */
  border: 1px solid var(--tb-edge);
  border-radius: var(--frame-radius);
  box-shadow: var(--frame-shadow);
  font-size: 13px; color: var(--tb-muted);
}
/* Sunset veil - the app-wide pass. Every plate that sits directly on the
   photo band (cards on any page, list rows on History) drops from solid
   white to a flat ~90% translucency: enough for the sunset to warm the
   plate, not enough to put texture under a table. Flat alpha only - NO
   backdrop blur at this scale (that's the expensive primitive, reserved
   for the few small frame elements below and the short Open Projects
   list, whose higher-specificity glass rules override these). Everything
   INSIDE a card (panel-2 tiles, gauge tracks, sticky grid cells in the
   workforce schedule) deliberately keeps its opaque fill: those are the
   working surfaces, and sticky cells in particular must stay opaque or
   the text scrolling beneath them shows through. */
body.app-bg.app-main .card { background: var(--veil-card); }
body.app-bg .project-list a { background: var(--veil-row); }
body.app-bg .project-list a:hover { background: var(--veil-row-hover); }

/* The project header placard is frame, not working surface - one glass
   element per page costs nothing. Background only: its border-left is the
   3px accent stripe and stays. */
body.app-bg .header-widget {
  background: var(--glass-card);
  backdrop-filter: blur(12px) saturate(1.1);
  -webkit-backdrop-filter: blur(12px) saturate(1.1);
}

/* Sunset glass on the frame chrome: the round nav buttons float over the
   translucent band wherever they are NOT already inside the always-graphite
   .whoami-bar (see the flat override for that case just below) - the
   closeout fill screens' own sticky header is the main other place they
   land, and that header still follows the page's theme. Small area - the
   blur cost is negligible (unlike the full-viewport band above, which stays
   blur-free on purpose). */
body.app-bg .nav-btn {
  background: var(--glass-fill);
  border-color: var(--glass-edge);
  backdrop-filter: blur(12px) saturate(1.1);
  -webkit-backdrop-filter: blur(12px) saturate(1.1);
}
/* Light theme (2026-09-07): frame chrome goes opaque like every other
   working surface - --glass-card already resolves to solid white here, so
   this only drops the now-pointless blur. */
:root[data-theme="light"] body.app-bg .header-widget,
:root[data-theme="light"] body.app-bg .nav-btn {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.app-bg .header-widget,
  body.app-bg .nav-btn {
    background: var(--glass-fallback);
  }
}
/* The identity bar itself is graphite in every theme (see .whoami-bar above).
   Its own nav buttons (☰ ← →) get their own colors instead of the plain
   .nav-btn panel fill - `body.app-bg` prefix to match that rule's
   specificity (a bare `.whoami-bar .nav-btn` is two classes to its
   one-type-plus-two-classes and loses in every theme but Dark, where the
   two happen to agree). */
body.app-bg .whoami-bar .nav-btn {
  background: var(--tb-hover); color: var(--tb-text); border-color: var(--tb-border);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
body.app-bg .whoami-bar .nav-btn:active { background: var(--tb-bg); }
/* The bar has two halves now: whatever the page put in the `topbar` block on
   the left, and identity + bell + account hard right. `margin-left: auto` on
   the group is what holds the right half there when the left one is empty -
   every page that is not a task page still renders exactly as before. */
/* flex: none - the identity group must not be the thing that shrinks. Its
   children do not wrap, so squeezing it just pushes the account button past
   the right edge of the bar; all the give belongs to the tab strip on the
   left, which is built to scroll. */
.whoami-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }
.whoami-who { white-space: nowrap; }
/* Below this the bar is carrying a page's own controls as well, and 
   "Signed in as" is the one part of it that says nothing the name does not. */
@media (max-width: 1500px) {
  .whoami-prefix { display: none; }
  /* And the button's own label: the name it would explain is now two
     elements to its left, and the panel it opens says all three of name,
     e-mail and role. The icon keeps the aria-label either way. */
  .whoami-right .account-menu-label { display: none; }
}
/* .account-menu carries its own margin-left:auto for the old flat bar; inside
   the group that would shove the account button away from the bell. */
.whoami-right .account-menu { margin-left: 0; }
.whoami-bar strong { color: var(--tb-text); }
.whoami-role { color: var(--tb-muted); }
/* A fixed blue, not --series-routine: that token repaints per theme, so a
   link here needs its own color the same way the bar's background does. */
.whoami-switch { color: #4fb3ff; text-decoration: none; white-space: nowrap; }
.whoami-switch:hover { text-decoration: underline; }
@media print { .whoami-bar { display: none; } }

/* ---- The account menu, top right of the identity bar.

 "My Account" used to be a fourth group in the left drawer, between Workforce
 and Admin. It did not belong there: the drawer is where you go to *work*
 (aircraft, crew, admin), and your own account is not work - it is the
 thing you are working *as*. Every other tool people here use puts it in the
 same corner, so that is where it went.

 <details>/<summary>, same reasoning as the drawer groups: open/close,
 Enter/Space and the screen-reader announcement come from the browser. A
 few lines in _base.html close it on an outside click, on Escape and after
 a link is followed - a <details> alone stays open until clicked again. ---- */
.whoami-bar { padding-top: 5px; padding-bottom: 5px; }
.account-menu { position: relative; margin-left: auto; flex: none; }
/* .account-menu only ever sits inside the always-graphite .whoami-bar, so
   its own colors go on --tb-* directly rather than through an extra
   descendant selector. */
.account-menu > summary {
  display: flex; align-items: center; gap: 7px;
  padding: 6px 10px 6px 9px; border-radius: 16px; cursor: pointer; list-style: none;
  color: var(--tb-text); font-size: 13px; font-weight: 600; white-space: nowrap;
  border: 1px solid transparent; user-select: none;
}
.account-menu > summary::-webkit-details-marker { display: none; }
.account-menu > summary:hover,
.account-menu[open] > summary { background: var(--tb-hover); border-color: var(--tb-border); }
.account-menu > summary .nav-ico { width: 15px; height: 15px; }
.account-menu-caret { font-size: 10px; color: var(--tb-muted); transition: transform .15s ease; }
.account-menu[open] .account-menu-caret { transform: rotate(180deg); }
.account-menu-panel {
  position: absolute; right: 0; top: calc(100% + 6px); min-width: 250px; max-width: 320px;
  padding: 6px; z-index: 20;
  background: var(--panel-1); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, .35);
}
:root[data-theme="light"] .account-menu-panel { box-shadow: 0 12px 32px rgba(27, 39, 64, .18); }
/* Who this is - name, login, role. The same facts as the "Signed in as" text
   to the left, spelled out in full: the bar abbreviates on a phone. */
.account-menu-head {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 10px 10px; margin-bottom: 6px; border-bottom: 1px solid var(--hairline);
}
.account-menu-head strong { color: var(--text-primary); font-size: 14px; }
.account-menu-head .account-menu-email {
  color: var(--text-secondary); font-size: 12px; font-family: var(--font-mono);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account-menu-role {
  align-self: flex-start; margin-top: 4px; padding: 1px 7px; border-radius: 9px;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--series-routine); background: rgba(79, 179, 255, .12);
}
:root[data-theme="light"] .account-menu-role { background: rgba(53, 119, 227, .12); }
.account-menu-panel a, .account-menu-panel button.account-menu-logout {
  display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: var(--radius-control);
  color: var(--text-secondary); text-decoration: none; font-size: 13.5px; white-space: nowrap;
}
.account-menu-panel a:hover, .account-menu-panel button.account-menu-logout:hover { background: var(--panel-2); color: var(--text-primary); }
.account-menu-panel a.active { color: var(--text-primary); background: var(--panel-2); }
.account-menu-sep { border: 0; border-top: 1px solid var(--hairline); margin: 6px 0; }
.account-menu-logout { color: var(--status-critical); }
.account-menu-logout:hover { color: var(--status-critical); }
/* On a phone the bar is ~360px wide: the identity text keeps one line and
   the button drops its label, the icon and caret are enough there. */
@media (max-width: 560px) {
  .whoami-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .account-menu > summary .account-menu-label { display: none; }
  .account-menu-panel { max-width: calc(100vw - 32px); }
}
@media (prefers-reduced-motion: reduce) { .account-menu-caret { transition: none; } }

/* ---- /account - the page behind "Account settings" ---- */
.acct-card h2 { margin: 0 0 14px; font-size: 16px; }
.acct-field { display: block; margin-bottom: 14px; }
.acct-field > span { display: block; font-size: 12.5px; color: var(--text-secondary); margin-bottom: 6px; }
.acct-field input {
  width: 100%; box-sizing: border-box; background: var(--panel-0); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: var(--radius-control); padding: 9px 10px; font-size: 14px;
  font-family: var(--font-body);
}
.acct-field input[readonly] { color: var(--text-secondary); font-family: var(--font-mono); cursor: default; }
.acct-hint { font-size: 12.5px; color: var(--text-secondary); margin: 6px 0 0; }
.acct-facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; font-size: 13px; margin: 0 0 14px; }
.acct-facts dt { color: var(--text-muted); margin: 0; }
.acct-facts dd { margin: 0; color: var(--text-primary); }
.acct-facts dd .mono { font-size: 12.5px; }
.acct-role-desc { color: var(--text-secondary); font-size: 12px; }
.acct-btn {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--accent-brand); color: var(--on-accent-text); border: none; border-radius: var(--radius-control);
  padding: 9px 14px; font-size: 13.5px; font-weight: 700; font-family: var(--font-body); cursor: pointer;
  text-decoration: none;
}
.acct-btn:hover { filter: brightness(1.08); }
.acct-btn-quiet {
  background: transparent; color: var(--text-secondary); border: 1px solid var(--border); font-weight: 600;
}
.acct-btn-quiet:hover { color: var(--text-primary); border-color: var(--series-routine); filter: none; }
.acct-btn-danger { background: transparent; color: var(--status-critical); border: 1px solid var(--status-critical); font-weight: 600; }
.acct-btn-danger:hover { background: var(--status-critical); color: var(--on-accent-text, #fff); filter: none; }
.acct-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; }
.acct-ok { color: var(--status-good); font-size: 13px; }
.acct-err { color: var(--status-critical); font-size: 13px; }
/* Header strip (who this is + own pages), then a short section menu on the
   left and exactly one open section on the right (t187). Everything sits on
   .card surfaces so the page stays readable over a personal photo in all
   three themes - the light theme's opaque-card rule keys on .card. */
.acct-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px;
  margin-bottom: 16px;
}
/* 240px, not 320: at 1280 a classic (Windows) scrollbar takes 15px, and with
   a 320px basis the four quick links dropped under the name only while the
   page was tall enough to scroll - so opening Appearance pushed everything
   below the header down by a row. */
.acct-head-id { min-width: 0; flex: 1 1 240px; }
.acct-head-kicker {
  margin: 0 0 2px; font-family: var(--font-mono); font-size: 12px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted);
}
.acct-head-name-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
.acct-head-name { margin: 0; font-size: 22px; line-height: 1.25; color: var(--text-primary); overflow-wrap: anywhere; }
.acct-head-name-row .account-menu-role { margin-top: 0; align-self: center; }
/* .account-menu-role itself is 12px now (t181, 15.09.2026 - was 10.5px for
   the compact account-menu dropdown); this page no longer needs its own
   override to reach the floor. */
.acct-head-meta {
  display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 6px 0 0;
  font-size: 13px; color: var(--text-secondary);
}
.acct-head-meta .mono { font-size: 13px; overflow-wrap: anywhere; }
.acct-quick { display: flex; flex-wrap: wrap; gap: 8px; }
.acct-quick a {
  display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 8px 12px;
  border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--panel-2);
  color: var(--text-primary); text-decoration: none; font-size: 13.5px; white-space: nowrap;
}
.acct-quick a:hover { border-color: var(--series-routine); }
.acct-layout { display: grid; grid-template-columns: 208px minmax(0, 1fr); gap: 16px; align-items: start; }
.acct-nav { display: flex; flex-direction: column; gap: 2px; padding: 8px; }
.acct-nav a {
  display: flex; align-items: center; min-height: 40px; padding: 8px 12px; border-radius: var(--radius-control);
  color: var(--text-secondary); text-decoration: none; font-size: 14px; font-weight: 600;
}
.acct-nav a:hover { color: var(--text-primary); background: var(--panel-2); }
.acct-nav a[aria-current="true"] {
  color: var(--text-primary); background: var(--panel-2); box-shadow: inset 3px 0 0 var(--accent-brand);
}
.acct-nav a:focus-visible, .acct-quick a:focus-visible { outline: 2px solid var(--accent-brand); outline-offset: 2px; }
.acct-sections { min-width: 0; }
.acct-section { min-width: 0; }
.acct-section[hidden] { display: none; }
.acct-section h2 { outline: none; }
.acct-sub { margin: 0 0 10px; font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.acct-sep { border: none; border-top: 1px solid var(--hairline); margin: 18px 0; }
.acct-hint-lead { margin: 0 0 10px; max-width: 72ch; }
.acct-hint-foot { margin-top: 14px; }
.acct-row-tight { margin-top: 0; }
.acct-upn { font-size: 12.5px; overflow-wrap: anywhere; }
.acct-section .acct-field { max-width: 480px; }
.acct-section .acct-themes { max-width: 520px; }
[data-theme-submit][hidden] { display: none; }
@media (max-width: 760px) {
  /* Phone: the section menu wraps above the section instead of scrolling
     sideways - a hidden-scrollbar row left "Microsoft 365" off the right
     edge at 375 with nothing to show it was there. */
  .acct-layout { grid-template-columns: minmax(0, 1fr); }
  .acct-nav { flex-direction: row; flex-wrap: wrap; padding: 6px; }
  .acct-nav a { flex: 1 1 auto; justify-content: center; white-space: nowrap; }
  .acct-nav a[aria-current="true"] { box-shadow: inset 0 -3px 0 var(--accent-brand); }
  .acct-quick { width: 100%; }
}
/* The content column's backing band. It lives on a ::before that bleeds
   past .page's real edges into the gutter and fades out there, so the seam
   against the photo is a soft gradient instead of a hard vertical line.

   Sunset Airfield made the band itself translucent (--band-fill, ~70% of
   panel-0 in both themes): the field between and around the cards lets the
   photo glow through, while the cards on top keep their own near-opaque
   fills (see the veil rules above). Flat alpha, deliberately NO backdrop
   blur here - this band is most of the viewport, and a blur that size is
   real GPU work on the hangar tablet, while plain translucency costs
   nothing. Layered over the body's own overlay wash, bare text between
   cards (tab bar included) still sits on an effectively ~90% solid ground.

   The band is the panel's surface, corner for corner (`inset: 0`, the
   panel's radius): the work area is a panel with its own rounded edge
   (t398), so the band never needs a bleed or a fade of its own to hide a
   seam against the gutter beside it. */
body.app-bg .main-content .page::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--band-fill);
  z-index: -1;
  pointer-events: none;
}
/* Open Projects: no bare text sits on .page outside a card (h1/subtitle read
   fine over the darkened hangar overlay), so let the photo show through the
   whole page height instead of the blended patch above. */
body.app-bg .main-content .page.page-blend::before { content: none; }
/* The work area stretches to the shell on every page (`flex: 1` on
   `.main-content .page`), so page-blend needs no min-height of its own. */
body.app-bg .main-content .page.page-blend { min-height: 0; }

/* Sunset glass - the photo-through page only. A two-line card stretched to
   the full 1100px column reads as a solid white stripe over the photo, and
   three of them read as venetian blinds. Two fixes together:

   Material: the cards go translucent with a backdrop blur - the same
   frosted glass the sidebar already is - so the sunset glows through them
   while the blur keeps the text on a calm, legible ground. The summary
   strip's nested KPI tiles drop their opaque grey for a lighter wash of
   the same glass, or they would read as solid slabs floating on it.

   Both themes: the fills come from the glass-* tokens. The extra
   .app-main class is what outranks the app-wide veil rules above. */
body.app-bg.app-main .page-blend .card,
body.app-bg.app-main .page-blend .project-list a {
  background: var(--glass-card);
  border-color: var(--glass-edge);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
}
body.app-bg.app-main .page-blend .project-list a:hover {
  background: var(--glass-card-hover);
}
body.app-bg.app-main .page-blend .kpi-tile {
  background: var(--glass-tile);
  border-color: var(--glass-edge);
}
/* Light theme (2026-09-07): same opaque-working-surface rule as the sheet
   cards above - the tokens already resolve solid, drop the blur. */
:root[data-theme="light"] body.app-bg.app-main .page-blend .card,
:root[data-theme="light"] body.app-bg.app-main .page-blend .project-list a {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* No backdrop-filter (old WebKit on the hangar tablet, some TVs): glass
   with nothing behind it frosted is just low-contrast - go back up to
   near-opaque so the text never sits on the raw photo. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.app-bg.app-main .page-blend .card,
  body.app-bg.app-main .page-blend .project-list a {
    background: var(--glass-fallback);
  }
}
/* The open-project board: one column per tail (see project_list.html), as
   many across as the window holds. 360px is where a project number and its
   "station - opened - delivery" line still fit on two lines; under that the
   grid keeps a single column and the page reads as the old stacked list. */
/* `min(360px, 100%)`, not a flat 360px (QA on t398, 16.09.2026): on a phone
   the column is narrower than 360px - 351px before the frame, 343px with its
   4px gutters - and a track that cannot shrink ran past the work area, where
   .main-content's overflow-x: clip cut the right end off every row. */
.tail-columns { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr)); gap: 0 24px; align-items: start; }
.tail-columns .tail-group-header { margin-top: 12px; }

/* ---- The nav is a drawer at every width, called from three buttons.

 It used to be a horizontal scrolling row across the top of every page.
 That cost a row of a small screen on every scroll position and hid the
 aircraft list off the right edge behind a fade - "this edge scrolls" is a
 hint, not a signpost, and people missed it.

 As a drawer the nav is vertical again (so the aircraft list is a list, not
 a strip), it costs nothing until it is called, and it is reachable from
 any scroll position. The three buttons matter most when the dashboard is
 opened from a home-screen icon: there it runs with no browser chrome at
 all, so back and forward have nowhere else to come from. Injected by
 static/nav.js - see that file for where they mount and why they are in the
 flow rather than floating over the content.

 The ☰ button is the same one on every width, so there is one thing to
 learn rather than a menu that appears only when a window gets small. What
 differs is where the drawer starts and what it costs when it is open - see
 the docked block further down. ---- */
.nav-chrome { display: flex; gap: 8px; align-items: center; }
.nav-chrome-loose { margin: 0 0 10px; }
/* Inside the top bar the buttons are one item among several rather than a row
   of their own, and 44px there makes a 54px strip stick to the top of every
   page. 38px still clears the 32px a thumb needs on the round hit area. */
/* The slot itself must take no room: display:contents makes the injected
   .nav-chrome a direct flex item of the strip, and leaves nothing behind on
   a page with scripting off. */
.chrome-slot { display: contents; }
.whoami-bar .nav-chrome { gap: 6px; flex: none; }
.whoami-bar .nav-btn { width: 38px; height: 38px; border-radius: 19px; font-size: 16px; }
.nav-btn {
  width: 44px; height: 44px; border-radius: 22px; font-size: 18px;
  background: var(--panel-2); color: var(--text-primary);
  border: 1px solid var(--hairline); cursor: pointer;
  transition: transform .1s ease, background-color .18s ease;
}
.nav-btn:active { transform: scale(.94); background: var(--panel-0); }
/* ☰ opens the drawer and then gets out of the way. It used to turn into a ✕
   and stay, which put two crosses on the screen at once - this one beside the
   arrows and .sidebar-close in the drawer's own corner, a few pixels apart -
   while the button itself sat behind the open panel anyway. Closing is the
   drawer's ✕ (plus the backdrop and Escape); ☰ comes back when it closes.

   display, not visibility: a reserved 44px hole beside the arrows reads as a
   button that broke. The arrows shift left instead, which at dock width they
   do regardless - the whole page moves over by 280px. */
body.nav-open [data-nav-menu] { display: none; }

.sidebar {
  /* The frame's gap all the way round (t398), so the drawer is the same
     floating panel whether it slides in over the page or sits docked beside
     it. translateX(-105%) still clears the screen: at 280px that is 294px of
     travel against an 8px start. */
  position: fixed; top: var(--frame-gap); left: var(--frame-gap); bottom: var(--frame-gap);
  width: 280px; max-width: 82vw; height: auto;
  flex-direction: column; align-items: stretch;
  overflow-y: auto; overflow-x: hidden;
  transform: translateX(-105%);
  /* Closed means gone, not just out of sight (QA on t398, 16.09.2026). Moved
     off-screen only, the drawer kept all of its links in the Tab order - a
     keyboard user tabbed through two dozen invisible items before reaching
     the page - and its 24px drop shadow, which starts at the panel's edge,
     reached back into the window: with the frame's gap the closed panel ends
     only 6-10px left of the screen, so an 18-23px dark smear lay down the
     left edge of every page with the drawer shut. `visibility: hidden` takes
     both away. It waits for the slide when closing (the delay) and comes on
     at once when opening (below), the same pattern as .side-panel. */
  visibility: hidden;
  transition: transform .25s cubic-bezier(.32,.72,0,1), visibility 0s linear .25s;
  z-index: 95; box-shadow: inset 0 0 0 1px var(--sb-edge), 4px 0 24px rgba(0, 0, 0, .5);
}
/* The scroll-hint fade belonged to the horizontal strip. */
.sidebar::after { content: none; }
body.nav-open .sidebar { transform: translateX(0); visibility: visible; transition-delay: 0s; }
.sidebar-nav, .sidebar-projects { flex-direction: column; align-items: stretch; }

.nav-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, .5);
  opacity: 0; pointer-events: none;
  transition: opacity .25s ease; z-index: 94;
}
body.nav-open .nav-backdrop { opacity: 1; pointer-events: auto; }

/* Closing it from inside the drawer, and the only ✕ anywhere on the screen.
 The ☰ button opens, and is hidden for as long as the drawer is open: once
 open it sits behind the drawer's own panel, so it was a control you could
 not see. Backdrop and Escape close it too; this is the one that is visible. */
/* Taken out of the column's flow. In it, this 40px button was the first
   item in the sidebar and pushed the ASF lockup a whole button-height down,
   leaving the top of the panel empty. Absolutely placed it keeps the same
   top-right corner while the logo starts at the top: the lockup is 176px
   wide inside a 280px drawer, so the two never overlap. */
.sidebar-close {
  position: absolute; top: 10px; right: 12px; z-index: 2;
  width: 40px; height: 40px;
  border: 1px solid var(--sb-border); border-radius: 20px;
  background: var(--sb-hover-bg); color: var(--sb-muted);
  font-size: 16px; line-height: 1; cursor: pointer;
}
.sidebar-close:hover { color: var(--sb-text); background: var(--sb-active-bg); }

/* ---- Docked, at desk width -------------------------------------------

 On a wide screen the drawer starts open and stays open: it is not covering
 anything, because the page makes room for it (margin, not overlay) and the
 backdrop stays down. Hiding it is the exception, taken with the drawer's own
 ✕, and nav.js remembers that choice per device - so a page at this width
 opens with no ☰ beside the arrows until you hide the sidebar.

 Under 1000px none of this applies and the drawer is what it was: an overlay
 that starts closed. A permanent 280px column on a 390px phone is not
 navigation, it is the whole screen - which is why the docked state is a
 media query and not a preference that could follow a person onto a phone.

 Margin on .main-content rather than putting the sidebar back in the flex
 flow: a fixed→sticky swap cannot animate, a margin can, so hiding and
 showing it moves at the same speed as the drawer it replaced. ---- */
@media (min-width: 1000px) {
  /* Docked width is 260px, not the 280px the mobile overlay still uses just
     below - same-specificity, later in the file, so this is what wins here. */
  .sidebar { width: 260px; }
  .main-content { transition: margin-left .25s cubic-bezier(.32,.72,0,1); }
  /* The drawer's own 8px on the left, plus 8px of gap to the work area
     (t398) - the page stops where the panel ends, not where the window
     would have put a 260px column. */
  body.nav-open .main-content { margin-left: calc(260px + var(--frame-gap) * 2); }
  /* Docked is not a modal layer: nothing to dim, nothing to click through. */
  body.nav-open .nav-backdrop { opacity: 0; pointer-events: none; }
  /* Docked, the drawer is one of the three floating panels and wears the
     frame's own shadow - not the heavy 24px drop of a panel over content,
     and no longer the border down its right side that used to stand in for
     an edge when it was flush against the page. */
  body.nav-open .sidebar { box-shadow: inset 0 0 0 1px var(--sb-edge), var(--frame-shadow); }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar, .nav-backdrop, .nav-btn, .main-content { transition: none; }
}
@media print {
  .nav-chrome, .nav-backdrop { display: none !important; }
  /* The sidebar itself is display:none in print - the room it was given
     would otherwise print as a 280px empty margin down every page. */
  .main-content { margin-left: 0 !important; }
}

/* ---- Recently opened screens, along the bottom (t235) ----

 The screens a person has opened on this device, as small tabs, with the
 screen's number in front of its name. It is the only place the numbers are
 printed now: they were in the drawer in front of every row and in a chip in
 the top strip for a few hours on 12.09.2026, and the drawer read like a
 list of codes.

 Fixed to the bottom of the window, graphite in all three themes like the
 top strip and the drawer - this is app chrome, not part of the page. Its
 own tokens for the same reason .whoami-bar has its own: a themed token
 would go faint on it in one theme out of three.

 `body.has-recent-bar` is added by the script only once a strip is actually
 drawn, and it is what keeps the page's last row clear of it - a fixed bar
 over a form's Save button is how this kind of chrome goes wrong. ---- */
/* t398: no full-width strip along the bottom of the window any more. The
   container is transparent and floats clear of the work area's lower-left
   corner, as wide as the tabs in it and no wider - which is not only the look
   the user asked for but the only honest way to do it: a full-width
   transparent bar would go on swallowing clicks along the bottom of every
   page, where before it at least showed what it was covering.

   Width by content, still bounded on the right: `left` and `right` mark the
   room it may use (the work area's, minus the rail when a page has one) and
   `width: fit-content` with `margin-right: auto` takes only as much of it as
   the tabs need, left-aligned. With more tabs than fit, it stops at the right
   edge and scrolls inside itself, as it always did.

   ...except `fit-content` alone does not stop anywhere (QA on t398,
   16.09.2026). For a positioned box it is max(min-content, room), and the
   min-content of a one-line flex row whose items are `flex: none` is the
   whole row - so six tabs came out ~1000px wide at 1024px, 768px and on a
   phone, ran past the window's right edge, and the tabs beyond it could be
   neither seen nor scrolled to (the row was exactly as wide as its content,
   so there was nothing to scroll). The bound is therefore an explicit
   `max-width` from the same two insets, kept in --rb-left / --rb-right so
   the docked drawer and the rail change one number each and the cap follows.
   `100%` and not `100vw`: for a fixed box that is the viewport without its
   scrollbar, which the office's Windows browsers do draw.

   The 3px padding is the capsules' shadows: `overflow-x: auto` clips on both
   axes (the other axis computes to `auto` with it), so without it a shadow
   would be cut off at the top and bottom of the row. */
.recent-bar {
  --rb-bg: #172630;
  --rb-text: #e5edf2;
  --rb-muted: #a5b7c4;
  --rb-border: #30414d;
  /* The tab capsules: --rb-bg at ~75%, over a short blur. The two states are
     the drawer's own hover and active greys at a higher alpha - a capsule
     that is being pointed at reads as more solid, not as a different colour. */
  --rb-glass: rgba(23, 38, 48, 0.75);
  --rb-glass-hover: rgba(34, 54, 65, 0.86);
  --rb-glass-active: rgba(48, 67, 79, 0.9);
  --rb-edge: rgba(255, 255, 255, 0.1);
  /* How far the row floats off the work area's corner (the spec's 12-16px). */
  --rb-float: 12px;
  --rb-left: calc(var(--frame-gap) + var(--rb-float));
  --rb-right: calc(var(--frame-gap) + var(--rb-float));

  position: fixed; z-index: 6;
  left: var(--rb-left);
  right: var(--rb-right);
  bottom: calc(var(--frame-gap) + var(--rb-float) + env(safe-area-inset-bottom, 0px));
  width: fit-content; margin-right: auto;
  max-width: calc(100% - var(--rb-left) - var(--rb-right));
  display: flex; align-items: center; gap: 6px;
  padding: 3px;
  background: none; border: 0;
  overflow-x: auto; scrollbar-width: thin;
}
/* The nav renders empty and un-hidden (templates/_base.html) until the
   script finds something to show; `display: flex` above would otherwise
   beat the browser's own `[hidden] { display: none }` the moment the
   script does call `bar.hidden = true` (nothing remembered yet, or every
   remembered screen no longer reachable) - the same trap `.notice-pop`
   fell into on 12.09.2026, in the same file. */
.recent-bar[hidden] { display: none; }
/* The pill is the wrapper, not the link: since 12.09.2026 a tab is a link
   and, beside it, the x that closes it (static/recent_screens.js), and the
   two have to read as one tab - one background, one hover, one border. */
/* Each tab is its own capsule since t398 - it used to be a pill drawn only on
   hover, on a bar that supplied the ground. Now the bar supplies nothing, so
   the capsule carries the fill, the hairline and a short shadow itself. */
.recent-tab-wrap {
  display: inline-flex; align-items: center; flex: none;
  border-radius: var(--radius-pill); border: 1px solid var(--rb-edge);
  background: var(--rb-glass);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .22);
}
.recent-tab-wrap:hover { background: var(--rb-glass-hover); }
.recent-tab-wrap.active { background: var(--rb-glass-active); border-color: var(--rb-border); }
/* Engines without a backdrop filter (the hangar tablet, some TVs): a capsule
   with nothing frosted behind it is just a see-through capsule, so it goes
   near-opaque instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .recent-tab-wrap { background: var(--rb-bg); }
  .recent-tab-wrap:hover { background: #223641; }
  .recent-tab-wrap.active { background: #30434f; }
}
.recent-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 3px 11px 3px 5px; color: var(--rb-muted);
  font-size: 12.5px; text-decoration: none; white-space: nowrap;
}
.recent-tab-wrap:hover .recent-tab, .recent-tab.active { color: var(--rb-text); }
/* With an x beside it the link gives back most of its right padding, or the
   tab gets a gap on both sides of the x. */
.recent-tab-wrap:has(.recent-tab-close) .recent-tab { padding-right: 3px; }
.recent-tab-close {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 20px; height: 20px; margin-right: 4px; padding: 0;
  border: 0; border-radius: 10px; background: transparent;
  color: var(--rb-muted); font-size: 15px; line-height: 1; cursor: pointer;
}
.recent-tab-close:hover { background: rgba(255, 255, 255, .12); color: var(--rb-text); }
.recent-tab-close:focus-visible { outline: 2px solid #4fb3ff; outline-offset: 1px; color: var(--rb-text); }
.recent-tab-no {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 21px; height: 21px; padding: 0 6px; border-radius: 11px;
  background: rgba(79, 179, 255, .18); color: #8fcdff;
  font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
}
/* 44px, not the strip's own ~31px: the whole row has to be reachable with a
   thumb on a phone, where this strip sits right above the home indicator. */
@media (max-width: 1024px), (pointer: coarse) {
  .recent-tab { min-height: 40px; padding-top: 0; padding-bottom: 0; }
  /* The x is a finger's target of its own, right beside the tab's: 20px is
     a mouse size, and on a phone it would close the neighbour as often as
     the tab you meant. */
  .recent-tab-close { width: 36px; height: 40px; margin-right: 0; border-radius: var(--radius-pill); }
}
/* The strip stands beside the drawer, not under it. Measured, not guessed:
   at desk width the docked drawer is 260px of opaque graphite over the
   left end of a full-width bar, and the first tabs simply disappeared
   under it (seen on the live build, 12.09.2026). Same 260px and the same
   condition as .main-content's margin just above. */
@media (min-width: 1000px) {
  /* Same 260px and the same condition as .main-content's margin, plus the
     frame's two gaps and the row's own float - so the first capsule lines up
     with the left edge of the work area's content, not with the drawer. */
  body.nav-open .recent-bar { --rb-left: calc(260px + var(--frame-gap) * 2 + var(--rb-float)); }
}
/* And clear of the right-hand rail on the pages that have one: it is
   fixed, full height and painted over anything that runs under it. The rail
   stands inside the frame too (t398), so this is its width plus the gap it
   sits in plus the row's own float. */
.recent-bar--dock { --rb-right: calc(var(--side-dock-w, 68px) + var(--frame-gap) + var(--rb-float)); }
/* How much room the strip needs is measured by the strip itself
   (static/recent_screens.js publishes --recent-bar-h on <html>, the one
   element nothing else redeclares it on) rather than written down twice: a
   constant was wrong by 10px at exactly 1024px and on every touch screen,
   where the tabs grow to 40px and a scrollbar appears under them - and the
   bottom row of Planned Works ended up under the strip (QA pass,
   12.09.2026).

   Design pass on t398 (16.09.2026) found the fallback below had been
   declared as `body.has-recent-bar { --recent-bar-h: ... }` since
   12.09.2026 - a real value ON body, not a default. A custom property does
   not fall through to an ancestor's value once the element itself has one,
   so every page (`.page` here) and every reader elsewhere in the app
   (chat.css, workforce.css, devtracker.css, planned_works.css - all
   `var(--recent-bar-h, <n>px)` on a body descendant) was permanently
   shadowed by this constant, script or no script: `getComputedStyle(html)`
   had the measured number, `getComputedStyle(body)` never did. Moved the
   guesses into the `var()` fallback at the two rules that actually need
   them, so the property itself is free to inherit from <html> down through
   body to everything under it. Both guesses are still the room the floating
   row leaves under itself (t398: the frame's gap plus the row's own 12px,
   which the script measures and includes - see publishHeight in
   static/recent_screens.js) - only where they live changed.

   Less one --frame-gap (QA on t398, 16.09.2026): the number is measured from
   the window's bottom edge, and the bottom --frame-gap of that is already
   .main-content's own margin under the work area - the page ends there, not
   at the window. Added whole, the gap was reserved twice and every page
   ended in 8px of empty band more than the 12px meant to be there. */
body.has-recent-bar .page { padding-bottom: calc(var(--recent-bar-h, 67px) - var(--frame-gap) + 12px); }
/* `.page.page--wide` has its own wide gutter (`.main-content .page.page--wide
   { padding: 12px 16px }`, three classes), which outranks the rule above and
   put the clearance back to 12px - the bottom ~50px of every wide page
   stayed under the strip, the departments board's sideways scrollbar among
   it (16.09.2026). Hence the second selector, at matching specificity. */
body.has-recent-bar .main-content .page.page--wide {
  padding-bottom: calc(var(--recent-bar-h, 67px) - var(--frame-gap) + 12px);
}
@media (max-width: 1024px), (pointer: coarse) {
  body.has-recent-bar .page { padding-bottom: calc(var(--recent-bar-h, calc(84px + env(safe-area-inset-bottom, 0px))) - var(--frame-gap) + 12px); }
  body.has-recent-bar .main-content .page.page--wide {
    padding-bottom: calc(var(--recent-bar-h, calc(84px + env(safe-area-inset-bottom, 0px))) - var(--frame-gap) + 12px);
  }
}
@media print { .recent-bar { display: none !important; } }

/* ---- Tabs (project detail page - Overview/Materials/Personnel/Labor) ---- */
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--hairline); margin-bottom: 20px; flex-wrap: wrap; }
.tab-btn {
  background: none; border: none; color: var(--text-secondary); font-family: var(--font-body);
  font-size: 13.5px; padding: 10px 14px; cursor: pointer; border-bottom: 2px solid transparent;
}
.tab-btn:hover { color: var(--text-primary); }
.tab-btn.active { color: var(--text-primary); border-bottom-color: var(--series-routine); font-weight: 600; }

/* ---- Side dock (project detail page, test builds - see dashboard.html) ----

 The tab strip's sections as a narrow column on the right edge - an icon with
 the section's name under it, the normal way round - each opening as a panel
 that slides in beside the column. The overview is
 the page and does not change, and it stays usable: the panel is not a modal
 (no backdrop, nothing dimmed, no click-outside-to-close) - it sits next to
 the page and is put away with its own "Hide", the dock button or Escape.
 When there is room (body.side-fits, measured in dashboard.html's script) the
 1100px column shifts left to make it; otherwise the panel overlaps the
 column's right part and the column hugs the left edge to lose as little as
 possible. (Two earlier shapes were rejected: a 44px column with the names
 written vertically, unreadable; and 176px horizontal tabs.) ---- */
/* --side-top is where the top strip ends: the strip's own height plus the
   room around it. 62px until t398, when the frame put 8px above the strip and
   8px under it in place of the old 0 + 12px. The rail itself is still flush to
   the window's right edge and full height - it is the page's own chrome, not
   one of the frame's three panels (see docs/specs/t398.md's boundaries).

   It keeps its own job and its own width, but it stands inside the frame like
   everything else since t398: flush to the window it covered the work area's
   right-hand corners and read as the one square thing on the screen. The
   width in --side-dock-w is unchanged, so what it costs the page (the
   `page--dock` gutter, the panels' travel, the strip at the bottom) is still
   written against that one number - plus the frame's gap, once, where the
   rail's own edge moved. */
:root { --side-top: 66px; --side-total: 800px; --side-dock-w: 68px; }
.side-dock {
  position: fixed; top: var(--side-top); right: var(--frame-gap); bottom: var(--frame-gap); z-index: 91;
  width: var(--side-dock-w); display: flex; flex-direction: column; gap: 4px; padding: 10px 0;
  background: var(--panel-1);
  /* Inset hairline, not a border: the rail's width is a layout constant. */
  border-radius: var(--frame-radius);
  box-shadow: inset 0 0 0 1px var(--frame-edge), var(--frame-shadow);
}
body.app-bg .side-dock { background: var(--glass-fill);
  backdrop-filter: blur(12px) saturate(1.1); -webkit-backdrop-filter: blur(12px) saturate(1.1); }
:root[data-theme="light"] body.app-bg .side-dock { backdrop-filter: none; -webkit-backdrop-filter: none; }
.side-dock-btn {
  display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%;
  font-family: var(--font-body); font-size: 10px; line-height: 1.15; text-align: center;
  color: var(--text-secondary); background: none; border: none; border-right: 2px solid transparent;
  padding: 10px 3px 10px 5px; cursor: pointer;
}
.side-dock-btn:hover { color: var(--text-primary); background: var(--panel-2); }
.side-dock-btn[aria-expanded="true"] { color: var(--text-primary); border-right-color: var(--series-routine); }
.side-dock-btn .nav-ico { width: 18px; height: 18px; margin: 0; }
.side-dock-label { overflow-wrap: anywhere; hyphens: auto; }
.side-dock-count { font-family: var(--font-mono); font-size: 11.5px; font-weight: 400; color: var(--text-secondary); }

/* The panel stands beside the rail, inside the frame with it (t398): its own
   gap on the far side of the rail, the frame's corners, and enough travel to
   clear both when it is closed. */
.side-panel {
  position: fixed; top: var(--side-top); right: calc(var(--side-dock-w) + var(--frame-gap) * 2); bottom: var(--frame-gap); z-index: 90;
  width: var(--side-w, 600px); max-width: calc(100vw - var(--side-dock-w) - var(--frame-gap) * 3);
  overflow-y: auto; padding: 0 18px 24px;
  background: var(--panel-0); border-left: 1px solid var(--border);
  border-radius: var(--frame-radius);
  box-shadow: -8px 0 28px rgba(0, 0, 0, .35);
  transform: translateX(calc(100% + var(--side-dock-w) + var(--frame-gap) * 3));
  /* visibility waits for the slide so closing animates too. */
  transition: transform .25s cubic-bezier(.32,.72,0,1), visibility 0s linear .25s;
}
/* `hidden` keeps it out of the accessibility tree and off the tab order; the
   transform keeps it off screen. Both flip together in the script. */
.side-panel[hidden] { display: block; visibility: hidden; }
.side-panel:not([hidden]) { transform: translateX(0); transition-delay: 0s; }
body.side-no-anim .side-panel, body.side-no-anim .main-content .page { transition: none; }
body.side-fits .side-panel { box-shadow: none; }
.side-panel .grid { grid-template-columns: repeat(12, 1fr); }
.side-panel-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 0 10px; margin-bottom: 12px;
  background: var(--panel-0); border-bottom: 1px solid var(--hairline);
}
.side-panel-head h2 { margin: 0; font-family: var(--font-mono); font-size: 14px; letter-spacing: 0.06em; text-transform: uppercase; }
.side-panel-hide {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--text-secondary);
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 3px;
  padding: 4px 10px; cursor: pointer;
}
.side-panel-hide:hover { color: var(--text-primary); border-color: var(--series-routine); }

/* ---- What the app's own two panels add (templates/_side_panels.html) ----

   "Open full page" beside Hide: the panel is the short way in, the real
   screen is one plain link away - no second rendering path, and it still
   works with scripting off. The body is what static/side_dock.js fills from
   the fragment; until it answers it holds one line of text, so an empty
   panel never reads as an empty inbox. */
.side-panel-actions { display: inline-flex; align-items: center; gap: 8px; }
.side-panel-expand {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--text-secondary);
  text-decoration: none; white-space: nowrap;
}
.side-panel-expand:hover { color: var(--text-primary); text-decoration: underline; }
.side-panel-body { min-height: 40px; }
.side-panel-body[aria-busy="true"] { opacity: .55; }
/* Nothing on the rail carries an unread badge any more - the two that did
   (Notifications, Messages) are icons in the top strip now, and the project
   page's own buttons count with .side-dock-count. position:relative stays:
   it is what any future badge would be placed against. */
.side-dock-btn { position: relative; }

/* ---- The bell and the message icon, top right (templates/_base.html) ----

   Moved back here from the rail on 11.09.2026, on the user's call after
   using both: this corner is where a person looks for them, and it is where
   every other tool in the office keeps them.

   Colors on --tb-*, not on the page's own tokens: the strip they sit in is
   graphite in every theme, so they follow the account button beside them -
   the same reason .account-menu does. */
.notice-btn {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 28px; padding: 0; cursor: pointer;
  color: var(--tb-muted); background: none;
  border: 1px solid transparent; border-radius: var(--radius-control);
}
.notice-btn:hover { color: var(--tb-text); background: var(--tb-hover); border-color: var(--tb-border); }
.notice-btn[aria-expanded="true"] { color: var(--tb-text); background: var(--tb-hover); border-color: var(--tb-border); }
.notice-btn .nav-ico { width: 16px; height: 16px; margin: 0; }
/* Half off the icon's top-right corner: the same badge as everywhere else
   (.tk-bell-count is absolutely positioned), placed for this button. */
.notice-btn .tk-bell-count { top: -2px; right: -3px; }

/* ---- The app's two panels hang under their icon ----

   Same panel, same fetched contents, same script - only the geometry is
   answered differently, because the button that opens them is no longer on
   the right edge. The sheet rules above stay exactly as they are: they are
   the project page's, and these two opt out with one attribute. */
.side-panel[data-side-drop] {
  top: calc(var(--side-top) + 6px); right: 12px; bottom: auto; left: auto;
  width: min(var(--side-w, 420px), calc(100vw - 24px));
  max-height: min(72vh, 620px);
  padding: 0 16px 16px;
  border: 1px solid var(--border); border-radius: var(--radius-card);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .38);
  /* Drops into place instead of sliding in from the edge - it belongs to
     the icon above it now, not to the side of the window. */
  transform: translateY(-8px); opacity: 0;
  transition: transform .16s ease, opacity .16s ease, visibility 0s linear .16s;
}
/* transition-delay repeated on purpose: the shorthand above would otherwise
   hold `visibility` back by .16s when OPENING too, and the panel would sit
   invisible for a sixth of a second and then simply be there. */
.side-panel[data-side-drop]:not([hidden]) {
  transform: translateY(0); opacity: 1; transition-delay: 0s;
}
:root[data-theme="light"] .side-panel[data-side-drop] { box-shadow: 0 18px 44px rgba(27, 39, 64, .18); }
@media (max-width: 560px) {
  .side-panel[data-side-drop] { right: 8px; left: 8px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .side-panel[data-side-drop] { transition: none; }
}

/* ---- "Something is waiting for you" ----

   The little window under the icons: an unread message or an unhandled
   notification should be visible without opening anything (user, 11.09.2026).
   Deliberately BELOW the panels in z-order - both live in this corner, and
   if a panel happens to be open the popup should be under it rather than
   floating on top of the list it is announcing. */
.notice-pop {
  position: fixed; top: calc(var(--side-top) + 6px); right: 12px; z-index: 89;
  display: flex; align-items: flex-start; gap: 10px;
  width: min(340px, calc(100vw - 24px)); padding: 11px 10px 11px 13px;
  background: var(--panel-0); border: 1px solid var(--border); border-radius: var(--radius-card);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .38);
}
/* The ✕ sets [hidden] on the element, and `display: flex` above would win
   over the browser's own `[hidden] { display: none }` - one class beats a
   UA rule, so the popup stayed on screen after being closed and after
   being dismissed on a previous page (reported 12.09.2026). Every rule in
   this file that gives an element a display MUST answer [hidden] too, or
   `el.hidden = true` silently does nothing. */
.notice-pop[hidden] { display: none; }
:root[data-theme="light"] .notice-pop { box-shadow: 0 18px 44px rgba(27, 39, 64, .18); }
.notice-pop-ico { width: 16px; height: 16px; flex: none; margin-top: 2px; color: var(--series-routine); fill: currentColor; }
.notice-pop-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.notice-pop-text strong { font-size: 13px; color: var(--text-primary); }
/* Two lines at most, then an ellipsis: a notification body can be a
   paragraph, and this is a nudge towards the panel, not the panel. */
.notice-pop-body {
  font-size: 12.5px; color: var(--text-secondary);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.notice-pop-meta { font-family: var(--font-mono); font-size: 11px; color: var(--text-tertiary, var(--text-secondary)); }
.notice-pop-open {
  align-self: flex-end; flex: none; margin-top: 2px;
  font-family: var(--font-mono); font-size: 11.5px; white-space: nowrap;
  color: var(--series-routine); background: none; border: 0; padding: 0; cursor: pointer;
  text-decoration: none;
}
.notice-pop-open:hover { text-decoration: underline; }
.notice-pop-close {
  flex: none; width: 20px; height: 20px; padding: 0; line-height: 18px;
  font-size: 15px; color: var(--text-secondary);
  background: none; border: 0; border-radius: var(--radius-control); cursor: pointer;
}
.notice-pop-close:hover { color: var(--text-primary); background: var(--panel-2); }
@media print { .notice-pop { display: none; } }

/* The overview column: the dock takes its width off the right at every size,
   and when the panel fits beside it the column narrows to what is left. */
.main-content .page.page--dock { padding-right: calc(var(--side-dock-w) + var(--frame-gap) + 24px); transition: margin .25s cubic-bezier(.32,.72,0,1); }
/* The page simply gives up the room the panel needs and reflows into what
   is left - it used to re-centre a fixed 1100px column in the remainder,
   which is meaningless now that the column is as wide as the window. */
body.side-fits .main-content .page.page--dock { margin-right: var(--side-total); }
/* Not enough room for both: the column hugs the left edge so the panel
   covers as little of it as possible. The uncovered part stays live. */
body.side-open:not(.side-fits) .main-content .page.page--dock { margin-left: 0; margin-right: auto; }

/* Refresh group in the top strip (dashboard.html's topbar block). */
.topbar-refresh { display: inline-flex; align-items: center; gap: 8px; flex: none; white-space: nowrap; margin-left: 8px; }

@media (prefers-reduced-motion: reduce) { .side-panel, .main-content .page.page--dock { transition: none; } }
@media print { .side-dock, .side-panel, .topbar-refresh { display: none !important; } .main-content .page.page--dock { padding-right: 24px; margin-right: auto; } }

/* Tabs used as navigation links (admin users role filter): same bar, but an
   <a> so each view is a real URL. */
a.tab-btn { text-decoration: none; display: inline-block; }
.tab-count {
  font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary);
  border: 1px solid var(--hairline); border-radius: 8px; padding: 0 5px; margin-left: 5px;
}

/* ---- The aircraft list (/history) ----
   One list of aircraft, freshest delivery first, with a search box over it
   (t231, 12.09.2026). It was a fleet catalog until then - cards by aircraft
   type, a grid/list toggle and a pagination of its own - and all three went
   with the rework; the rows themselves are the app's ordinary .project-list.
   .view-toggle stays below: the project overview widget uses it too. */
.aircraft-search {
  background: var(--panel-1); border: 1px solid var(--border); border-radius: var(--radius-control);
  color: var(--text-primary); padding: 7px 12px; font-size: 13px; width: 220px;
  font-family: var(--font-body); margin-bottom: 14px;
}
.aircraft-search::placeholder { color: var(--text-muted); }
/* Said out loud because it is a trap this project has already fallen into:
   the rows carry no display of their own, so the [hidden] the search sets
   in static/app.js is what hides them - a later `display` on a class here
   would beat the attribute and leave filtered-out aircraft on screen. */
#aircraft-list li[hidden], .empty-state[hidden] { display: none; }

.view-toggle { display: flex; border: 1px solid var(--border); border-radius: var(--radius-control); overflow: hidden; flex: none; }
.view-toggle-btn {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 30px; background: var(--panel-1); border: none;
  color: var(--text-muted); cursor: pointer;
}
.view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--border); }
.view-toggle-btn.active { background: var(--panel-2); color: var(--status-critical); }
.view-toggle-btn svg { width: 15px; height: 15px; fill: currentColor; }

/* ---- Planned Works (Gantt/timeline over the local test workbook) ---- */
/* Planned Works's .pw-* rules moved to static/planned_works.css on
   2026-09-03 - that page loads its own stylesheet from {% block head %}. */

/* ---- Print ----
   Forces the light palette regardless of the on-screen theme (dark ink on a
   dark background is unreadable on paper), strips chrome that means nothing
   on a printout (sidebar, hangar photo, refresh/theme buttons, hint
   popups), and prints only whichever tab is currently open - same "print
   what you're looking at" a browser gives any page, not an attempt to force
   every tab onto one printout. static/app.js's beforeprint handler expands
   every <details> first, since a closed one prints as an empty triangle
   with nothing under it - CSS alone can't reliably force open a closed
   <details> across browsers. */
@media print {
  :root {
    --panel-0: #f4f7f9; --panel-1: #fbfcfd; --panel-2: #eef3f6;
    --hairline: #d7e0e6; --border: rgba(23, 33, 42, 0.14);
    --text-primary: #17212a; --text-secondary: #61707c; --text-muted: #61707c;
    --series-routine: #0a63a8; --series-nrc: #9c480b;
    --status-good: #0e6b38; --status-warning: #875c04; --status-critical: #c8261c;
    --card-shadow: none; --on-accent-text: #f5f8fb;
  }
  body.app-bg { background-image: none; }
  body.app-bg .main-content .page::before { content: none; }

  .sidebar, .mock-banner, .refresh-btn, .back-btn, .sidebar-theme-toggle,
  .hint-popup, .tabs, .aircraft-search,
  .view-toggle, .card-drilldown summary::before, .blocker-drilldown summary::before {
    display: none !important;
  }
  /* Paper has no frame (t398). The floating panels are a screen device: on
     a printed closeout form or an invoice they would come out as a grey
     margin down every edge, a rounded corner across a table and a shadow
     rendered as a grey smear. So the work area goes back to being the page:
     no gap, no corners, no shadow, no photograph - and .main-content is a
     plain block again, because the frame made it a flex column. */
  /* The column the frame made of <body> goes too: on paper a `min-height` of
     one viewport is a blank tail after the last row. */
  body.app-main { display: block; min-height: 0; }
  .app-shell { display: block; }
  .main-content {
    display: block;
    margin: 0 !important;
  }
  .whoami-bar { margin: 0; box-shadow: none; }
  .main-content .page {
    max-width: none; padding: 0;
    margin: 0; border-radius: 0;
    background: none;
    box-shadow: none;
  }
  /* The same reset again at the specificity of the screen rules that set it
     (QA on t398, 16.09.2026): the page's own background is put on the work
     area by `body.app-bg.app-main .main-content .page` and the strip's room
     by `body.has-recent-bar ... .page`, both of which outrank the plain
     `.main-content .page` above - so every printout ended in ~80px of blank
     room kept for a strip that print hides. */
  body.app-bg.app-main .main-content .page { background: none; }
  body.has-recent-bar .page,
  body.has-recent-bar .main-content .page.page--wide { padding-bottom: 0; }

  a { color: inherit; text-decoration: none; }
  .card { box-shadow: none; border: 1px solid var(--hairline); break-inside: avoid; }
  table { break-inside: auto; }
  tr { break-inside: avoid; }
  thead { display: table-header-group; }

  /* Every disclosure forced open (see app.js) - print its content, not the
     little ▸/▾ marker that only makes sense as an on-screen affordance. */
  details[open] > summary { list-style: none; cursor: default; }
  .card-drilldown summary b, .blocker-drilldown summary { color: var(--text-primary); }

  /* The header widget's existing "updated HH:MM:SS" note (dashboard.html,
     tv_project.html) is left visible on purpose - a printout with no
     freshness stamp is a future dispute over whether it's still current. */
  .cache-note { color: var(--text-secondary); }
}

/* ---- Phone-as-remote for the TV screens (dashboard/tv_remote.py) ---- */

/* The QR badge in the TV page's corner (static/tv_remote.js). White plate
   regardless of theme: a QR needs dark-on-light for phone cameras, and the
   hangar TVs run the dark theme. */
.tv-remote-badge {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 40;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  text-align: center;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.tv-remote-qr { display: block; width: 132px; height: 132px; margin: 0 auto; }
.tv-remote-caption { color: #444; font-size: 11px; margin-top: 6px; max-width: 140px; }
.tv-remote-code { color: #111; font-size: 15px; letter-spacing: 2px; margin-top: 2px; }

/* Inline variant used by tv_project.html's topbar: fixed bottom-right, the
   badge sat on top of the CPI card's numbers. Small enough to live in a
   12px bar and still scan from arm's length. */
.tv-remote-badge.tv-remote-inline {
  position: static;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 12px;
  padding: 3px 8px;
  border-radius: 6px;
  box-shadow: none;
  vertical-align: middle;
}
/* The inline badge is now only used while a panel is zoomed (?focus=): the
   zoom can only have come from an already-paired phone, so this is a
   reminder, not the primary scan target - small enough not to cost the
   zoomed table a row. The balanced layout embeds the QR in the forecast
   panel instead, see .tv-remote-embed. */
.tv-remote-inline .tv-remote-qr { width: 56px; height: 56px; margin: 0; }
.tv-remote-inline .tv-remote-caption { margin-top: 0; font-size: 10px; max-width: 90px; white-space: normal; text-align: left; }
.tv-remote-inline .tv-remote-code { margin-top: 0; font-size: 12px; }

/* Embedded variant (tv_project.html, balanced layout): sat in the empty
   lower half of FORECAST DATES - that panel holds four fixed lines and
   nothing else, so the space is always there. Shaped like the user's
   mockup (04.09): a bordered nested chip across the panel's foot, QR at
   left, caption and pairing code stacked at right. tv_remote.js appends
   img, caption, code in that order - a 2x2 grid with the img spanning both
   rows gives the stack without touching the script. Sized in em off --u so
   it grows with the density, capped so wall's 1.7x doesn't eat the dates. */
.tvb-forecasts .tv-remote-badge.tv-remote-embed {
  position: static; margin: auto 0 0; padding: 0.45em 0.6em; box-shadow: none;
  border: 1px solid var(--border); border-radius: 0.45em; background: var(--panel-2);
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;
  column-gap: 0.7em; row-gap: 0.15em; align-items: center; text-align: left; min-width: 0;
}
.tv-remote-embed .tv-remote-qr { grid-row: 1 / 3; width: clamp(56px, 4.4em, 96px); height: clamp(56px, 4.4em, 96px); margin: 0; border-radius: 0.25em; }
.tv-remote-embed .tv-remote-caption { display: block; margin: 0; max-width: none; align-self: end; font-size: 0.82em; color: var(--text-secondary); white-space: normal; line-height: 1.2; }
.tv-remote-embed .tv-remote-code { margin: 0; align-self: start; font-size: 0.85em; letter-spacing: 0.14em; color: var(--text-muted); }
body.tv-mode .tv-remote-qr { width: 168px; height: 168px; }
body.tv-mode .tv-remote-caption { font-size: 13px; max-width: 176px; }

/* On mobile, add padding to the TV grid to prevent the last card
   from being covered by the fixed QR badge. */
@media (max-width: 600px) {
  .tv-grid { padding-bottom: 180px; }
}

/* The phone page behind the QR (templates/tv_remote.html). */
.tv-remote-body { background: var(--panel-0); }
.tv-remote-page { max-width: 480px; margin: 0 auto; padding: 20px 16px 40px; }
.tv-remote-page h1 { font-size: 22px; margin: 0 0 10px; }
.tv-remote-page h2 { font-size: 14px; color: var(--text-secondary); margin: 22px 0 8px; }
.tv-remote-code-chip {
  font-size: 13px;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 2px 8px;
  letter-spacing: 2px;
  vertical-align: middle;
}
.tv-remote-status { margin: 0 0 14px; overflow-wrap: break-word; word-break: break-word; }
.tv-remote-actions { display: flex; flex-direction: column; gap: 8px; }
.tv-remote-projects { display: flex; flex-direction: column; gap: 8px; }
.tv-remote-btn {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--panel-1);
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  font-size: 16px;
  cursor: pointer;
}
.tv-remote-btn:disabled { opacity: 0.6; }
.tv-remote-btn.tv-remote-current { border-color: var(--series-routine); box-shadow: inset 3px 0 0 var(--series-routine); }
.tv-remote-btn.tv-remote-sent { border-color: var(--status-good, var(--series-routine)); }
.tv-remote-project .tv-remote-tail { display: block; font-size: 18px; font-weight: 600; }
.tv-remote-project .tv-remote-meta { display: block; color: var(--text-secondary); font-size: 13px; margin-top: 2px; }

/* ============================================================

/* ============================================================
   Big-screen board (tv_project.html, body.tv-board)

   Panel order and placement follow the reference the user approved
   2026-09-03; every colour, face and surface is this app's own, so the
   board reads as the same product as the rest of the site:
     - surfaces  --panel-1 plates on the --panel-0 ground, --border edges,
                 --hairline row rules (no invented greys)
     - type      IBM Plex Sans for words, JetBrains Mono for every figure
     - colour    status only: --status-good / -warning / -critical, plus
                 --series-routine / --series-nrc for the two card groups
                 and --series-dependency for "blocked by another card".
                 The reference's arbitrary brights are gone.

   TWO DENSITIES, ONE LAYOUT. Everything is sized in em off --u:
     .density-desk - a monitor at a workplace, read from ~1 m
     .density-wall - a TV across the hangar, read from 3-6 m (~1.7x)
   Density comes from the URL, never width: a 1080p TV and a 1080p monitor
   report the same viewport (dashboard/tv_board.py). Table row caps live in
   the view model; the type scale lives here.

   Scoped to .tv-board - dashboard.html shares no markup with this page.
   ==================== */
/* Desk was 1.12vh; at that scale rows 1-2 (fixed 27% each) sat half empty
   and the tables were ~11px on a 1080p monitor (user's screenshot,
   2026-09-04: "слишком растянутые ... плохо видно"). 1.3vh fills them;
   the blocking-cards row is flex: 1 and still fits its 15 rows once the QR
   badge left the chrome bar (see .tv-remote-embed). */
.tv-board.density-desk .tv-fit-page { --u-base: clamp(10px, 1.55vh, 30px); }
.tv-board.density-wall .tv-fit-page { --u-base: clamp(15px, 1.9vh, 38px); }
/* --zoom is set inline on <body> from ?zoom= (tv_project.html) - the phone
   remote's text-size steps (dashboard/tv_board.py ZOOM_STEPS). One
   multiplier on the one knob, so type, spacing and the QR chip all scale
   together; the view model trims the row caps by the same factor. */
.tv-board .tv-fit-page { --u: calc(var(--u-base) * var(--zoom, 1)); padding: 0; gap: 0.85vh; font-size: var(--u); background: transparent; position: relative; z-index: 1; }

/* ---- the hangar photo, behind everything instead of in its own strip ----
   The hero strip cost 15% of the height for decoration and went (03.09);
   the user then asked for the photo back as a full-screen background under
   the interface (04.09): visible in the gaps and through the panels,
   strongest behind the identity bar, gone by the blocking-cards table, at
   "20-30% intensity" so 91.7% / 09-08 / 4 DAYS read first. A fixed layer
   on body::before rather than body's background so its opacity and the
   dark gradient don't touch the panels; same shot as the old hero
   (hangar-jet-dark.jpg - the user's "ChatGPT Image 3 сент. 16_16_00" is
   this file's source PNG). The jet sits left of centre in the frame; the
   zoom and offset push it up and to the right, under TODAY / CARDS
   CLOSED, where the mockup had it. */
body.tv-board { background: var(--panel-0); }
body.tv-board::before {
  content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(180deg, rgba(16, 21, 26, 0.2) 0%, rgba(16, 21, 26, 0.55) 40%, rgba(16, 21, 26, 0.94) 78%, var(--panel-0) 100%),
    url('images/background/gallery/hangar-jet-dark.jpg');
  /* 128% zoom, then offset so the fuselage runs along the top edge, centre-
     right: nose and windows behind the identity bar, the rest ghosting
     through the row-1 panels. Numbers from the frame: jet centre at 32% x /
     41% y of the picture. */
  background-size: auto 100%, 128% auto;
  background-position: 0 0, 24vw -47vh;
  background-repeat: no-repeat;
  opacity: 0.6;
}
/* Panels 93% opaque (the user's "92-96%"): tables stay solid to read, the
   photo only ghosts through as a tint. The identity bar is the one place
   the photo is meant to be seen, so it is the most transparent. */
.tv-board .tvb-panel { background: rgba(26, 32, 41, 0.93); }
/* The bar is where the photo is meant to show: 5.5% of the screen (the
   mockup's header) instead of a text line, and the most transparent
   surface on the page. The extra height comes out of row 1 (22.5%, was
   24.5%), not the blocking-cards table. */
.tv-board .tvb-idbar { background: rgba(26, 32, 41, 0.55); min-height: 5.5vh; align-items: center; }
.tv-board .tvb-cards { background: rgba(26, 32, 41, 0.97); }
:root[data-theme="light"] body.tv-board::before { opacity: 0.28; }
:root[data-theme="light"] .tv-board .tvb-panel,
:root[data-theme="light"] .tv-board .tvb-cards { background: rgba(255, 255, 255, 0.94); }
:root[data-theme="light"] .tv-board .tvb-idbar { background: rgba(255, 255, 255, 0.8); }
.tv-board .tvb-row { display: grid; gap: 0.7vw; min-height: 0; padding: 0 0.7vw; }

/* ---- identity bar: one line, no photo. The hangar shot lived here and
     cost 15% of the page height for decoration; on a board whose whole job
     is fitting more rows, height is the scarcest resource. ---- */
.tvb-idbar { flex: none; display: flex; align-items: baseline; gap: 0.7em;
  padding: 0.5em 0.9vw 0.45em; border-bottom: 1px solid var(--border); background: var(--panel-1); }
.tvb-tail { font-family: var(--font-mono); font-weight: 700; font-size: 1.7em; line-height: 1;
  letter-spacing: 0.01em; color: var(--text-primary); }
.tvb-check { font-size: 1em; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-secondary); }
.tvb-proj { font-size: 0.92em; color: var(--text-muted); }
.tvb-idbar-gap { flex: 1; }
.tvb-customer { font-size: 0.95em; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 28vw; }
.tvb-where { font-size: 0.9em; color: var(--text-muted); white-space: nowrap; }
.tvb-phase { padding: 0.12em 0.5em; border-radius: 0.3em; background: var(--panel-2); color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.82em; }
.tvb-phase.sev-critical { color: var(--status-critical); }
.tvb-phase.sev-warning { color: var(--status-warning); }
.tvb-phase.sev-good { color: var(--status-good); }
.tvb-updated { font-size: 0.88em; color: var(--text-muted); white-space: nowrap; }
.tvb-updated b { color: var(--text-secondary); font-weight: 500; }
.tvb-live { display: inline-flex; align-items: center; gap: 0.35em; font-size: 0.85em;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--status-good); }
.tvb-live-dot { width: 0.45em; height: 0.45em; border-radius: 50%; background: var(--status-good); }
.tvb-logo { height: 1.5em; width: auto; opacity: 0.95; align-self: center; }
.tvb-logo-on-light { display: none; }
:root[data-theme="light"] .tvb-logo-on-dark { display: none; }
:root[data-theme="light"] .tvb-logo-on-light { display: block; }

/* ---- row proportions ---- */
/* Desk row 1 is 24.5%, not 27%: at the 1.3vh scale the TODAY tiles still
   had air while the 15-row blocking table below ran 21px past its panel at
   1920x1080 - this hands those pixels down. Wall keeps 27% (rule below). */
.tvb-row-1 { flex: 0 0 22.5%; grid-template-columns: 1.55fr 1.25fr 1fr; }
/* Four columns: blocking | labor | forecast dates | hours-over-CPI. Hours
   and CPI used to be two full-height columns with a headline figure each
   and air below (the TV photo, 04.09); stacked they take one column and
   the row can be shorter. `order` puts the stack last without moving the
   markup. */
.tvb-row-2 { flex: 0 0 27%; grid-template-columns: 1.45fr 1.9fr 1.15fr 1fr; }
/* Swapped 04.09 (user: "forecast date, hours и kpi перенеси - поменять
   местами") - the Hours/CPI stack now sits where Forecast Dates was, and
   Forecast Dates (with the QR chip) takes the last column. Same markup
   order in tv_project.html, only the two `order` values traded. */
.tvb-kpi-col { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 0.85vh; order: 1; }
.tvb-kpi-col > .tvb-panel { flex: 1 1 0; gap: 0.25em; }
.tvb-forecasts { order: 2; }
/* Half a row each, so the two panels keep only their headline: the
   routine/nonroutine hour breakdown, the gauge's 0/1.0/2+ scale and the
   fine-print captions go (measured: without this Hours ran 69px and CPI
   20px past a half-row on desk, 48/32 on wall). The plan figure carries
   its label inline. */
.tvb-kpi-col .tvb-hours-rows, .tvb-kpi-col .tvb-cpi-cap, .tvb-kpi-col .tvb-cpi-scale { display: none; }
.tvb-kpi-col .tvb-cpi-val { font-size: 2em; }
.tvb-kpi-col .tvb-cpi-delta { font-size: 0.9em; }
.tvb-kpi-col .tvb-hours-plan .tvb-cell-cap { font-size: 0.6em; margin-left: 0.3em; }
.tv-board .tvb-cards { flex: 1; min-height: 0; margin: 0 0.7vw; }
/* Desk turns that upside down (user, 04.09: "показывай только пять
   блокинг-карт, остальное раскрою; за счёт этого увеличь другие"): the
   blocking-cards panel is content-sized around its five rows and rows 1-2
   share everything else, weighted towards row 2 (two tables). The type
   scale for desk (--u above) is set so those rows are full, not airy.
   Wall keeps the percentage rows below; the focus rules further down
   override all of this when a panel is zoomed. */
.tv-board.density-desk .tvb-row-1 { flex: 1 1 0; }
.tv-board.density-desk .tvb-row-2 { flex: 1.15 1 0; }  /* was 1.3 before hours/CPI were stacked; 1.15 fits 9 labor rows */
.tv-board.density-desk .tvb-cards { flex: 0 0 auto; }

/* ---- panel shell ---- */
.tvb-panel { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 0.45em;
  background: var(--panel-1); border: 1px solid var(--border); border-radius: 0.5em;
  padding: 0.65em 0.9em; overflow: hidden; }
.tvb-panel-head { flex: none; display: flex; align-items: center; gap: 0.5em; font-size: 0.95em;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
.tvb-head-count, .tvb-head-total { margin-left: auto; color: var(--text-secondary); letter-spacing: 0; }
.tvb-head-summary { display: none; margin-left: auto; color: var(--text-secondary); letter-spacing: 0; text-transform: none; }
.tv-board.focus-labor .tvb-progress .tvb-head-summary,
.tv-board.focus-cards .tvb-progress .tvb-head-summary { display: inline; }
.tvb-head-total b { font-size: 1.5em; color: var(--text-primary); }
.tvb-cell-cap { font-size: 0.88em; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); white-space: nowrap; }
.tvb-cell-val { font-size: 1.9em; font-weight: 700; line-height: 1.1; color: var(--text-primary); white-space: nowrap; }
.tvb-unit { font-size: 0.5em; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.tvb-dim { color: var(--text-muted); }
.tvb-empty { font-size: 1em; color: var(--text-muted); }
.tvb-more { flex: none; font-size: 0.88em; color: var(--text-muted); padding-top: 0.25em; }
.tvb-ico { width: 1.7em; height: 1.7em; flex: none; }
.grp-routine { color: var(--series-routine); }
.grp-nrc { color: var(--series-nrc); }
.verdict-behind { color: var(--status-critical); }
.verdict-on_schedule, .verdict-delivered { color: var(--status-good); }
.verdict-unknown { color: var(--text-secondary); }
/* blocker categories - tokens only, one hue each */
.cat-material { color: var(--status-critical); }
.cat-tool { color: var(--status-warning); }
.cat-child_cards { color: var(--series-dependency); }
.cat-not_started { color: var(--text-muted); }

/* ---- project progress ---- */
.tvb-progress-body { flex: 1; min-height: 0; display: flex; align-items: center; gap: 1.3em; }
.tvb-donut { position: relative; flex: none; height: min(10.5em, 100%); aspect-ratio: 1; display: grid; place-items: center; }
.tvb-donut svg { width: 100%; height: 100%; display: block; }
.tvb-donut-mid { position: absolute; text-align: center; }
.tvb-donut-pct { font-size: 2em; font-weight: 700; line-height: 1; color: var(--text-primary); }
.tvb-donut-cap { font-size: 0.82em; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
.tvb-progress-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.7em; }
/* Was gap-only, so three short values (dates, day count) clumped at the
   left with the rest of the panel's width sitting empty (user, 04.09:
   "растянуть, добавить пробелы между значениями"). Each cell now grows to
   fill the row; text-align follows position so they read as a left-to-
   right timeline (start / forecast / status) instead of drifting center. */
.tvb-dates { display: flex; align-items: flex-end; gap: 0.7em; padding-bottom: 0.6em; border-bottom: 1px solid var(--hairline); }
.tvb-date-cell { flex: 1 1 0; min-width: 0; text-align: left; }
.tvb-date-cell:nth-of-type(2) { text-align: center; }
.tvb-date-cell:last-of-type { text-align: right; }
.tvb-arrow { flex: none; }
.tvb-arrow { font-size: 1.5em; color: var(--text-muted); line-height: 1.2; }
.tvb-submetrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.7em; }
.tvb-sub { min-width: 0; }
.tvb-sub .tvb-cell-val { font-size: 1.5em; }
.tvb-sub-note { font-size: 0.88em; color: var(--text-muted); }

/* ---- today: five icon tiles ---- */
.tvb-today-tiles { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.45em; }
.tvb-tile { min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.15em; text-align: center; background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 0.45em; padding: 0.5em 0.3em; }
.tvb-tile-val { font-size: 1.9em; font-weight: 700; line-height: 1; color: var(--text-primary); }
.tvb-tile-cap { font-size: 0.78em; line-height: 1.2; color: var(--text-muted); }
.tvb-tile.tone-good { color: var(--status-good); }
.tvb-tile.tone-bad { color: var(--status-critical); }
.tvb-tile.tone-warn { color: var(--status-warning); }
.tvb-tile.tone-routine { color: var(--series-routine); }
.tvb-tile.tone-plain { color: var(--text-secondary); }
.tvb-tile.tone-good .tvb-tile-val, .tvb-tile.tone-bad .tvb-tile-val,
.tvb-tile.tone-warn .tvb-tile-val, .tvb-tile.tone-routine .tvb-tile-val { color: inherit; }

/* ---- trend: real daily closures, both groups on one axis ---- */
.tvb-legend { flex: none; display: flex; gap: 0.9em; font-size: 0.82em; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em; }
.tvb-key { display: inline-flex; align-items: center; gap: 0.35em; }
.tvb-key i { width: 0.75em; height: 0.75em; border-radius: 2px; display: inline-block; }
.tvb-chart-wrap { flex: 1; min-height: 0; display: flex; gap: 0.5em; }
.tvb-yaxis { flex: none; display: flex; flex-direction: column; justify-content: space-between;
  font-size: 0.78em; color: var(--text-muted); padding-bottom: 1.5em; text-align: right; }
.tvb-chart { flex: 1; min-width: 0; display: flex; align-items: flex-end; gap: 0.4em; }
.tvb-bar-col { flex: 1; min-width: 0; height: 100%; display: flex; flex-direction: column; gap: 0.25em; }
.tvb-bar-stack { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 0.15em; }
.tvb-bar-total { flex: none; font-size: 0.82em; text-align: center; color: var(--text-secondary); }
.tvb-bar { display: flex; flex-direction: column; border-radius: 0.15em 0.15em 0 0; overflow: hidden; min-height: 2px; }
.tvb-bar-seg { display: block; min-height: 2px; }
.seg-routine { background: var(--series-routine); }
.seg-nrc { background: var(--series-nrc); }
.tvb-bar-col.is-today .tvb-bar { opacity: 0.5; }
.tvb-bar-label { flex: none; font-size: 0.8em; text-align: center; color: var(--text-muted); }

/* ---- what's blocking: four icon cards ---- */
.tvb-block-cards { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.4em; }
.tvb-block-card { min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.15em; text-align: center; background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 0.45em; padding: 0.5em 0.25em; }
.tvb-block-card.tvb-zero { opacity: 0.4; }
.tvb-block-count { font-size: 1.8em; font-weight: 700; line-height: 1; color: inherit; }
.tvb-block-label { font-size: 0.76em; line-height: 1.2; color: var(--text-muted); }
.tvb-block-note { flex: none; font-size: 0.82em; color: var(--text-muted); padding-top: 0.3em;
  border-top: 1px solid var(--hairline); }

/* ---- hours ---- */
.tvb-hours-main { font-size: 1.9em; font-weight: 700; line-height: 1; color: var(--text-primary); }
.tvb-hours-plan { font-size: 1.35em; font-weight: 600; line-height: 1; color: var(--text-secondary); margin-top: 0.2em; }
.tvb-hours-rows { margin-top: auto; display: flex; flex-direction: column; gap: 0.2em; font-size: 0.92em; }
.tvb-hours-row { display: flex; justify-content: space-between; gap: 0.5em; color: var(--text-secondary); }
.tvb-hours-grp { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.88em; }
.tvb-hours-cap { font-size: 0.82em; color: var(--text-muted); }

/* ---- CPI ---- */
.tvb-cpi-val { font-size: 2.8em; font-weight: 700; line-height: 1; }
.cpi-good { color: var(--status-good); }
.cpi-warning { color: var(--status-warning); }
.cpi-critical { color: var(--status-critical); }
.tvb-cpi-gauge { display: flex; flex-direction: column; gap: 0.2em; }
.tvb-cpi-track { position: relative; height: 0.7em; border-radius: 0.35em;
  background: linear-gradient(to right, var(--status-good) 0%, var(--status-good) 52%,
    var(--status-warning) 52%, var(--status-warning) 60%, var(--status-critical) 60%, var(--status-critical) 100%); }
.tvb-cpi-marker { position: absolute; top: -0.2em; bottom: -0.2em; width: 0.22em; transform: translateX(-50%);
  background: var(--text-primary); border-radius: 0.1em; }
.tvb-cpi-scale { display: flex; justify-content: space-between; font-size: 0.78em; color: var(--text-muted); }
.tvb-cpi-delta { font-size: 1em; font-weight: 600; }
.tvb-cpi-cap { font-size: 0.8em; color: var(--text-muted); margin-top: auto; }

/* ---- forecast dates ---- */
.tvb-fc-rows { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: center; gap: 0.3em; }
.tvb-fc-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6em;
  padding-bottom: 0.28em; border-bottom: 1px solid var(--hairline); }
.tvb-fc-row:last-child { border-bottom: 0; }
.tvb-fc-label { font-size: 0.86em; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.tvb-fc-date { font-size: 1.3em; font-weight: 700; color: var(--text-primary); }
.tvb-fc-row.fc-routine .tvb-fc-date { color: var(--series-routine); }
.tvb-fc-row.fc-nrc .tvb-fc-date { color: var(--series-nrc); }
.tvb-fc-row.fc-promised .tvb-fc-date { color: var(--text-secondary); }

/* ---- tables (no scroll: the view model caps the rows) ---- */
.tvb-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 0.9em; }
.tvb-table th { text-align: left; font-size: 0.86em; font-weight: 500; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-muted); padding: 0 0.4em 0.3em 0; border-bottom: 1px solid var(--hairline); }
.tvb-table td { padding: 0.22em 0.4em 0.22em 0; border-bottom: 1px solid var(--hairline);
  color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tvb-table tr:last-child td { border-bottom: 0; }
.tvb-table .num { text-align: right; }
.tvb-td-name { color: var(--text-primary); }
.tvb-dot { display: inline-block; width: 0.55em; height: 0.55em; border-radius: 50%;
  background: currentColor; margin-right: 0.5em; vertical-align: 0.05em; }
.tvb-status { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.88em; font-weight: 600; }
.st-ok { color: var(--status-good); }
.st-over { color: var(--status-critical); }
.st-not_started { color: var(--text-muted); }
.tvb-labor .tvb-table th:nth-child(1), .tvb-labor .tvb-table td:nth-child(1) { width: 27%; }
.tvb-labor .tvb-table th:nth-child(2), .tvb-labor .tvb-table td:nth-child(2) { width: 30%; }
.tvb-labor .tvb-table th:nth-child(3), .tvb-labor .tvb-table td:nth-child(3) { width: 12%; }
.tvb-labor .tvb-table th:nth-child(4), .tvb-labor .tvb-table td:nth-child(4) { width: 17%; }
.tvb-labor .tvb-table th:nth-child(5), .tvb-labor .tvb-table td:nth-child(5) { width: 14%; }
/* Card | Type | Discrepancy (takes the rest) | Why | RQ | P/N | Status | PO due
   (reordered 04.09 - user wanted Discrepancy right after Type, RQ moved
   after Why; widths below just followed columns 3 and 5 swapping places) */
.tvb-cards-table th:nth-child(1), .tvb-cards-table td:nth-child(1) { width: 9.5%; }
.tvb-cards-table th:nth-child(2), .tvb-cards-table td:nth-child(2) { width: 6.5%; }
.tvb-cards-table th:nth-child(4), .tvb-cards-table td:nth-child(4) { width: 15%; }
.tvb-cards-table th:nth-child(5), .tvb-cards-table td:nth-child(5) { width: 9.5%; }
.tvb-cards-table th:nth-child(6), .tvb-cards-table td:nth-child(6) { width: 11%; }
.tvb-cards-table th:nth-child(7), .tvb-cards-table td:nth-child(7) { width: 8%; }
.tvb-cards-table th:nth-child(8), .tvb-cards-table td:nth-child(8) { width: 8.5%; }
.tvb-td-desc { color: var(--text-primary); }
.tvb-td-ostatus { text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.9em; }
/* Zoomed (1.7em / 1.3em type): the fixed-width columns need more of the
   row or "10884/0288", "CHILD CARD DEPENDENCY" and the PO date clip; the
   free-text discrepancy is the one column that can give (it ellipsizes,
   full text in the title). Sized so every fixed column fits at wall's 27px
   mono on a 1920 board. */
.tv-board.focus-cards .tvb-cards-table th:nth-child(1), .tv-board.focus-cards .tvb-cards-table td:nth-child(1) { width: 11.5%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(2), .tv-board.focus-cards .tvb-cards-table td:nth-child(2) { width: 7.5%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(4), .tv-board.focus-cards .tvb-cards-table td:nth-child(4) { width: 19%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(5), .tv-board.focus-cards .tvb-cards-table td:nth-child(5) { width: 11.5%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(6), .tv-board.focus-cards .tvb-cards-table td:nth-child(6) { width: 12%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(7), .tv-board.focus-cards .tvb-cards-table td:nth-child(7) { width: 9.5%; }
.tv-board.focus-cards .tvb-cards-table th:nth-child(8), .tv-board.focus-cards .tvb-cards-table td:nth-child(8) { width: 10.5%; }
.tvb-td-card { color: var(--text-primary); }
.tvb-td-type { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.9em; }
.cat-text { text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.tvb-card-note { color: var(--text-muted); margin-left: 0.5em; }
.tvb-td-due { color: var(--text-secondary); }
.tvb-td-due.tvb-overdue { color: var(--status-critical); font-weight: 600; }
.tvb-aog { color: var(--status-critical); font-weight: 700; letter-spacing: 0.06em; margin-left: 0.5em; }

/* ---- chrome: present, never competing with the data ---- */
.tvb-chrome { flex: none; font-size: 0.7em; color: var(--text-muted); display: flex; align-items: center;
  gap: 0.4em; padding: 0 0.7vw 0.4vh; }
.tvb-chrome .refresh-btn { font-size: 1em; }

/* Wall density: at 1.7x the footnotes stop being readable and start
   costing rows, so they go and the biggest figures come down a notch. */
.tv-board.density-wall .tvb-cpi-cap,
.tv-board.density-wall .tvb-hours-cap { display: none; }
.tv-board.density-wall .tvb-cpi-val { font-size: 2.3em; }
.tv-board.density-wall .tvb-tail { font-size: 2.6em; }
.tv-board.density-wall .tvb-block-note { font-size: 0.76em; }
/* Measured, not guessed: at wall the labor table ran 64px past its row and
   the narrow hours column 104px, because the per-group used/approved pairs
   wrap at that size. Give row 2 the height back off row 1, drop the
   breakdown (the two headline hour figures stay) and widen hours a little. */
/* Same 27:31 split as before, but as flex weights over what the
   content-sized cards panel leaves, like desk - so a ?zoom= step that trims
   the card rows hands the height to these rows instead of clipping them. */
.tv-board.density-wall .tvb-row-1 { flex: 27 1 0; }
.tv-board.density-wall .tvb-row-2 { flex: 31 1 0; grid-template-columns: 1.4fr 1.85fr 1.1fr 1fr; }
.tv-board.density-wall .tvb-cards { flex: 0 0 auto; }
.tv-board.density-wall .tvb-kpi-col .tvb-hours-main { font-size: 1.7em; }  /* half-row at wall was 2px short */
.tv-board.density-wall .tvb-hours-rows { display: none; }
.tv-board.density-wall .tvb-table th { font-size: 0.78em; }
/* At wall the four sub-metric captions sit shoulder to shoulder and read as
   one run of words - give them air and a smaller cap. */
/* Four nowrap captions in a 4-track grid overran the panel by 12-16px at
   wall width; buy it back from the gap and the caption, not from the
   figures, which are the part anyone actually reads from 5 m. */
.tv-board.density-wall .tvb-submetrics { gap: 0.6em; }
.tv-board.density-wall .tvb-sub .tvb-cell-cap { font-size: 0.66em; letter-spacing: 0.04em; }
.tv-board.density-wall .tvb-dates { gap: 1em; }
/* "Behind schedule" is the one caption too long for its third of the dates
   row at wall width - it was the 12-16px the progress panel overran by.
   Let it wrap there (the cell has vertical room) rather than shrink the
   dates themselves. */
.tv-board.density-wall .tvb-dates .tvb-cell-cap { white-space: normal; font-size: 0.66em; line-height: 1.15; }
.tv-board.density-wall .tvb-dates { gap: 0.7em; }

/* ---- Phone-remote panel zoom (dashboard/tv_board.py FOCUSABLE, set via
   ?focus=labor|cards) ----
   One zone grows at the others' expense, requested by the user 2026-09-03:
   "какая-то зона увеличилась за счёт уменьшения других [...] обязательно
   смогу вернуть обратно" (toggle back via the same phone button, or the
   "reset view" link in the chrome bar).

   No magic percentages: every non-focused row/panel collapses to just its
   .tvb-panel-head (label + count), which is naturally small and scales
   with --u like everything else; flex: 0 0 auto sizes the row to that
   collapsed content, and the focused row/panel takes flex: 1 to absorb
   whatever height that freed up. Adapts automatically to density and to
   any future change in header/type size - nothing here is a hand-tuned
   number that can drift out of sync. */
.tv-board.focus-labor .tvb-row-1,
.tv-board.focus-cards .tvb-row-1,
.tv-board.focus-cards .tvb-row-2 {
  flex: 0 0 auto;
}
.tv-board.focus-labor .tvb-row-1 > .tvb-panel > *:not(.tvb-panel-head),
.tv-board.focus-cards .tvb-row-1 > .tvb-panel > *:not(.tvb-panel-head),
.tv-board.focus-cards .tvb-row-2 .tvb-panel > *:not(.tvb-panel-head) {
  display: none;
}
.tv-board.focus-labor .tvb-row-2 .tvb-panel:not(.tvb-labor) > *:not(.tvb-panel-head) {
  display: none;
}
.tv-board.focus-labor .tvb-row-2 { flex: 1; grid-template-columns: 0.4fr 5fr 0.5fr 0.5fr; }
/* The hours/CPI stack lies flat while collapsed, so the row stays one head
   tall instead of two. */
.tv-board.focus-labor .tvb-kpi-col,
.tv-board.focus-cards .tvb-kpi-col { flex-direction: row; gap: 0.7vw; }
/* Those four columns just went from ~1fr to ~0.4-0.5fr - their head line
   ("WHAT'S BLOCKING COMPLETION") no longer fits on one line at that width.
   Wrap it rather than let it push past the panel edge. */
.tv-board.focus-labor .tvb-row-2 .tvb-panel:not(.tvb-labor) > .tvb-panel-head {
  white-space: normal;
  flex-wrap: wrap;
  line-height: 1.15;
  overflow-wrap: anywhere;
  font-size: 0.72em;
  gap: 0.3em;
}
.tv-board.focus-labor .tvb-cards { flex: 0 0 auto; padding-top: 0.4em; padding-bottom: 0.4em; gap: 0; }
/* Blocking cards collapse to their head too while labor is zoomed (04.09):
   even five rows of it cost the zoomed table four of its own, and the head
   still carries the count. */
.tv-board.focus-labor .tvb-cards > *:not(.tvb-panel-head) { display: none; }
.tv-board.focus-cards .tvb-cards { flex: 1; }

/* Zoomed means readable from across the hangar, not merely "more rows at
   the same size" (user, 2026-09-04: "информация должна быть читаемая с
   большого экрана [...] растянуть под размер экрана"). The focused table
   takes a bigger em off --u; the row caps in dashboard/tv_board.py
   _FOCUS_ROW_CAPS are sized to this type, measured at 1920x1080, so the
   two must move together. Wall density is already 1.7x desk, so it gets a
   smaller boost or a row would be a foot tall. */
.tv-board.focus-labor .tvb-labor .tvb-table,
.tv-board.focus-cards .tvb-cards .tvb-table { font-size: 1.5em; }  /* was 1.7 when --u was 1.3vh; 1.5 x 1.55vh is bigger still (25px vs 24px at 1080p) */
.tv-board.density-wall.focus-labor .tvb-labor .tvb-table,
.tv-board.density-wall.focus-cards .tvb-cards .tvb-table { font-size: 1.3em; }
.tv-board.focus-labor .tvb-labor .tvb-more,
.tv-board.focus-cards .tvb-cards .tvb-more { font-size: 1.3em; }

/* A collapsed row's panels keep only their head line - drop the padding
   that was sized for a full panel, or the "row" is mostly empty air. */
.tv-board.focus-labor .tvb-row-1 > .tvb-panel,
.tv-board.focus-cards .tvb-row-1 > .tvb-panel,
.tv-board.focus-cards .tvb-row-2 .tvb-panel,
.tv-board.focus-labor .tvb-row-2 .tvb-panel:not(.tvb-labor) {
  padding-top: 0.4em;
  padding-bottom: 0.4em;
  gap: 0;
}

/* A small marker on the zoomed panel's own head, so a passer-by who has
   never touched the phone can tell this is a deliberate zoom, not a
   layout that broke. */
.tv-board.focus-labor .tvb-labor > .tvb-panel-head::after,
.tv-board.focus-cards .tvb-cards > .tvb-panel-head::after {
  content: "\25B2 " attr(data-zoom-label);
  margin-left: 0.6em;
  color: var(--series-routine);
  letter-spacing: 0.06em;
}


/* What the notifications bell left behind.

   The bell was a dropdown in the header, reusing .account-menu for its
   details/summary/panel mechanics. It is the side dock's Notifications
   button and panel now (templates/_side_panels.html and
   tasks/_notifications_panel.html): the count below hangs off the dock
   button, and everything from .tk-bell-head down styles the fragment the
   panel loads. The list is also still the shape static/tasks.css expects on
   the full /tasks/notifications page. */
.tk-bell-count {
  position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--status-critical, #e5484d); color: var(--on-accent-text, #fff);
  font-size: 10.5px; font-weight: 700; line-height: 16px; text-align: center;
}
/* .tk-bell-panel and .tk-bell-all went with the dropdown: the panel has its
   own width (--side-w) and its own "Open full page" link in the head. */
/* Right-aligned, not space-between: the row used to carry a "Notifications"
   heading on its left, and the panel's own head says that now - so all this
   holds is "Mark all read", which belongs over the list it acts on. */
.tk-bell-head { display: flex; align-items: center; justify-content: flex-end; padding: 10px 12px; border-bottom: 1px solid var(--hairline); font-size: 13px; }
.tk-bell-link { font: inherit; font-size: 12px; color: var(--series-routine); background: none; border: 0; cursor: pointer; padding: 0; }
.tk-bell-item { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; border-bottom: 1px solid var(--hairline); color: inherit; text-decoration: none; font-size: 12.5px; }
.tk-bell-item:hover { background: var(--panel-2, rgba(255,255,255,.04)); }
.tk-bell-item.unread { box-shadow: inset 3px 0 0 var(--series-routine); }
.tk-bell-kind { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); }
.tk-bell-item.unread .tk-bell-kind { color: var(--series-routine); }
.tk-bell-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tk-bell-meta { font-size: 12px; color: var(--text-secondary); font-family: var(--font-mono); }
.tk-bell-empty { padding: 14px 12px; font-size: 12.5px; color: var(--text-secondary); }


/* ---- Theme menu (sidebar, test builds) - a small popup list rather than
   the old two-state toggle. Same footprint as .sidebar-theme-toggle so the
   sidebar does not shift; the list opens upward from the bottom of the
   drawer. ---- */
.sidebar-theme-menu { position: relative; }
.sidebar-theme-menu > summary { list-style: none; }
.sidebar-theme-menu > summary::-webkit-details-marker { display: none; }
.sidebar-theme-menu > summary .icon-sun { display: none; }
:root[data-theme="light"] .sidebar-theme-menu > summary .icon-moon { display: none; }
:root[data-theme="light"] .sidebar-theme-menu > summary .icon-sun { display: block; }
/* Sidebar tokens (var(--sb-*), declared on .sidebar in the app-shell block
   above), not the shared --panel-* / --text-* set - the popup is part of the
   always-graphite panel, so it stays graphite too instead of flashing white
   whenever the active page theme is Light. */
.sidebar-theme-list {
  position: absolute; left: 8px; right: 8px; bottom: calc(100% + 4px); z-index: 30;
  display: flex; flex-direction: column; gap: 2px; padding: 4px;
  background: var(--sb-bg); border: 1px solid var(--sb-border); border-radius: var(--sb-radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .35);
}
.sidebar-theme-list button {
  display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px;
  background: none; border: none; border-radius: var(--sb-radius); text-align: left; cursor: pointer;
  color: var(--sb-muted); font-family: var(--font-body); font-size: 13px;
}
.sidebar-theme-list button:hover { background: var(--sb-hover-bg); color: var(--sb-text); }
.sidebar-theme-list button[aria-checked="true"] { color: var(--sb-text); font-weight: 600; }
.sidebar-theme-list button[aria-checked="true"]::after { content: ""; margin-left: auto; width: 7px; height: 7px; border-radius: 50%; background: var(--sb-focus); }
.theme-swatch {
  flex: none; width: 18px; height: 12px; border-radius: 3px; border: 1px solid rgba(255, 255, 255, .25);
  background: linear-gradient(90deg, var(--sw-a) 0 60%, var(--sw-b) 60%);
}
.theme-swatch-dark { --sw-a: #10151a; --sw-b: #4fb3ff; }
.theme-swatch-light { --sw-a: #f5f7fa; --sw-b: #3577e3; border-color: rgba(0, 0, 0, .18); }

/* Account page - the same two choices as large tiles. */
.acct-themes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 0 0 12px; }
.acct-theme {
  display: flex; flex-direction: column; gap: 8px; padding: 8px; cursor: pointer;
  border: 2px solid var(--border); border-radius: var(--radius-control); background: var(--panel-2);
}
.acct-theme:hover { border-color: var(--text-muted); }
.acct-theme:has(input:checked) { border-color: var(--accent-brand); }
.acct-theme input { position: absolute; opacity: 0; pointer-events: none; }
.acct-theme-preview { height: 46px; border-radius: 6px; border: 1px solid rgba(0, 0, 0, .2); background: var(--sw-a); position: relative; overflow: hidden; }
.acct-theme-preview::before { content: ""; position: absolute; left: 6px; top: 6px; width: 40%; height: 8px; border-radius: 3px; background: var(--sw-b); }
.acct-theme-preview::after { content: ""; position: absolute; left: 6px; right: 6px; bottom: 6px; height: 14px; border-radius: 3px; background: var(--sw-c); }
.acct-theme-preview.theme-swatch-dark { --sw-c: #1a2029; }
.acct-theme-preview.theme-swatch-light { --sw-c: #ffffff; }
.acct-theme span { font-size: 12.5px; color: var(--text-primary); text-align: center; }


/* Bell: a question addressed to me carries its text and a one-line reply box. */
.tk-bell-body { font-size: 12.5px; color: var(--text-secondary); white-space: normal; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.tk-bell-title { color: inherit; text-decoration: none; }
.tk-bell-form { display: flex; gap: 6px; align-items: stretch; margin: 4px 0 2px; }
.tk-bell-form textarea { flex: 1; min-width: 0; resize: none; font: inherit; font-size: 13px; padding: 6px 8px; background: var(--panel-1); border: 1px solid var(--border); border-radius: var(--radius-control); color: var(--text-primary); }
.tk-bell-form button { font: inherit; font-size: 12.5px; padding: 0 10px; border-radius: var(--radius-control); border: 1px solid var(--series-routine); background: var(--series-routine); color: var(--on-accent-text, #fff); cursor: pointer; }
.tk-bell-question:hover { background: transparent; }

/* Server-side messages after a plain form POST (tasks_routes._error_response). */
.flash-stack { margin: 0 0 12px; display: flex; flex-direction: column; gap: 6px; }
.flash { padding: 10px 14px; border-radius: var(--radius-control); font-size: 13.5px; border: 1px solid var(--border); background: var(--panel-1); color: var(--text-primary); }
.flash-error { border-color: var(--status-critical, #e5484d); background: color-mix(in srgb, var(--status-critical, #e5484d) 12%, transparent); }


/* ============================================================================
   Sections below arrived with the phone/tablet fit and the sidebar freshness
   mark, which were released to the server before the graphite sidebar above
   (revision 5 / naряд 07.09). Kept at the end of the file so the phone
   overrides come after every desk-width rule they narrow.
   ============================================================================ */

/* ---- Phone / touch fit ----
   Everything below this line exists because the primary device is a
   personal phone (user decision 2026-08-31) and the shared hangar iPad,
   and until 2026-09-05 nothing in this file said anything below 560px.
   Measured, not guessed: on a 375px iPhone /people and /admin/users ran to
   613px and 859px behind .main-content's overflow-x: clip - nothing moved,
   the right-hand columns were simply gone. See FOCUS_GROUP_MOBILE.md for
   the per-screen numbers this section answers.

   One phone breakpoint, 640px, the same one workforce.css and workforce.js
   already use - a second number would mean two layouts to keep in step.

   Kept out of the .main-content and drawer blocks on purpose:
   tests/test_nav_chrome.py reads those as text. ---- */

/* The page never drags sideways, on any document. .main-content already
   clips the app pages; the login, TV and customer pages do not extend
   _base.html and had no such rule. `clip`, not `hidden`: hidden would turn
   body into a scroll container and break position: sticky inside it. */
html, body { overflow-x: clip; }

/* The one wrapper for a table wider than a phone. Tables that fit stay as
   they are; a table that cannot fit scrolls inside this instead of being
   cut off. `.table-scroll--wide` is for the 6-8 column labor / material
   tables, whose columns would otherwise crush to one word per line. */
.table-scroll {
  overflow-x: auto; max-width: 100%;
  -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;
}
.table-scroll > table { min-width: 100%; }
.table-scroll--wide > table { min-width: 640px; }

/* Active Labor (t181, 15.09.2026) - shared by /people's per-project
   section, the project card's Labor tab and /forecast's project detail
   (_widget_personnel.html plus its identical-markup copy in people.html;
   both tables carry .labor-table so one rule reaches all three screens).
   At 1024-1280 the browser's auto table layout was sizing each column by
   its widest *word*, not its full header/value - that is exactly why
   "Logged in" / "since" and a card id like "W-24-0175-C0018" could break
   at a space or a hyphen while plenty of columns still had short numbers
   to spare. `nowrap` makes each of those an unbreakable unit, so the
   column's real minimum width is honoured; where that pushes the table
   past its container, .table-scroll (already wrapping it) scrolls
   sideways instead of crushing a word - the same fallback this table's
   own wrapper class exists for. Decided against a guessed min-width
   number or shorter header wording: the wrap was the actual cause, not a
   missing floor, and English source strings stay descriptive. Description
   (3rd column) keeps wrapping - free text, not a value that must stay
   whole. */
.labor-table th, .labor-table td { white-space: nowrap; }
.labor-table th:nth-child(3), .labor-table td:nth-child(3) { white-space: normal; }

/* "Turn your device sideways" - templates/_rotate_hint.html. A banner,
   not an overlay: the content under it still scrolls, the person who wants
   the portrait view keeps it. Hidden everywhere except a narrow portrait
   screen, so on a desktop or a landscape phone it costs nothing. */
.rotate-hint { display: none; }
@media (max-width: 640px) and (orientation: portrait) {
  .rotate-hint {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; margin: 0 0 10px;
    background: var(--panel-2); border: 1px solid var(--border);
    border-left: 3px solid var(--status-warning); border-radius: var(--radius-control);
    font-size: 13px; color: var(--text-secondary);
  }
  .rotate-hint svg { width: 22px; height: 22px; flex: none; color: var(--status-warning); }
  .rotate-hint a { color: var(--series-routine); white-space: nowrap; margin-left: auto; }
}
/* The TV board is a 1920px canvas scaled to the window; on a portrait
   tablet that is 0.43x and as unreadable as on a phone, so its hint goes
   up to 1024px. Same look - the block above is what displays it. */
@media (min-width: 641px) and (max-width: 1024px) and (orientation: portrait) {
  .rotate-hint--tablet {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; margin: 0 0 10px;
    background: var(--panel-2); border: 1px solid var(--border);
    border-left: 3px solid var(--status-warning); border-radius: var(--radius-control);
    font-size: 13px; color: var(--text-secondary);
  }
  .rotate-hint--tablet svg { width: 22px; height: 22px; flex: none; color: var(--status-warning); }
  .rotate-hint--tablet a { color: var(--series-routine); white-space: nowrap; margin-left: auto; }
}
@media print { .rotate-hint { display: none !important; } }

/* ---- Phones (<= 640px) ---- */
@media (max-width: 640px) {
  /* The frame takes a step down (t398): 8px of gutter on each side of a 375px
     screen is 4% of it spent on nothing, and 14px corners on a panel that
     wide read as a toy. 4px and 10px - the spec's own narrow-screen figures.
     Declared on :root, plain, so it outranks nothing but the plain :root
     block at the top of the file and leaves the per-theme values alone.
     The drawer keeps its full-height slide-out behaviour: every offset it
     uses is written against --frame-gap, so it simply starts 4px in. */
  :root { --frame-gap: 4px; --frame-radius: 10px; }

  /* 24px of padding a side was 13% of a 375px screen; 12px is what the
     platform's own list views use. */
  .page { padding: 12px; }
  .main-content .page.page--wide { padding: 8px; }
  .card { padding: 12px; }
  .header-widget { padding: 12px 14px; }
  .header-widget h1 { font-size: 17px; }
  .grid { gap: 12px; }
  /* Every span becomes the full row, not just span-6/4/8 at 900px. */
  .grid > [class*="span-"] { grid-column: 1 / -1; }

  /* The gap under the strip is the frame's now (t398), not a 10px of its
     own - one value between all three panels, at every width. */
  .whoami-bar { padding-left: 10px; padding-right: 10px; }

  /* Project page widgets. Fixed minimums that fit a 1100px column were the
     whole 327px a phone has left after padding, so every one gives way. */
  .forecast-card, .labor-forecast-details, .pricing-details, .cpi-tile, .pricing-tile { min-width: 0; flex-basis: 100%; }
  .cpi-tile .value, .pricing-tile .value { font-size: 28px; }
  .kpi-tile-pace .value, .kpi-tile-yesterday .value { font-size: 34px; }
  .forecast-card .big-date, .forecast-card .big-pct { font-size: 22px; }
  .blocker-row { grid-template-columns: minmax(80px, 1fr) 2fr 36px; gap: 8px; }

  /* Tabs share the row equally instead of trailing off to the right. */
  .tabs { gap: 0; }
  .tab-btn { flex: 1 1 auto; text-align: center; padding: 12px 6px; }

  /* History: the search box takes the whole row on a phone. */
  .aircraft-search { width: 100%; }
  .project-list.with-invoice a { flex-wrap: wrap; }
  /* On a phone the invoice link drops under the row instead of
     squeezing the project number (t137). */
  .project-list.with-invoice li { flex-wrap: wrap; }
  .project-list .pinvoice-link { flex: 1 0 100%; }
}

/* ---- Touch screens: phones and tablets, or any coarse pointer ----
   Two rules from the closeout and workforce passes, now app-wide:
   - inputs at 16px, because iOS Safari zooms the page on focusing anything
     smaller and does not zoom back (DEV_LOG 2026-08-28). !important because
     the login templates set 14px inline.
   - 44px is the floor for anything a finger has to hit. Search boxes were
     missing from this list - .aircraft-search (screen 7) measured ~37px at
     375px width (design pass, 12.09.2026, t234). type="search" covers every
     toolbar search field app-wide (history, tasks, devtracker); box-sizing
     is border-box globally (top of this file) so the padding these fields
     already carry is included in the 44px, not added on top, and a text
     input centers its value vertically on its own as the box grows. */
@media (max-width: 1024px), (pointer: coarse) {
  input:not([type=checkbox]):not([type=radio]):not([type=color]):not([type=range]),
  select, textarea { font-size: 16px !important; }
  .tab-btn, .refresh-btn, .back-btn, .crew-period, .acct-btn,
  .account-menu > summary, .sidebar-nav a, .sidebar-project,
  .sidebar-group-label, .pw-btn, .view-toggle-btn,
  form > button[type=submit], input[type="search"] { min-height: 44px; }
  .view-toggle-btn { min-width: 44px; }
  .view-toggle-btn { height: auto; }
  /* Exception: the home screen's quick filter already holds its search
     box, select and button to one shared 40px (static/home.css, "one
     height and one baseline" - a past bug had them disagree by a few px
     each way). Bumping just the search box to 44px here would reopen that
     exact mismatch, so it keeps its row's height instead of the app-wide
     floor; 40px is still within the ~40px target this pass measured to. */
  .home-search input[type="search"] { min-height: 40px; }

  /* Checkboxes and radios: the OS box is 13px and ignores padding, and a
     native control draws no pseudo-elements, so it cannot be given a bigger
     hit area as it is. Here it becomes the app's own control (the same look
     the devtracker's наряд box already had): 24px visible, and an invisible
     ::before 10px wider on every side, so a finger has 44px to land on -
     clicks on a pseudo-element go to its input. Measured before this: every
     bare box in a table (invoice settings "Billed", admin visibility grid,
     task roles, forecast cohort) was a 13x13 target (design pass on revision
     13, 16.09.2026). A box wrapped in a <label> was already easier to hit
     through its text; this also fixes the box itself.
     Left alone: inputs hidden on purpose and driven by their label - the
     task tabs (.tk-tab-radio) and the theme picker (.acct-theme). They keep
     position:absolute/opacity:0, and a 44px hit box would sit over the tabs.
     Desktop with a mouse keeps the native 13px box: density there is
     deliberate and a mouse needs no 44px. */
  input:is([type=checkbox], [type=radio]):not(.tk-tab-radio, .acct-theme input) {
    appearance: none; -webkit-appearance: none;
    width: 24px; height: 24px; flex: 0 0 auto;
    border: 2px solid var(--text-muted); border-radius: 5px;
    background: var(--panel-1); position: relative; vertical-align: middle; cursor: pointer;
  }
  input[type=radio]:not(.tk-tab-radio, .acct-theme input) { border-radius: 50%; }
  input:is([type=checkbox], [type=radio]):not(.tk-tab-radio, .acct-theme input)::before {
    /* -12px, not -10px: an absolute child is placed against the padding
       box, which the 2px border already shrank to 20px. */
    content: ""; position: absolute; inset: -12px;
  }
  input[type=checkbox]:not(.tk-tab-radio, .acct-theme input):checked {
    background: var(--accent-brand); border-color: var(--accent-brand);
  }
  input[type=checkbox]:not(.tk-tab-radio, .acct-theme input):checked::after {
    content: ""; position: absolute; left: 6px; top: 1px; width: 6px; height: 12px;
    border: solid var(--on-accent-text); border-width: 0 2.5px 2.5px 0; transform: rotate(45deg);
  }
  input[type=checkbox]:not(.tk-tab-radio, .acct-theme input):indeterminate {
    background: var(--panel-1); border-color: var(--accent-brand);
  }
  input[type=checkbox]:not(.tk-tab-radio, .acct-theme input):indeterminate::after {
    content: ""; position: absolute; left: 4px; top: 8px; width: 12px; height: 2.5px;
    background: var(--accent-brand); border: none; transform: none;
  }
  input[type=radio]:not(.tk-tab-radio, .acct-theme input):checked { border-color: var(--accent-brand); }
  input[type=radio]:not(.tk-tab-radio, .acct-theme input):checked::after {
    content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--accent-brand);
  }
  input:is([type=checkbox], [type=radio]):not(.tk-tab-radio, .acct-theme input):focus-visible {
    outline: 2px solid var(--accent-brand); outline-offset: 2px;
  }
  input:is([type=checkbox], [type=radio]):not(.tk-tab-radio, .acct-theme input):disabled {
    opacity: .45; cursor: default;
  }
}

/* ---- Home-screen app on a notched iPhone ----
   Only in effect with viewport-fit=cover (templates/_base.html) and when
   launched from the home screen; in a normal browser tab every inset is 0
   and these rules change nothing. */
@media (display-mode: standalone) {
  .whoami-bar { top: env(safe-area-inset-top, 0px); }
  .page {
    padding-left: max(12px, env(safe-area-inset-left, 0px));
    padding-right: max(12px, env(safe-area-inset-right, 0px));
    padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
  }
}


/* Amber timestamp beside the "Aircraft in progress" heading, rendered only
   once the list is past SIDEBAR_STALE_MINUTES (app.py). Silent the rest of
   the time on purpose: a permanent clock in the menu is noise, and the point
   here is to break the illusion that an hour-old list is current. */
.sidebar-stale {
  margin-left: auto; font-family: var(--font-mono); font-size: 10.5px;
  color: var(--status-warning); letter-spacing: 0;
}

/* The build badge in the sidebar header (templates/_base.html,
   build_identity() in app.py). align-items keeps each chip the width of its
   own text - stretch, the flex default, would turn them into two bars across
   the panel. The "+N not pushed" chip is louder than the build name on
   purpose: the name is a fact about the window, this is a warning that the
   window shows more than the office server has. */
.sidebar-brand .brand-build {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
}
.sidebar-brand .brand-ahead {
  display: inline-block; padding: 1px 5px; border-radius: 3px;
  background: var(--status-critical, #a11); color: var(--on-accent-text, #fff);
  font-size: 10px; letter-spacing: .06em;
}


/* ---- Whole-visit progress curve (production feedback items 12-13) --------
   Cumulative cards closed over every day of the visit, Total as the primary
   line with ROUTINE and NRC muted behind it. The SVG itself is built by
   dashboard/progress_chart.py; everything here is chrome around it.
   preserveAspectRatio="none" on the SVG means it stretches to the card, so
   the only height rule that matters is the one below. ---- */
#progress-curve-widget .progress-curve {
  display: block;
  width: 100%;
  height: 180px;
  margin-top: 4px;
}
.curve-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin: 2px 0 6px;
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
/* A short rule in the series' own colour rather than a dot: these are lines
   on the chart, and a legend that shows dots for lines makes the reader
   match them by elimination. */
.curve-legend-item::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 0;
  margin-right: 6px;
  vertical-align: 3px;
  border-top-style: solid;
}
.curve-legend-total { color: var(--text-primary); }
.curve-legend-total::before { border-top-width: 2.5px; border-top-color: var(--text-primary); }
.curve-legend-routine::before { border-top-width: 1.5px; border-top-color: var(--series-routine); opacity: 0.75; }
.curve-legend-nrc::before { border-top-width: 1.5px; border-top-color: var(--series-nrc); opacity: 0.75; }

/* On a phone the date labels and the y-axis numbers are the first thing to
   become an unreadable smear, and the curve's shape - the only thing this
   chart exists to show - survives a shorter box fine. */
@media (max-width: 560px) {
  #progress-curve-widget .progress-curve { height: 140px; }
  .curve-legend { font-size: 10.5px; gap: 2px 10px; }
}
