/* 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;
  /* The legibility floor (production list 11.09.2026, sec. 7; the design
     checklist's "no figure below 12 px"): the smallest size any text may
     render at. Small chrome labels point here instead of repeating 12px. */
  --fs-floor: 12px;

  /* 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: var(--fs-floor);
  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);
  /* 12px floor (NEXT_UPDATE 157/158): this eyebrow is a real section
     heading ("Whole team", "The clock codes", "What to do right now" on
     the wave-2 labor screens), not an axis tick - .sh-label's documented
     pass doesn't apply here. One rule, every .card on the dashboard. */
  font-size: var(--fs-floor);
  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: 17px; height: 17px; margin-left: 6px; border-radius: 50%;
  border: 1px solid var(--text-muted); color: var(--text-muted);
  font-size: 12px; 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: var(--z-toast); 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: var(--z-hint); 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: 12px; 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); }
/* A phone finger needs a real target and the 12px floor. Scoped to the
   phone width: on a desk the small button is right. */
@media (max-width: 767px) {
  .back-btn { display: inline-flex; align-items: center; font-size: 12px; min-height: 44px; }
}

/* ---- 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;
}
/* No project date yet: the reason, in words, where the date would be - no
   lamp and no dash (external UX review, 26.09.2026). */
.project-forecast-summary .project-forecast-pending { font-size: 13px; color: var(--text-secondary); }

.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; }
/* Last resort, so a number can never print outside the tile it belongs to:
   if the four columns still don't fit, the table scrolls inside its own
   border instead of spilling over it. */
.kpi-tile-breakdown { max-width: 100%; overflow-x: auto; }

/* ---- The KPI row answers to its own width, not only the window's
   (04.10.2026). On the project screen a section panel opened from the dock
   can sit beside the page (body.side-fits), and on a wide monitor that
   leaves the row about 500px while the window is far past the 1200px
   breakpoint above - the row kept four columns, the Hours Worked tile got
   2.5 of 5 shares (~220px), and its REMAINING column printed past the
   tile's right edge, over the panel.

   The fix is scoped to where the viewport rules leave four columns
   (min-width: 1201px), so every width they already handle looks exactly as
   before; above it, the same two layouts switch on the widget's own width.
   740px is under the narrowest row a 1201px window gives without a panel
   (~755px with the sidebar), so the desktop layout without a panel - one
   row of tiles at 1280, 1366, 1440 - is unchanged; at 740px the Hours
   Worked tile still has ~360px for a table whose minimum is ~280px
   ("NONROUTINE" plus three numbers). Review of revision 25: the first
   version used 1100px and stacked the tiles on every desktop below ~1550px.
   The id keeps the rules off the hangar copy of .kpi-row, not in a split. ---- */
#kpi-widget { container-type: inline-size; }
@media (min-width: 1201px) {
  @container (max-width: 740px) {
    #kpi-widget .kpi-row { grid-template-columns: 1.6fr 1fr; }
    #kpi-widget .kpi-tile-wide,
    #kpi-widget details.kpi-tile-donut[open] { grid-column: 1 / -1; }
  }
  @container (max-width: 560px) {
    #kpi-widget .kpi-row { grid-template-columns: repeat(2, 1fr); }
    #kpi-widget .kpi-tile-donut,
    #kpi-widget details.kpi-tile-donut[open],
    #kpi-widget .kpi-tile-wide { grid-column: 1 / -1; }
  }
}

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; }
/* Names this date as the by-hours forecast, apart from the overview's
   by-cards project date; same small-caps label as .project-forecast-label. */
.labor-forecast-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
.labor-forecast-pending { font-size: 13px; color: var(--text-secondary); 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;
}
/* Test build (owner, 02.10.2026): no line under the logo. Only the line - the
   padding and the 12px gap to the first group stay, so the first heading does
   not sit against the mark. */
.sidebar-header.is-plain { border-bottom: 0; }
/* 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: var(--fs-floor); 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: var(--fs-floor); color: var(--sb-muted);
  letter-spacing: 0.02em; font-variant-numeric: tabular-nums;
}
/* Test build: the same line ends the Admin group instead of the footer. Left
   edge in line with the group's links (.sidebar-group > a has 22px); a plain
   muted text, no hover, no pointer. */
.sidebar-revision-in-group { padding: 8px 16px 4px 22px; }

.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: var(--z-header);
  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; }
/* t495: the phone bottom nav's four slot pickers (templates/account_profile.
   html) - same ground, border and radius as .acct-field input just above,
   so a <select> reads as one of the page's own controls rather than the
   browser's bare default box (design finding, 26.09.2026). `min-height:
   44px` at phone width makes the whole control body-and-arrow a real tap
   target, not just its own font-driven line height - the same floor
   static/tech_nav.css's own .tech-nav-btn already holds the bar's own
   buttons to. */
.acct-select {
  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);
}
@media (max-width: 767px) {
  .acct-select { min-height: 44px; }
}
.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); }
/* `display: inline-flex` above beats the UA's [hidden] rule: without this the
   "Allow notifications" button stayed on screen after the browser had answered. */
.acct-btn[hidden] { display: none; }
.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-ink); 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: var(--z-drawer); 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: var(--z-drawer-backdrop);
}
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 1024px (1000px until t476) 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: 1024px) {
  /* 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: var(--z-recent);
  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; }
/* ---- "Recent (N)" popover (28.09.2026) ----
   Folded by default, the strip is one button; from tablet width up it opens
   this small list over the page (static/recent_screens.js; the phone keeps
   its bottom sheet). Fixed and outside the page flow, so opening it never
   narrows or moves the work area. Themed panel colours, not the --rb-*
   graphite: it is a panel lying on the page, not a pill on the photo. The
   rows reuse the sheet's .recent-sheet-list look, a size smaller. */
.recent-pop {
  position: fixed; z-index: var(--z-notice);
  min-width: 240px; max-width: min(360px, calc(100vw - 16px));
  overflow-y: auto; padding: 6px;
  background: var(--panel-1); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: var(--radius-card, 8px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .35);
}
.recent-pop[hidden] { display: none; }
.recent-pop-list { display: flex; flex-direction: column; gap: 2px; }
.recent-pop .recent-pop-list .recent-tab { min-height: 36px; font-size: 13.5px; }
.recent-pop .recent-pop-list .recent-tab-close { width: 32px; height: 32px; }
.recent-pop .recent-tab-close:focus-visible, .recent-pop .recent-tab:focus-visible {
  outline: 2px solid var(--series-routine, #4fb3ff); outline-offset: -2px;
}
.recent-pop-pin {
  display: block; width: 100%; margin-top: 4px; padding: 8px 12px;
  border: 0; border-top: 1px solid var(--hairline, var(--border)); border-radius: 0;
  background: none; color: var(--text-secondary); font: inherit; font-size: 12.5px;
  text-align: left; cursor: pointer;
}
.recent-pop-pin:hover { color: var(--text-primary); }
.recent-pop-pin:focus-visible { outline: 2px solid var(--series-routine, #4fb3ff); outline-offset: -2px; }
@media print { .recent-pop { display: none !important; } }
/* 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: 1024px) {
  /* 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.

   `--content-bottom-safe` (25.09.2026): the same clearance, named once as a
   property instead of repeated inside two `calc()`s. Reported overlap on
   /workforce/labor/report/monthly, /accounting/repair-open,
   /accounting/projects and /admin/visibility turned out not to be a hole in
   which selector carries the padding - `.page` covers all four templates,
   `{% block content %}` in _base.html has exactly one wrapper - but the
   room was only ever recalculated on `resize`/`pageshow`. A tab label that
   reflows after its web font swaps in, or a fold/unfold of the strip that
   changes its own height without the window changing, left the stylesheet
   holding a stale, too-small number until the next resize; these four
   pages carry the longest tables and the most Cyrillic screen names in the
   strip's own history, which is what made the stale gap visible on them
   first. `static/recent_screens.js` now re-publishes on a ResizeObserver of
   the bar itself, not just of the window, so any table or sticky element
   built off this property tracks the strip's real height instead of the
   last time the window happened to change. Kept as its own property (not
   folded into --recent-bar-h itself) so nothing here risks the 16.09.2026
   shadowing bug again - only <html> ever declares --recent-bar-h. */
body.has-recent-bar {
  --content-bottom-safe: calc(var(--recent-bar-h, 67px) - var(--frame-gap) + 12px);
}
@media (max-width: 1024px), (pointer: coarse) {
  body.has-recent-bar {
    --content-bottom-safe: calc(var(--recent-bar-h, calc(84px + env(safe-area-inset-bottom, 0px))) - var(--frame-gap) + 12px);
  }
}
body.has-recent-bar .page { padding-bottom: var(--content-bottom-safe, 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: var(--content-bottom-safe, 12px);
}
/* Keyboard/anchor scrolling (`:target`, a focused field, "jump to row")
   moves the viewport, not `.page` - a `padding-bottom` on `.page` cannot
   keep a focused control clear of the strip once the window itself has
   scrolled it to the very bottom edge. `--recent-bar-h` is already the
   full room the strip needs below the window's own bottom edge (unlike
   --content-bottom-safe, nothing here sits inside .main-content's own
   margin, so no --frame-gap correction applies), and it is 0 whenever the
   strip is empty or hidden, so this costs nothing on a page with no strip. */
html { scroll-padding-bottom: var(--recent-bar-h, 0px); }
@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; }
/* ---- One z-index scale for the app's chrome (t476) ----
   Header < recent strip < "something waiting" popup < side panels < rail <
   drawer backdrop < drawer < bottom sheet < modal < hint < toast. The
   numbers are the ones these layers already had, so a desk looks exactly as
   before; what changed is that each layer is written against its name, and
   the order is read here instead of reconstructed from nine rules. The
   sheet (static/ui_sheet.js) is the new one: above the drawer, because a
   sheet can open from inside it, and below a real modal.

   Not on this scale, on purpose: everything positioned INSIDE the page.
   `.main-content .page` carries `position: relative; z-index: 0`, which
   makes it a stacking context of its own - a sticky table head at z 5 or a
   context menu at z 40 in workforce.css / planned_works.css is ordered only
   against its neighbours in the page and can never rise over the header,
   whatever its number. The help window is a <dialog> in the browser's top
   layer and needs no number at all. */
:root {
  --z-header: 5;
  --z-recent: 6;
  --z-notice: 89;
  --z-panel: 90;
  --z-dock: 91;
  --z-drawer-backdrop: 94;
  --z-drawer: 95;
  --z-sheet: 100;
  --z-modal: 150;
  --z-hint: 200;
  --z-toast: 300;
}
.side-dock {
  position: fixed; top: var(--side-top); right: var(--frame-gap); bottom: var(--frame-gap); z-index: var(--z-dock);
  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; }
/* The project card's sections were easy to miss (external UX review,
   26.09.2026): grey 10px words on a grey rail read as window chrome, not as
   four things to open. Same buttons, same places, same width - they now
   read as a list of sections: label in the text colour and semi-bold, the
   icon in the accent, a hairline between neighbours, and a small "‹" that
   says the section opens to the left. */
.side-dock-btn { color: var(--text-primary); font-weight: 600; }
.side-dock-btn .nav-ico { color: var(--series-routine); }
.side-dock-btn + .side-dock-btn { border-top: 1px solid var(--hairline); }
.side-dock-btn[data-panel]::before {
  /* On the icon's row, clear of the label, which may wrap to full width. */
  content: "\2039"; position: absolute; left: 6px; top: 10px; line-height: 18px;
  font-size: 14px; font-weight: 400; color: var(--text-muted);
}
.side-dock-btn[aria-expanded="true"]::before { color: var(--series-routine); }
.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: var(--z-panel);
  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: var(--fs-floor); 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: var(--fs-floor); 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).
   The owner, 01.10.2026: it is the FIRST layer. It used to sit below the
   panels (--z-notice 89 < --z-panel 90), and on a project page with the
   Documents panel open it hid under the panel at the bottom right where
   nobody could reach it. Now above panels, dock, drawer, sheets and
   dialogs; only the hint and the toast are higher.
   Being on top, it must not sit on the page's own buttons: at the top right
   it covered a form's Edit / Print until closed (live check, same day). So it
   lives at the bottom right on every page, above the Recent strip and the
   phone's bottom bar (both set --recent-bar-h). */
.notice-pop {
  position: fixed; bottom: calc(var(--recent-bar-h, 0px) + 12px); right: 12px; z-index: 160;
  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: 12px; 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: 12px; 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); }
/* A page with the right-hand dock (Materials / Labor / Documents / Invoice)
   kept the pop-up under the dock: its close button was hidden and only
   "Open" showed, so the one way to get rid of it was to be taken away
   (reported 01.10.2026). It stands just left of the dock instead. */
body:has(.side-dock) .notice-pop {
  right: calc(var(--side-dock-w, 68px) + var(--frame-gap, 0px) + 12px);
  width: min(340px, calc(100vw - var(--side-dock-w, 68px) - var(--frame-gap, 0px) - 24px));
}
/* Folded (after ten quiet seconds): a round 44px badge in the same corner,
   same layer. Not an answer - a click unfolds the card again. The card's own
   children are all hidden; the badge button is the only thing left. */
.notice-pop-badge { display: none; }
.notice-pop.is-collapsed,
body:has(.side-dock) .notice-pop.is-collapsed {
  width: 44px; height: 44px; padding: 0; gap: 0; border-radius: 50%;
  align-items: center; justify-content: center;
}
.notice-pop.is-collapsed > :not(.notice-pop-badge) { display: none; }
.notice-pop.is-collapsed > .notice-pop-badge {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; padding: 0; border: 0; border-radius: 50%;
  background: none; color: var(--series-routine); cursor: pointer;
}
.notice-pop.is-collapsed > .notice-pop-badge[hidden] { display: none; }
.notice-pop-badge .notice-pop-ico { width: 20px; height: 20px; margin: 0; }
.notice-pop-badge:focus-visible { outline: 2px solid var(--accent-brand); outline-offset: 2px; }
.notice-pop-dot {
  position: absolute; top: 9px; right: 9px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--series-routine); border: 2px solid var(--panel-0); box-sizing: content-box; margin: -2px;
}
.notice-pop-n {
  position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 4px;
  box-sizing: border-box; border-radius: 9px; font-family: var(--font-mono); font-size: 12px;
  line-height: 18px; text-align: center; color: var(--panel-0); background: var(--series-routine);
}
@media (prefers-reduced-motion: no-preference) {
  .notice-pop { animation: notice-pop-swap .18s ease-out; }
}
@keyframes notice-pop-swap { from { opacity: .4; transform: scale(.92); } to { opacity: 1; transform: none; } }
@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: 18px; height: 18px; padding: 0 4px; box-sizing: border-box;
  border-radius: 9px; background: var(--status-critical, #e5484d); color: var(--on-accent-text, #fff);
  font-size: 12px; font-weight: 700; line-height: 18px; 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; }

/* Moved from chat.css (t200, 25.09.2026): the account page's sound toggle
   reuses this exact markup/class and does not load chat.css. */
.chat-switch { display: flex; align-items: center; gap: 10px; min-height: 44px; font-size: 14px; cursor: pointer; }
.chat-switch input { width: 20px; height: 20px; accent-color: var(--series-routine); }
/* t200 /account > Notifications: the four sounds, each with its own
   "Listen" button, tucked under the sound switch. */
.notify-sounds { margin: 2px 0 6px 30px; max-width: 340px; }
.notify-sound { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.notify-sound .chat-switch { min-height: 40px; }
@media (max-width: 480px) { .notify-sounds { margin-left: 0; } }


/* 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: 12px;
  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: 12px; letter-spacing: .02em;
}


/* ---- 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;
  /* Was 11.5px - under the 12px floor (CLAUDE.md "Просто для обычного
     человека"); design review, t311, 2026-09-23. */
  font-size: 12px;
  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;
}

/* ---- The Cumulative view (29.09.2026) --------------------------------
   dashboard/progress_chart.py:visit_progress_chart. Lines are a stretched
   SVG; every piece of text is HTML placed in percent of the same axis, so
   it keeps its real size at any width. Heights are fixed, so the open block
   stays about the height it always had. */
#progress-curve-widget .vp-summary {
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 14px;
  margin-left: auto; padding-left: 16px; min-width: 0; justify-content: flex-end;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em;
  text-transform: none; font-weight: 400; color: var(--text-secondary);
}
#progress-curve-widget .vp-summary b { color: var(--text-primary); font-weight: 700; font-size: 13px; }
#progress-curve-widget .vp-status {
  font-weight: 600; padding: 1px 8px; border-radius: 999px; border: 1px solid currentColor;
}
#progress-curve-widget .vp-status--bad { color: var(--status-critical-ink); }
#progress-curve-widget .vp-status--good { color: var(--status-good); }
/* Legend: one word per line, in that line's colour (owner, 29.09.2026). */
.vp-legend { margin: 0 0 4px; }
.vp-leg-closed { color: var(--series-routine); }
.vp-leg-closed::before { border-top-width: 3px; border-top-color: var(--series-routine); }
.vp-leg-scope { color: var(--series-dependency); }
.vp-leg-scope::before { border-top-width: 1.5px; border-top-color: var(--series-dependency); }
.vp-leg-required { color: var(--status-warning); }
.vp-leg-required::before { border-top-width: 2px; border-top-style: dashed; border-top-color: var(--status-warning); }
.vp-leg-bars { color: var(--series-routine); }
.vp-leg-bars::before { border-top: 0; width: 8px; height: 10px; vertical-align: -1px; background: var(--series-routine); }

/* A short visit is not stretched across the card (owner, 29.09.2026):
   the grid and axis run the full width, but the days (.vp-data) get up to
   --vp-day-w each, so the data grows with the visit and fills the card only
   once there are enough days. */
.vp {
  --vp-day-w: 64px;
  position: relative; display: grid;
  grid-template-columns: 36px 1fr; grid-template-rows: 196px 40px 22px;
  column-gap: 6px; margin-top: 18px; outline: none; touch-action: pan-y;
}
.vp:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 4px; border-radius: var(--radius-control); }
.vp-y { grid-column: 1; grid-row: 1; position: relative; }
.vp-y span {
  position: absolute; right: 0; transform: translateY(50%);
  font-family: var(--font-mono); font-size: 12px; color: var(--text-muted);
}
.vp-plot, .vp-bars, .vp-x { grid-column: 2; position: relative; }
.vp-plot { grid-row: 1; border-bottom: 1px solid var(--border); }
.vp-bars { grid-row: 2; }
.vp-x { grid-row: 3; }
.vp-data {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: min(100%, max(398px, calc(var(--vp-days, 30) * var(--vp-day-w))));
}
.vp-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.vp-svg * { vector-effect: non-scaling-stroke; }
.vp-grid { stroke: var(--border); stroke-width: 1; opacity: 0.6; }
.vp-line-closed { fill: none; stroke: var(--series-routine); stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; }
.vp-line-partial { stroke-dasharray: 4 3; opacity: 0.75; }
.vp-line-scope { fill: none; stroke: var(--series-dependency); stroke-width: 1.5; }
.vp-line-required { fill: none; stroke: var(--status-warning); stroke-width: 2; stroke-dasharray: 6 4; }
.vp-mark-today, .vp-mark-due { stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 2 3; }
line.vp-mark-due { stroke: var(--text-secondary); }
.vp-chip {
  position: absolute; top: -2px; transform: translateX(-50%); white-space: nowrap;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 0 6px; border-radius: 999px; background: var(--panel-2); color: var(--text-secondary);
  border: 1px solid var(--border); pointer-events: none;
}
.vp-chip.vp-mark-due { top: 20px; color: var(--text-primary); }
.vp-chip--end { transform: translateX(-100%); }
.vp-chip--start { transform: none; }
.vp-dot {
  position: absolute; width: 9px; height: 9px; margin: 0 0 -4.5px -4.5px; border-radius: 50%;
  background: var(--panel-2); border: 2px solid var(--series-routine); pointer-events: none;
}
.vp-dot-sel { background: var(--series-routine); }
.vp-cursor { position: absolute; top: 0; bottom: -62px; width: 0; border-left: 1px solid var(--text-secondary); pointer-events: none; }
.vp-bar { fill: var(--series-routine); opacity: 0.85; }
.vp-bar-partial { opacity: 0.45; }
.vp-bar-zero { fill: var(--text-muted); opacity: 0.45; }
.vp-tick {
  position: absolute; top: 4px; transform: translateX(-50%); white-space: nowrap;
  font-family: var(--font-mono); font-size: 12px; color: var(--text-muted);
}
.vp-tick--start { transform: none; }
.vp-tick--end { transform: translateX(-100%); }
.vp-tick--today { color: var(--text-secondary); font-weight: 600; }
.vp-tip {
  position: absolute; top: 8px; z-index: 5; min-width: 220px; max-width: 280px;
  padding: 10px 12px; border-radius: var(--radius-control); background: var(--panel-1); border: 1px solid var(--border);
  font-size: 13px; color: var(--text-primary); pointer-events: none;
}
.vp-tip h4 { margin: 0 0 6px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); }
.vp-tip dl { display: grid; grid-template-columns: 1fr auto; gap: 2px 16px; margin: 0; }
.vp-tip dt { color: var(--text-secondary); }
.vp-tip dd { margin: 0; text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.vp-tip .vp-tip-sub { padding-left: 12px; }
.vp-tip .vp-tip-gap { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 4px; }
.vp-tip .vp-bad { color: var(--status-critical-ink); }
.vp-tip .vp-grow-row { color: var(--series-dependency); }
/* "+N" where the scope grew - the scope line's own colour, just above the step. */
.vp-grow {
  position: absolute; transform: translate(-50%, -4px); white-space: nowrap; pointer-events: none;
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; color: var(--series-dependency);
}

/* A label the chart script (initVisitProgress) dropped because it would sit
   on its neighbour. Hidden, not removed: it takes no space either way, and
   the day detail still carries the number. */
.vp-declutter { visibility: hidden; }

/* 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; }
  .vp { grid-template-columns: 30px 1fr; grid-template-rows: 160px 34px 22px; }
  .vp-tick--minor { display: none; }
  #progress-curve-widget .vp-summary { margin-left: 0; padding-left: 0; width: 100%; margin-top: 4px; }
  .curve-legend { font-size: 12px; gap: 2px 10px; }
}

/* =====================================================================
   t476 - the shared mobile shell (docs/specs/t476.md)

   Three widths, decided once for the whole app: phone < 768px, tablet
   768-1023px, desk >= 1024px (where the drawer docks - see the docked block
   and static/nav.js). Everything below sits inside a max-width query, or is
   hidden by default and only shown inside one, so the desk layout is the one
   it was before this section existed. The job here is not new features -
   it is that the existing ones stop breaking on a narrow screen.
   ===================================================================== */

/* Desk defaults: the phone-only pieces do not exist at 1024px and up. */
.whoami-title, .whoami-new, .drawer-me, .recent-toggle, .mobile-notice, .rt-more-btn, .rt-primary-only { display: none; }
.mobile-notice[hidden] { display: none !important; }

/* ---- The bottom sheet (static/ui_sheet.js) ----
   The element is the dimmed backdrop; the panel sits at its bottom edge.
   Themed like any surface on the page (not graphite like the chrome): what
   it carries - a list of views, of screens, later of filters - is content
   picked from, and it has to read in all three themes the way the page
   does. Width-capped so a sheet opened on a tablet is a panel, not a
   1000px-wide strip of rows. */
.ui-sheet {
  position: fixed; inset: 0; z-index: var(--z-sheet);
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center;
  background: rgba(0, 0, 0, .5); opacity: 0; transition: opacity .2s ease;
}
.ui-sheet[hidden] { display: none; }
.ui-sheet.is-open { opacity: 1; }
.ui-sheet-panel {
  width: 100%; max-width: 640px; max-height: min(80vh, 640px);
  display: flex; flex-direction: column;
  background: var(--panel-1); color: var(--text-primary);
  border: 1px solid var(--border); border-bottom: 0; border-radius: 16px 16px 0 0;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, .35);
  /* The home indicator of a notched phone is not a place for the last row. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  transform: translateY(24px); transition: transform .22s cubic-bezier(.32,.72,0,1);
}
:root[data-theme="light"] .ui-sheet-panel { box-shadow: 0 -12px 40px rgba(27, 39, 64, .18); }
.ui-sheet.is-open .ui-sheet-panel { transform: none; }
.ui-sheet-head {
  position: relative; flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 8px 6px 16px; border-bottom: 1px solid var(--hairline);
}
/* The grabber: says "this can be pulled down" without a word (the head
   answers a downward swipe - see ui_sheet.js). */
.ui-sheet-head::before {
  content: ""; position: absolute; top: 5px; left: 50%; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 2px; background: var(--border);
}
.ui-sheet-head h2 { margin: 0; font-size: 15px; font-weight: 700; }
.ui-sheet-close {
  flex: none; width: 44px; height: 44px; padding: 0; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--text-secondary);
  background: none; border: 0; border-radius: var(--radius-pill);
}
.ui-sheet-close:hover { color: var(--text-primary); background: var(--panel-2); }
.ui-sheet-body { overflow-y: auto; overscroll-behavior: contain; padding: 8px; }
.ui-sheet-list { display: flex; flex-direction: column; gap: 2px; }
.ui-sheet-item {
  display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 12px;
  border-radius: var(--radius-control); color: var(--text-primary); text-decoration: none; font-size: 15px;
}
.ui-sheet-item:hover, .ui-sheet-item.active { background: var(--panel-2); }
.ui-sheet-item.active { font-weight: 700; }
.ui-sheet-item-mark { flex: none; width: 18px; color: var(--series-routine); font-weight: 700; }
.ui-sheet-item-name { flex: 1 1 auto; min-width: 0; }
.ui-sheet-item:focus-visible, .ui-sheet-close:focus-visible { outline: 2px solid var(--accent-brand); outline-offset: -2px; }
/* Scroll lock while a sheet is open (the class is set by ui_sheet.js). */
html.ui-sheet-lock { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .ui-sheet, .ui-sheet-panel { transition: none; }
}
@media print { .ui-sheet, .recent-toggle, .mobile-notice { display: none !important; } }

/* The recent screens as rows of the sheet. The tabs are the strip's own
   (static/recent_screens.js draws them into both), re-dressed from graphite
   capsules into themed rows: the --rb-* colours are for a pill floating on
   the photo, and pale blue on a light panel would be unreadable. */
.recent-sheet-list .recent-tab-wrap {
  display: flex; border: 0; border-radius: var(--radius-control);
  background: transparent; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none;
}
.recent-sheet-list .recent-tab-wrap:hover, .recent-sheet-list .recent-tab-wrap.active { background: var(--panel-2); }
.recent-sheet-list .recent-tab {
  flex: 1 1 auto; min-height: 48px; padding: 0 12px; gap: 10px;
  color: var(--text-primary); font-size: 15px;
}
.recent-sheet-list .recent-tab.active { font-weight: 700; }
.recent-sheet-list .recent-tab-no { background: var(--panel-2); color: var(--text-secondary); }
.recent-sheet-list .recent-tab-wrap.active .recent-tab-no { background: var(--panel-1); }
.recent-sheet-list .recent-tab-close { width: 44px; height: 44px; margin: 0; color: var(--text-secondary); }
.recent-sheet-list .recent-tab-close:hover { background: var(--panel-1); color: var(--text-primary); }

/* ---- Below desk width: phone and tablet together ---- */
@media (max-width: 1023px) {
  /* 44px is the floor for everything a finger has to hit in the chrome.
     Several of these were mouse-sized (the bell 30x28, "?" 28, the arrows
     38) - fine on a desk, a miss half the time with a thumb. */
  .whoami-bar .nav-btn, .drawer-me .nav-btn { width: 44px; height: 44px; border-radius: 22px; }
  /* Two classes for the "?": help.css loads after this file and sizes it. */
  .notice-btn, .whoami-bar .help-btn, .drawer-me .help-btn { width: 44px; height: 44px; }
  .sidebar-close { width: 44px; height: 44px; border-radius: 22px; }
  .side-panel-hide { min-height: 44px; }
  .recent-tab { min-height: 44px; }
  .recent-tab-close { width: 44px; height: 44px; }

  /* An open panel covers the whole strip, "New task" included, and the page
     under it holds still: below desk width a panel is the thing being
     worked with, not a neighbour of the page (side_dock.js sets the class).
     The drawer gets the same lock while it is an overlay; docked (1024px
     and up) it is part of the layout and locks nothing. */
  .side-panel, .side-panel[data-side-drop] { top: var(--frame-gap); }
  .side-panel[data-side-drop] { bottom: var(--frame-gap); max-height: none; }
  html.side-panel-open, html:has(> body.nav-open) { overflow: hidden; }

  /* t476 finding 1: the project page's own panels (Materials/Labor/
     Documents/Invoice/Pricing, templates/dashboard.html) render inside
     `.main-content .page`'s `{% block content %}`, unlike the app-wide
     Messages/Notifications panels (templates/_side_panels.html), which are
     plain children of <body> for exactly the reason given where they are
     mounted (_base.html: keeping them out of `.main-content`/`.page` means
     no future style on either can quietly change how a "fixed" panel
     behaves). `.page` carries `position: relative; z-index: 0` a little
     further up in this file, which makes it a stacking context of its own -
     every `--z-*` value inside it (the panel's --z-panel: 90 included) is
     ordered only against its neighbours THERE, and can never rise above
     `.whoami-bar` (--z-header: 5) or the recent strip (--z-recent: 6),
     which sit beside `.page`, not inside it. That capped the panel under
     both: at 390px its head sat behind the top strip, so "Hide ›" landed on
     the bell or the refresh button instead.

     Moving the panels' markup out of `.page` would fix this the way the
     app's own two panels already are fixed, but t476 stage 1 does not move
     DOM. So instead: while a panel is open AND the window is narrower than
     desk width, `.page` gives up the one property that makes it a stacking
     context. `z-index: auto` with `position: relative` alone creates none
     (only `position` + a real `z-index` does), so this is inert everywhere
     else - the `.page::before` backdrop band a few hundred lines up, which
     needs `.page` to stay `position: relative`, is untouched, and the
     moment `html.side-panel-open` is gone (side_dock.js clears it with the
     panel) `.page` is exactly what it was before.

     `z-index: auto` alone was not enough, caught live at 390px: `.page`
     also carries `animation: page-in .22s ease-out both` a little further
     up. An element animating `opacity` with a fill-mode that holds (`both`)
     generates its OWN stacking context for as long as that holds - nothing
     to do with position or z-index at all - so the panel stayed trapped
     even with the rule above. By the time anyone can open a panel the
     page has long since faded in, so turning the animation off here changes
     nothing on screen; it only stops it from acting as a second, invisible
     cause of the exact same bug. */
  html.side-panel-open .main-content .page { z-index: auto; animation: none; }
  /* The rail (`.side-dock`, --z-dock: 91) sits deliberately above the panel
     (--z-panel: 90) at a desk, where the two are neighbours and the rail
     stays reachable beside an open panel. Below desk width the open panel
     IS the modal surface and has to cover the rail too (spec: "above
     everything - header, recent bar, rail"), so it borrows the next free
     step on the scale for as long as it is open. Scoped to the project
     panels only: the app's two [data-side-drop] ones already sit correctly,
     outside `.page`, and never needed this. */
  html.side-panel-open .side-panel:not([data-side-drop]) { z-index: 92; }
  /* The dim layer behind it (static/side_dock.js creates one element and
     reuses it for whichever panel is open) - between the rail/header it
     covers and the panel itself. */
  .side-panel-backdrop {
    position: fixed; inset: 0; z-index: 89;
    background: rgba(0, 0, 0, .5); opacity: 0; pointer-events: none;
    transition: opacity .2s ease;
  }
  .side-panel-backdrop.is-visible { opacity: 1; pointer-events: auto; }
  @media (prefers-reduced-motion: reduce) { .side-panel-backdrop { transition: none; } }

  /* "Recent (N)": the strip folds to this one button - always on a phone,
     by choice on a tablet (static/recent_screens.js). */
  .recent-toggle {
    display: inline-flex; align-items: center; gap: 8px; flex: none;
    min-height: 44px; padding: 0 14px; cursor: pointer;
    border-radius: var(--radius-pill); border: 1px solid var(--rb-edge);
    background: var(--rb-glass); color: var(--rb-text);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    box-shadow: 0 2px 6px rgba(0, 0, 0, .22);
    font: 600 13.5px var(--font-body); white-space: nowrap;
  }
  .recent-toggle:hover { background: var(--rb-glass-hover); }
  .recent-toggle:focus-visible { outline: 2px solid #4fb3ff; outline-offset: 2px; }
  .recent-toggle-caret { color: var(--rb-muted); }
}

/* ---- Desk width: the strip is foldable here too (QA, 26.09.2026) ----
   Until now "at a desk the button is not shown" (see the comment beside
   PHONE/FOLD_KEY in static/recent_screens.js: "on a tablet ... at a desk
   the button is not shown") meant literally that: `.recent-toggle` stayed
   at its desk-default `display: none` past 1024px, and `.recent-bar.is-folded
   > .recent-tab-wrap { display: none }` existed only inside the 768-1023px
   tablet query - so `folded` in static/recent_screens.js could be true
   (read back from localStorage, same key a tablet session on the same
   device had set) with nothing on screen able to act on it: the class
   landed on .recent-bar, syncToggle() flipped the caret and the aria
   attributes, and not one chip actually left the row. Reported as "folding
   does nothing / doesn't stick" on a 1280-1440px build.

   The fix is the toggle pill beside the chips, not a replacement for them
   (unlike the tablet/phone version, which is the ONLY thing shown) - at
   desk width there is room for both, so folding is an option instead of a
   forced state. Same markup, same click handler (static/recent_screens.js
   never branched on width for the fold path, only for the phone sheet), so
   nothing there had to change. */
@media (min-width: 1024px) {
  .recent-toggle {
    display: inline-flex; align-items: center; gap: 6px; flex: none;
    height: 100%; padding: 3px 12px 3px 5px; cursor: pointer;
    border-radius: var(--radius-pill); border: 1px solid var(--rb-edge);
    background: var(--rb-glass); color: var(--rb-muted);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    font: 600 12.5px var(--font-body); white-space: nowrap;
  }
  .recent-toggle:hover { background: var(--rb-glass-hover); color: var(--rb-text); }
  .recent-toggle:focus-visible { outline: 2px solid #4fb3ff; outline-offset: 1px; }
  .recent-toggle-caret::before { content: "\2039"; }
  .recent-bar.is-folded .recent-toggle-caret::before { content: "\203A"; }
  /* Folded, only the toggle stays in the strip's own flex row - the tabs
     are still built into the sheet too (recent_screens.js draws both), but
     the sheet itself only opens on a phone, so a folded desk session simply
     has nothing to reopen them with except unfolding again. That is the
     point: "collapsed" at a desk means "out of the way", not "moved
     somewhere else to look for". */
  .recent-bar.is-folded > .recent-tab-wrap { display: none; }
}

/* ---- Tablet: 768-1023px ---- */
@media (min-width: 768px) and (max-width: 1023px) {
  /* One row, never the two it wrapped into under 900px: two rows of 44px
     controls would be ~110px of strip on a screen this height. With the
     view tabs scrolling inside their pill the row is ~64px (spec: <= 88). */
  .app-main .whoami-bar { flex-wrap: nowrap; }
  .app-main .whoami-right { order: 0; width: auto; }
  /* The role is in the account menu; the name alone still answers "whose
     session is this" on a shared tablet, and the tabs get the room. */
  .whoami-bar .whoami-role { display: none; }

  .recent-toggle-caret::before { content: "\2039"; }
  .recent-bar.is-folded .recent-toggle-caret::before { content: "\203A"; }
  .recent-bar.is-folded > .recent-tab-wrap { display: none; }

  .mobile-notice--tablet { display: block; }
  html.mobile-gated .mobile-notice--tablet ~ * { display: none !important; }

  /* t476 finding 1: the project panel as a drawer from the right - at most
     ~420px (about the width the desktop layout gives Documents, its
     narrowest panel), full width if the screen itself is narrower. The
     desktop `right` (clear of the rail, plus two gaps) and `width` (up to
     600-900px) both belong to a layout with room for a rail-width neighbour;
     here the panel is the modal surface, so it takes the frame's own gap
     on the right and however much of the screen it needs, capped at 420. */
  .side-panel:not([data-side-drop]) {
    right: var(--frame-gap); width: min(420px, calc(100vw - var(--frame-gap) * 2));
    max-width: calc(100vw - var(--frame-gap) * 2);
  }
}

/* ---- Phone: under 768px ---- */
@media (max-width: 767px) {
  /* The strip is one 56px row: ☰, the screen's name (or, on a task view,
     the view picker - tasks.css), the tasks bell, "New task". Back/forward,
     "?", "Signed in as", the account menu and (QA wave 2, 390px: a second
     bell crowded the view picker's name down to a sliver, e.g. "Вид:
     Доск…") the messages bell (`.notice-btn[data-panel="messages"]`, a
     test-build-only icon - never on the office server) are all in the
     drawer instead (static/nav.js moves them there and back). `.app-main`
     for specificity: tasks.css loads after this file and wraps the strip
     under 900px. */
  .app-main .whoami-bar { flex-wrap: nowrap; gap: 6px; min-height: 56px; padding: 5px 8px; }
  .app-main .whoami-right { order: 0; width: auto; gap: 4px; }
  /* Without scripting nothing moves to the drawer; the name is then the
     part that goes, so the row still fits. */
  .whoami-bar .whoami-who { display: none; }
  :root { --side-top: calc(56px + var(--frame-gap) * 2); }

  .whoami-title {
    display: block; flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 15px; font-weight: 600; color: var(--tb-text);
  }
  /* On a task view the picker names the screen instead (tasks.css). */
  .whoami-bar:has(.tk-view-pick) .whoami-title { display: none; }

  .whoami-new {
    display: inline-flex; align-items: center; justify-content: center; gap: 4px; flex: none;
    min-width: 44px; min-height: 44px; padding: 0 12px;
    border-radius: var(--radius-pill); text-decoration: none; white-space: nowrap;
    background: var(--series-routine); color: var(--on-accent-text);
    font-size: 14px; font-weight: 700;
  }
  .whoami-new > span[aria-hidden] { font-size: 18px; line-height: 1; }

  /* The project card's refresh keeps its place, as an icon: the stamp beside
     it is on the card itself, and the words would push the bell off the row.
     font-size 0 keeps "↻ Refresh" as the button's accessible name. */
  .topbar-refresh { margin-left: 0; }
  .topbar-refresh > :not(.refresh-btn) { display: none; }
  .topbar-refresh .refresh-btn {
    width: 44px; height: 44px; padding: 0; font-size: 0; border-radius: var(--radius-pill);
    background: var(--tb-hover); color: var(--tb-text); border-color: var(--tb-border);
  }
  .topbar-refresh .refresh-btn::before { content: "\21BB"; font-size: 18px; }

  /* What moved into the drawer: a block at the top of its scrolling part,
     on the drawer's own graphite. The --tb-* tokens are pointed at the
     drawer's --sb-* ones, because the moved controls are styled for the top
     strip and read those. */
  body.drawer-me-on .drawer-me {
    display: flex; flex-direction: column; gap: 10px;
    padding: 0 12px 12px; margin: 0 0 10px; border-bottom: 1px solid var(--sb-border);
    --tb-bg: var(--sb-bg); --tb-text: var(--sb-text); --tb-muted: var(--sb-muted);
    --tb-hover: var(--sb-hover-bg); --tb-border: var(--sb-border);
  }
  .drawer-me-nav { display: flex; gap: 8px; align-items: center; }
  body.app-bg .drawer-me .nav-btn {
    background: var(--sb-hover-bg); color: var(--sb-text); border-color: var(--sb-border);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .drawer-me .whoami-who { font-size: 13px; color: var(--sb-muted); }
  .drawer-me .whoami-who strong { color: var(--sb-text); }
  .drawer-me .whoami-prefix { display: inline; }
  .drawer-me .account-menu { margin-left: 0; }
  .drawer-me .account-menu > summary { min-height: 44px; }
  .drawer-me .account-menu > summary .account-menu-label { display: inline; }
  /* The menu opens in place, pushing the drawer's list down, instead of
     hanging as a popover out of a panel that scrolls. */
  .drawer-me .account-menu-panel {
    position: static; min-width: 0; max-width: none; margin-top: 6px;
    background: var(--sb-hover-bg); border-color: var(--sb-border); box-shadow: none;
  }
  .drawer-me .account-menu-head strong { color: var(--sb-text); }
  .drawer-me .account-menu-head .account-menu-email { color: var(--sb-muted); }
  .drawer-me .account-menu-panel a,
  .drawer-me .account-menu-panel button.account-menu-logout { min-height: 44px; color: var(--sb-text); }
  .drawer-me .account-menu-panel a:hover,
  .drawer-me .account-menu-panel a.active,
  .drawer-me .account-menu-panel button.account-menu-logout:hover { background: var(--sb-active-bg); color: var(--sb-text); }

  /* The bell's and the messages' panels take the whole screen: the base
     .side-panel cap leaves room for a rail these two never stand beside, and
     on a phone that room was the strip's right end - "New task" stayed
     uncovered and live beside an open panel. */
  .side-panel[data-side-drop] { left: var(--frame-gap); right: var(--frame-gap); width: auto; max-width: none; }

  /* t476 finding 1: the project panel is a full-screen layer here, not a
     drawer beside a rail that no longer has the width to stand next to
     anything - same edge-to-edge treatment as the app's two panels just
     above, just without their own `[data-side-drop]` geometry (this one
     still slides in from the side, not drops from a corner icon). */
  .side-panel:not([data-side-drop]) {
    left: var(--frame-gap); right: var(--frame-gap);
    width: auto; max-width: none;
  }

  /* The strip is the one button; the tabs are in the sheet. */
  .recent-bar > .recent-tab-wrap { display: none; }
  .recent-toggle-caret::before { content: "\25B4"; }

  .mobile-notice--phone { display: block; }
  html.mobile-gated .mobile-notice--phone ~ * { display: none !important; }
}

/* ---- "Made for a large screen" (templates/_base.html) ----
   A plain card, not an error: the screen is fine, the width is not. */
.mobile-notice {
  margin: 0 0 16px; padding: 16px;
  background: var(--panel-1); border: 1px solid var(--border);
  border-left: 3px solid var(--series-routine); border-radius: var(--radius-card);
}
.mobile-notice h2 { margin: 0 0 6px; font-size: 16px; }
.mobile-notice p { margin: 0 0 14px; font-size: 14px; line-height: 1.45; color: var(--text-secondary); }
.mobile-notice-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.mobile-notice-actions .acct-btn { min-height: 44px; }
.mobile-notice-open {
  min-height: 44px; padding: 0 14px; cursor: pointer;
  font: 600 13.5px var(--font-body); color: var(--text-primary);
  background: none; border: 1px solid var(--border); border-radius: var(--radius-control);
}
.mobile-notice-open:hover { background: var(--panel-2); }
.mobile-notice-open[hidden] { display: none; }

/* ---- /projects board (test builds; templates/project_list.html, the
   `board` branch). One block, `.projects-*` only. Phone: single-column cards;
   768-1279px: three columns (aircraft/project | location | delivery/status);
   1280px+: a five-column table-like grid. Bottom clearance for the phone nav
   and the Recent bar is the app-wide `.page` padding (tech_nav.css, style.css
   `--content-bottom-safe`) - nothing is added for it here. ---- */
.projects-page { min-width: 0; overflow-x: clip; }
.projects-board, .projects-head-card, .projects-bar, .projects-more { min-width: 0; }
.projects-head-card h1 { margin: 0 0 4px; }
.projects-head-card, .projects-bar { margin-bottom: 8px; }
/* Capped, left-aligned: on a 1920px monitor the five columns would otherwise stretch to 1600px. */
.projects-head-card, .projects-board, .projects-more { max-width: 1400px; }
.projects-calculating { font-style: italic; }
/* Keep the 40px hit area of an info dot without moving anything around it. */
.projects-chip .hint, .projects-nostatus .hint { position: relative; }
.projects-chip .hint::after, .projects-nostatus .hint::after { content: ""; position: absolute; inset: -12px; }
.projects-sub { margin: 0; color: var(--text-secondary); font-size: 14px; }

.projects-strip { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.projects-chip { display: inline-flex; align-items: center; }
.projects-chip a {
  display: inline-flex; align-items: center; gap: 6px; min-height: 40px; box-sizing: border-box;
  padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius-pill);
  color: var(--text-primary); text-decoration: none; font-size: 14px; background: transparent;
}
.projects-chip a:hover { background: var(--panel-2); }
.projects-chip { --chip-tone: var(--series-routine); }
.projects-chip--behind { --chip-tone: var(--status-critical); }
.projects-chip--stalled { --chip-tone: var(--status-warning); }
.projects-chip--on_track { --chip-tone: var(--status-good); }
.projects-chip.is-current a {
  background: var(--panel-2); background: color-mix(in srgb, var(--chip-tone) 18%, transparent);
  border: 2px solid var(--chip-tone); padding: 5px 11px; font-weight: 700;
}
.projects-chip.is-current .projects-chip-count { font-weight: 800; }
.projects-chip-count { font-family: var(--font-mono); font-weight: 700; }
.projects-chip-label { color: var(--text-secondary); }
.projects-chip.is-current .projects-chip-label { color: var(--text-primary); font-weight: 700; }
.projects-chip .hint { margin-left: 4px; }
.projects-glyph { font-size: 12px; line-height: 1; }
.projects-chip--behind .projects-glyph, .projects-pill--behind .projects-glyph { color: var(--status-critical-ink); }
.projects-chip--stalled .projects-glyph, .projects-pill--stalled .projects-glyph { color: var(--status-warning); }
.projects-chip--on_track .projects-glyph, .projects-pill--on_track .projects-glyph { color: var(--status-good); }
.projects-nostatus { margin: 8px 0 0; font-size: 13px; color: var(--text-secondary); }
.projects-nostatus a { color: var(--text-secondary); }
.projects-nostatus a[aria-current] { color: var(--text-primary); font-weight: 600; }

.projects-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; margin-top: 12px; }
.projects-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.projects-field--q { flex: 1 1 240px; }
.projects-field--sort { flex: 0 1 200px; }
.projects-field-label { font-size: 12px; color: var(--text-secondary); }
.projects-field input, .projects-field select {
  box-sizing: border-box; width: 100%; min-height: 44px; padding: 8px 10px; font: inherit; font-size: 14px;
  color: var(--text-primary); background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-control);
}
.projects-apply {
  min-height: 44px; padding: 8px 18px; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  color: var(--text-primary); background: var(--panel-2); border: 1px solid var(--text-secondary); border-radius: var(--radius-control);
}
.projects-apply:hover { background: var(--panel-1); }
.projects-field input:focus-visible, .projects-field select:focus-visible, .projects-apply:focus-visible,
.projects-chip a:focus-visible, .projects-row:focus-visible, .projects-reset:focus-visible,
.projects-retry:focus-visible, .projects-more-toggle:focus-visible, .projects-nostatus a:focus-visible, .projects-result:focus-visible {
  outline: 2px solid var(--series-routine); outline-offset: 2px;
}
.projects-result-row { display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center; margin-top: 10px; }
.projects-result { margin: 0; font-size: 13px; color: var(--text-secondary); }
.projects-result:focus { outline: none; }
.projects-result:focus-visible { outline: 2px solid var(--series-routine); outline-offset: 2px; }
.projects-active {
  margin: 0; font-size: 14px; color: var(--text-primary); display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center;
  padding: 6px 8px 6px 12px; border: 1px solid var(--series-routine); border-left-width: 4px; border-radius: var(--radius-control);
  background: var(--panel-2); background: color-mix(in srgb, var(--series-routine) 12%, transparent);
}
.projects-active-label { font-weight: 700; }
.projects-tag { padding: 2px 10px; border-radius: var(--radius-pill); background: var(--panel-2); color: var(--text-primary); font-weight: 600; border: 1px solid var(--border); overflow-wrap: anywhere; }
.projects-reset, .projects-retry { color: var(--series-routine); font-weight: 600; }
.projects-active .projects-reset {
  display: inline-flex; align-items: center; min-height: 36px; padding: 0 14px; box-sizing: border-box; text-decoration: none;
  border: 1px solid var(--series-routine); border-radius: var(--radius-control); background: var(--panel-1);
}
.projects-active .projects-reset:hover { background: var(--panel-2); }
@media (max-width: 767px) { .projects-active .projects-reset { min-height: 44px; } }
.projects-updating { margin: 8px 0; font-size: 13px; color: var(--text-secondary); }
.projects-updating[hidden] { display: none; }
.projects-board.is-updating .projects-rows { opacity: 0.5; transition: opacity 0.15s; pointer-events: none; }

.projects-alert { border-left: 3px solid var(--status-critical); }
.projects-alert p { margin: 0 0 8px; color: var(--status-critical-ink); }
.projects-nomatch { text-align: center; }
.projects-nomatch-title { margin: 0; font-weight: 600; }

.projects-rows { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 8px; }
.projects-cols { display: none; }
.projects-row {
  display: grid; box-sizing: border-box; min-height: 44px; min-width: 0; align-items: center;
  grid-template-columns: minmax(0, 1fr) auto 20px;
  grid-template-areas: "tail status chev" "proj proj chev" "loc loc chev" "del del chev";
  gap: 2px 12px; padding: 12px 12px 12px 14px; text-decoration: none; color: var(--text-primary);
  background: var(--panel-1); border: 1px solid var(--border); border-left: 3px solid var(--hairline);
  border-radius: var(--radius-control); box-shadow: var(--card-shadow);
}
.projects-row:hover { border-left-color: var(--series-routine); }
body.app-bg.app-main .page-blend .projects-row { background: var(--glass-card); border-color: var(--glass-edge); border-left-color: var(--hairline); }
body.app-bg.app-main .page-blend .projects-row:hover { background: var(--glass-card-hover); border-left-color: var(--series-routine); }
.pr-tail { grid-area: tail; font-family: var(--font-mono); font-weight: 700; font-size: 17px; overflow-wrap: anywhere; min-width: 0; }
.pr-proj { grid-area: proj; font-size: 13px; color: var(--text-secondary); overflow-wrap: anywhere; min-width: 0; }
.pr-loc { grid-area: loc; font-size: 14px; overflow-wrap: anywhere; min-width: 0; }
.pr-del { grid-area: del; font-size: 14px; display: flex; flex-wrap: nowrap; white-space: nowrap; gap: 4px 8px; align-items: center; min-width: 0; }
.pr-status { grid-area: status; justify-self: end; }
.pr-chev { grid-area: chev; color: var(--text-muted); font-size: 22px; line-height: 1; justify-self: end; }
.pr-unset { color: var(--text-muted); }
.pr-overdue { font-size: 12px; font-weight: 600; color: var(--status-critical-ink); border: 1px solid var(--status-critical); border-radius: var(--radius-pill); padding: 1px 8px; }
.projects-pill {
  display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; font-size: 12.5px; font-weight: 600; white-space: nowrap;
  border: 1px solid var(--border); border-radius: var(--radius-pill); background: var(--panel-2); color: var(--text-primary);
}
.projects-only-mid, .projects-only-wide { display: none; }

.projects-more { padding: 10px 16px; }
.projects-more-toggle {
  cursor: pointer; display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: center; min-height: 44px; width: 100%;
  box-sizing: border-box; padding: 0; margin: 0; font: inherit; color: inherit; text-align: left; background: none; border: 0;
}
.projects-more-toggle[hidden], .projects-more-panel[hidden] { display: none; }
.projects-more-toggle::after { content: "▾"; margin-left: auto; color: var(--text-secondary); }
.projects-more-toggle[aria-expanded="true"]::after { content: "▴"; }
.projects-more-title { font-weight: 600; }
.projects-more-preview { font-size: 13px; color: var(--text-secondary); }
.projects-more-toggle[aria-expanded="true"] .projects-more-preview { display: none; }
.projects-more-list { margin: 8px 0 0; display: flex; flex-wrap: wrap; gap: 8px 28px; }
.projects-more-list div { display: flex; gap: 8px; align-items: baseline; }
.projects-more-list dt { color: var(--text-secondary); font-size: 13px; }
.projects-more-list dd { margin: 0; font-family: var(--font-mono); font-weight: 700; }

@media (min-width: 768px) {
  .projects-controls { flex-wrap: nowrap; }
  .projects-field--q { flex: 1 1 0; min-width: 0; }
  .projects-field--sort { flex: 0 0 200px; }
  .projects-apply { flex: 0 0 auto; }
  .projects-only-mid { display: inline; }
  .projects-cols, .projects-row {
    display: grid; gap: 2px 20px; padding-left: 14px; padding-right: 12px; box-sizing: border-box;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.1fr) 20px;
  }
  .projects-cols {
    grid-template-areas: "tail loc del chev"; padding-top: 8px; padding-bottom: 8px; margin: 0 0 8px;
    font-size: 12px; color: var(--text-secondary); border: 1px solid var(--glass-edge, var(--border)); border-left-width: 3px;
    border-left-color: transparent; border-radius: var(--radius-control); background: var(--glass-card);
  }
  .pc-tail { grid-area: tail; } .pc-loc { grid-area: loc; } .pc-del { grid-area: del; }
  .pc-proj, .pc-status { display: none; }
  .projects-row { grid-template-areas: "tail loc del chev" "proj loc status chev"; }
  .pr-status { justify-self: start; margin-top: 4px; }
}
@media (min-width: 1280px) {
  .projects-only-mid { display: none; } .projects-only-wide { display: inline; }
  .projects-rows { gap: 6px; }
  .projects-cols, .projects-row {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.1fr) minmax(0, 1.3fr) minmax(0, 1.3fr) 112px 20px;
  }
  .projects-cols { grid-template-areas: "tail proj loc del status chev"; }
  .pc-proj, .pc-status { display: block; } .pc-proj { grid-area: proj; } .pc-status { grid-area: status; }
  .projects-row { grid-template-areas: "tail proj loc del status chev"; padding-top: 8px; padding-bottom: 8px; min-height: 48px; }
  .pr-status { margin-top: 0; justify-self: start; }
  .pr-proj { font-size: 14px; color: var(--text-primary); }
}
@media (max-width: 480px) {
  .projects-strip { gap: 3px; flex-wrap: nowrap; }
  .projects-chip a { padding: 6px 6px; gap: 3px; font-size: 12px; min-height: 40px; }
  .projects-chip-count { font-size: 12px; }
  .projects-chip .hint { margin-left: 1px; width: 14px; height: 14px; }
  .projects-glyph { font-size: 9px; }
  .projects-bar { padding: 12px; }
  .projects-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 8px; margin-top: 8px; align-items: end; }
  .projects-field--q { grid-column: 1 / -1; }
  .projects-field--q .projects-field-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .projects-field--sort { flex: none; }
  .projects-result-row { margin-top: 6px; }
  .projects-nostatus { margin-top: 4px; }
  .projects-chip a { white-space: nowrap; }
}
/* Bottom clearance on phones/tablets: the Recent pill (--recent-bar-h) and,
   under 768px, the bottom nav (--tn-h, tech_nav.css; 58px fallback) plus the
   safe-area inset - recent_screens.js measures --recent-bar-h from the
   window bottom, so it already includes both; only a small gap is added.
   Scoped to this page; specificity raised to beat the app-wide `.page`
   bottom-padding rules. */
@media (max-width: 1023px) {
  html body[class][class] .main-content .page.projects-page {
    padding-bottom: calc(var(--recent-bar-h, 84px) + 16px);
  }
  .projects-page .pr-tail, .projects-page .pr-proj { overflow-wrap: anywhere; }
}
/* 360-379: four chips no longer fit on one line - wrap the strip rather
   than break a label ("On / track") inside its chip. */
@media (max-width: 379px) {
  .projects-strip { flex-wrap: wrap; row-gap: 4px; }
}

/* ---- Compact page head (owner, 30.09.2026; test build only) -----------------
   On most screens the top of the page used to spend 350-400 px before the
   content: a 32 px bare <h1> with browser margins, the "Wings data as of"
   line, a row of date / filter buttons, a row of big stat tiles, hint lines.
   Here the same things - nothing hidden, nothing deleted - are gathered into
   one or two rows:

     row 1  [back] Title  stamp   date nav   filters        actions ->
     row 2  stat strip / hint line

   Switch: templates/_base.html puts `head-compact` on <body> only in a test
   build (app.py _inject_compact_head), so production keeps the old spacing
   and every rule below starts with `.head-compact`. A page opts in with
   {% set compact_page = true %} (the `hc` class on .page - labor_board,
   closeout, chat and the other excluded screens never get it, so shared
   rules such as .wf-kpis cannot reach them) and wraps its own top in
   <div class="page-head"> behind {% if compact_head %}.

   Desktop only: the phone layout (<768 px) already has its own one-row top
   strip and a plain block flow in .page-head, so nothing here applies there.

   To reuse on another screen: wrap h1 + stamp + toolbar (+ hint) in
   .page-head; put stat tiles in `.ph-strip` or keep `.wf-kpis` / `.kpi-row`
   right under it. A <p> that is a direct child of .page-head becomes the
   hint line (row 2), a .wf-toolbar becomes the growing middle of row 1. */
@media (min-width: 768px) {
  body.head-compact .main-content .page.hc { padding-top: 12px; }

  .head-compact .page-head {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 6px 12px; margin: 0 0 10px;
  }
  /* A long title may shrink and wrap inside the row instead of running past
     the edge (flex-shrink is on; it only shrinks once it is alone on a line). */
  .head-compact .page-head > h1 {
    flex: 0 1 auto; min-width: 0; margin: 0; font-size: 19px; line-height: 1.25; font-weight: 700;
    overflow-wrap: anywhere;
  }
  /* The "Wings data as of ..." stamp, inline next to the title. The stamp
     partial carries an inline margin, hence !important. */
  .head-compact .page-head > .cache-note {
    order: 0; flex: none; margin: 0 !important; font-size: var(--fs-floor); white-space: nowrap;
  }
  /* Date navigation / filters / actions: the toolbar takes the rest of row 1
     and wraps inside itself; its .spacer still pushes the actions right. */
  .head-compact .page-head > .wf-toolbar,
  .head-compact .page-head > .wf-mode-tabs,
  .head-compact .page-head > .wf-actions {
    flex: 1 1 auto; min-width: 0; margin: 0; gap: 6px 8px;
  }
  .head-compact .page-head > .wf-btn { flex: none; }
  /* Hint lines, banners and notes: a full row of their own, tight. They keep
     their place in the source order (no `order`), so Tab walks the head in the
     order the eye does; one that directly follows the title shares its row
     (below), the rest are rows of their own. */
  .head-compact .page-head > p,
  .head-compact .page-head > .meta,
  .head-compact .page-head > .wf-now-banner,
  .head-compact .page-head > .wf-error {
    flex: 1 1 100%; margin: 0 !important;
    font-size: 13px; color: var(--text-secondary);
  }
  .head-compact .page-head > h1 + p,
  .head-compact .page-head > h1 + .meta { flex: 1 1 280px; min-width: 0; }
  .head-compact .page-head > h1 + .wf-now-banner,
  .head-compact .page-head > h1 + .wf-error { flex: 1 1 auto; min-width: 0; }
  .head-compact .page-head > .wf-now-banner { padding: 6px 12px; color: var(--text-primary); }

  /* Stat tiles as one compact strip: value and label side by side. */
  .head-compact .ph-strip,
  .head-compact .hc .wf-kpis { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 0 0 10px; }
  .head-compact .hc .wf-kpi,
  .head-compact .ph-strip > * {
    display: flex; align-items: baseline; gap: 8px;
    min-width: 0; padding: 5px 12px;
    border: 1px solid var(--border); border-radius: var(--radius-control);
  }
  .head-compact .hc .wf-kpi .v { font-size: 18px; line-height: 1.3; }
  .head-compact .hc .wf-kpi .l { line-height: 1.3; }
  .head-compact .hc .kpi-row { gap: 8px; margin-bottom: 10px; }
  .head-compact .hc .kpi-row > .kpi-tile {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; padding: 6px 12px;
  }
  .head-compact .hc .kpi-row > .kpi-tile .label { margin: 0; }
  .head-compact .hc .kpi-row > .kpi-tile .value { font-size: 18px; }
  .head-compact .hc .kpi-row > .kpi-tile .sub { margin: 0; }

  /* ---- Second pass (owner, 30.09.2026): the live screens that are not
     workforce/admin - the placard of People and the project card, the Open
     projects board, History. Same switch, same rules; pages of other
     families add their own block at the end of their own file (home.css,
     hangar.css, invoice.css, tasks.css). */
  /* A heading with an inline margin (History, Hangar) must not leave 4 px
     under the row. */
  .head-compact .page-head > h1[style*="margin-bottom"] { margin-bottom: 0 !important; }
  /* History: the count stays beside the aircraft name, not on a row of its own. */
  .head-compact .hist-head > p { order: 0; flex: 1 1 auto; }
  .head-compact .hc .aircraft-search { margin-bottom: 8px; }

  /* The header placard (People, the project card): same card, tighter. */
  .head-compact .hc .header-widget {
    align-items: center; gap: 4px 16px; padding: 8px 16px; margin-bottom: 10px;
  }
  .head-compact .hc .header-widget h1 { font-size: 19px; line-height: 1.25; }

  /* Open projects: title card and status/search bar become two tight rows. */
  .head-compact .projects-head-card {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 14px;
    padding: 8px 16px; margin-bottom: 8px;
  }
  .head-compact .projects-head-card h1 { margin: 0; font-size: 20px; line-height: 1.25; }
  .head-compact .projects-head-card .projects-sub { font-size: 13px; }
  .head-compact .projects-bar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
    padding: 8px 12px; margin-bottom: 8px;
  }
  .head-compact .projects-bar .projects-strip { gap: 6px; }
  .head-compact .projects-nostatus { margin: 0; }
  /* Row 1: status chips and the result count; row 2: search, sort, Apply. One
     row would not fit beside the chips at 1280-1440. */
  .head-compact .projects-controls { order: 3; flex: 1 1 100%; margin-top: 0; align-items: center; gap: 6px 8px; }
  .head-compact .projects-field { flex-direction: row; align-items: center; gap: 8px; }
  .head-compact .projects-field--q { flex: 1 1 0; min-width: 0; }
  .head-compact .projects-field--sort { flex: 0 0 auto; }
  .head-compact .projects-field--sort select { width: auto; min-width: 150px; }
  .head-compact .projects-field-label { flex: none; white-space: nowrap; }
  .head-compact .projects-result-row { order: 2; margin: 0 0 0 auto; flex: 0 1 auto; }
}

@media (min-width: 1024px) and (pointer: fine) {
  /* The smaller buttons, fields and chips of the compact head are for a mouse on
     a desktop-sized window only (the app-wide 44 px floor at 1024 px and below,
     and on every touch screen, stays). A date field, a select and the Show button
     of a toolbar are one height with its buttons. */
  .head-compact .page-head .wf-btn { padding: 6px 12px; }
  .head-compact .page-head > .wf-toolbar input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),
  .head-compact .page-head > .wf-toolbar select { box-sizing: border-box; height: 31px; padding: 4px 10px; }
  .head-compact .page-head > .wf-toolbar > button[type="submit"],
  .head-compact .page-head > .wf-toolbar > input[type="search"] { min-height: 31px; }
  .head-compact .hc .aircraft-search { min-height: 36px; padding-top: 4px; padding-bottom: 4px; }
  .head-compact .projects-chip a { min-height: 34px; padding: 3px 12px; }
  .head-compact .projects-chip.is-current a { padding: 1px 11px; }
  .head-compact .projects-field input, .head-compact .projects-field select { min-height: 36px; padding: 4px 10px; }
  .head-compact .projects-apply { min-height: 36px; padding: 4px 16px; }
}

@media (min-width: 768px) {
  /* The pop-up for an unread message hangs at the top right, which is where row 1
     of a compact head keeps its buttons (it does not close by itself). On a screen
     with the compact head it sits at the bottom right instead, above the Recent
     strip, so every control in the head stays clickable while it is open. The
     screens without the compact head keep it where it always was. */
  .head-compact:has(.page.hc) .notice-pop { top: auto; bottom: calc(var(--recent-bar-h, 0px) + 12px); }
}
