/* ==== THEME TOKENS: generated from the pre-theme stylesheet; see
   docs/design/LIGHT_THEME.md before adding a colour. ==================

   Every colour in this file is a custom property. :root holds the dark theme
   (the original look, value for value); html[data-theme="light"] overrides
   what the light theme changes. theme.js sets data-theme on <html> before
   first paint from the person's saved choice.

   Adding a colour: use an existing token if one fits the role. If none does,
   add a token here with BOTH a dark and a light value. A bare hex or rgba()
   outside this block fails scripts/frontend-smoke.sh (theme gate). */

:root {
  /* Colour channels. Used as rgba(var(--rgb-x), <alpha>): the alpha carries the role (hairline, tint, border) and stays put; the channel is what a theme swaps. */
  --rgb-slate: 148, 163, 184;
  --rgb-overlay: 255, 255, 255;
  --rgb-mist: 127, 145, 170;
  --rgb-green: 89, 209, 100;
  --rgb-green-2: 34, 197, 94;
  --rgb-emerald: 52, 211, 153;
  --rgb-blue: 96, 165, 250;
  --rgb-red: 239, 68, 68;
  --rgb-red-soft: 248, 113, 113;
  --rgb-red-deep: 220, 38, 38;
  --rgb-amber: 234, 179, 8;
  --rgb-orange: 245, 158, 11;
  --rgb-yellow: 251, 191, 36;
  /* Page and surfaces. */
  --bg: #07111c;
  --rgb-bg: 7, 17, 28;
  --bg-deep: #030a12;
  --body-bg: linear-gradient(135deg, rgba(10, 21, 34, 0.98), rgba(2, 7, 13, 1));
  --shell-bg: radial-gradient(circle at 18% 12%, rgba(89, 209, 100, 0.08), transparent 30%), radial-gradient(circle at 78% 8%, rgba(96, 165, 250, 0.09), transparent 35%), #07111c;
  --rotate-overlay-bg: radial-gradient(circle at 18% 12%, rgba(89, 209, 100, 0.1), transparent 32%), radial-gradient(circle at 78% 8%, rgba(96, 165, 250, 0.11), transparent 36%), #07111c;
  /* The two glows that drift behind the signed-out screens (.login-sky): the shell's own green and blue, as faint. */
  --login-sky-1: rgba(89, 209, 100, 0.1);
  --login-sky-2: rgba(96, 165, 250, 0.11);
  --nav-bg: linear-gradient(180deg, rgba(4, 13, 22, 0.98), rgba(3, 10, 18, 0.98));
  --panel: rgba(11, 22, 34, 0.94);
  --panel-2: rgba(12, 25, 39, 0.9);
  --card-bg: linear-gradient(180deg, rgba(13, 27, 42, 0.93), rgba(9, 20, 32, 0.93));
  --float-panel-bg: linear-gradient(180deg, rgba(13, 27, 42, 0.97), rgba(9, 20, 32, 0.97));
  --sheet-bg: #0b1725;
  --popover-bg: #111c2b;
  --popover-bg-2: #0c1725;
  --popover-bg-3: #0f202f;
  --menu-bg: rgba(7, 17, 28, 0.96);
  --popup-bg: rgba(7, 17, 28, 0.94);
  --tab-bar-bg: rgba(7, 17, 28, 0.96);
  --table-head-bg: #0f202f;
  --surface-raised: #0d1b2a;
  --surface-raised-2: #1b2b3b;
  --tile-bg: rgba(5, 15, 26, 0.56);
  --tile-bg-2: rgba(5, 15, 26, 0.5);
  --tile-bg-3: rgba(5, 15, 26, 0.4);
  --well-bg: rgba(2, 8, 15, 0.5);
  --inset-bg: rgba(2, 6, 23, 0.32);
  --drop-bg: rgba(3, 10, 18, 0.26);
  --active-item-bg: linear-gradient(90deg, rgba(36, 52, 70, 0.82), rgba(28, 43, 59, 0.64));
  --scroll-fade: linear-gradient(180deg, rgba(2, 8, 15, 0.35), rgba(2, 8, 15, 0));
  --paper: #fff;
  /* The strip behind the installed iPhone app's status bar, which draws white text (SAFE AREA). */
  --status-strip-bg: #07111c;
  /* Scrims behind modals, sheets and the calendar. */
  --scrim: rgba(3, 10, 18, 0.72);
  --scrim-2: rgba(3, 10, 18, 0.6);
  --scrim-3: rgba(3, 10, 18, 0.62);
  --scrim-4: rgba(2, 6, 12, 0.6);
  /* Lines. --line is the card hairline; control borders are separate because they must stay visible (3:1) in both themes. Dark ones are SOLID: a translucent edge composites differently over every surface a field sits on (card, sheet, popover), and the old .22-.35 alphas measured 1.4 to 1.9:1 against them. #66768c is 3.1 to 4.2:1 on all of them; the hover pair (#8a99ad), 5.0 to 6.8:1, is the stronger step. */
  --line: rgba(148, 163, 184, 0.18);
  --control-border: #66768c;
  --control-border-2: #66768c;
  --control-border-3: #66768c;
  --control-border-4: #66768c;
  --control-border-hover: #8a99ad;
  --control-border-hover-2: #8a99ad;
  /* The frame round a box that is not a control (a notice, a results list, a failed page, a label): it never had to reach 3:1, so it stays the faint slate the control edges used to be (the light theme's is unchanged too). */
  --frame-border: rgba(148, 163, 184, 0.22);
  --swatch-border: rgba(2, 8, 15, 0.28);
  /* The Flights map's own layer colours (spray points, takeoff and landing
     markers), which the map draws the same in both themes: so one value, here,
     for the swatches beside those layers' tick boxes. */
  --flight-spray: #16a34a;
  --flight-takeoff: #38bdf8;
  --flight-landing: #f97316;
  /* Text, strongest to faintest. */
  --fg-strong: #ffffff;
  --fg-strong-2: #f9fafb;
  --text: #f8fafc;
  --fg-title: #f1f6fc;
  --fg-title-2: #f1f5fb;
  --fg-bright: #eef2f7;
  --fg-bright-2: #e6edf6;
  --fg-body: #e5edf7;
  --fg-body-2: #e2e8f0;
  --fg-neutral: #e5e7eb;
  --fg-calendar: #dbe4f0;
  --fg-nav: #d7deea;
  --fg-control-2: #d1d5db;
  --fg-soft: #cdd8e6;
  --fg-label: #cbd5e1;
  --fg-copy: #c2cad7;
  --fg-meta: #b9c3d0;
  --fg-desc: #b3c0d1;
  --fg-empty: #a8b3c4;
  --fg-when: #9fb3cc;
  --muted: #9aa8bb;
  --fg-muted-2: #9ca3af;
  --fg-subtle: #94a3b8;
  --fg-desc-2: #8b98ac;
  --fg-faint: #8090a5;
  --fg-faint-2: #7c8aa0;
  --fg-faint-3: #64748b;
  --fg-disabled: #4a5872;
  /* Accent and state text. */
  --green: #59d164;
  --green-soft: rgba(89, 209, 100, 0.16);
  /* A focused text field's or dropdown's edge: the accent, solid. It was the keyboard ring's colour too until the ring got its own, softer one below (Josh, 2026-11-01); the field's edge and its glow stay as they were. */
  --focus-ring: #59d164;
  /* The keyboard focus ring (Josh, 2026-11-01: "more subtle"). Solid and one token, so a ring never goes faint on one page; a green with less of the accent's glare, 3:1 or better against every surface it can sit on: 3.5:1 on a hovered secondary button (the lowest), 4.5 on a secondary button, 5.0 on a selected tab or row, 6.4 on a card, 6.9 on the page (the light theme's: 3.35 to 4.4:1). It was the accent itself, 9:1 on a dark card. ux-focus-ring-frontend.mjs holds the table. */
  --keyboard-ring: #5aae66;
  /* Its width: 0.15625rem draws 2 CSS px at the 85% desktop root (2.125px) and the 87.5% phone root (2.1875px) at 1x, 2x and 3x, never under 2 (the 2px of WCAG 2.4.13, Focus Appearance); the old 0.1875rem drew 2.5px on a 2x screen and 2.33px on an iPhone. One width for every ring, so no ring is heavier than another, and the one switch the pointer needs (html.using-pointer, the global ring rules). */
  --keyboard-ring-width: 0.15625rem;
  /* A ring drawn inside its control (where an ancestor would clip one outside): inset by exactly its width, so it lies wholly inside. */
  --keyboard-ring-inset: calc(-1 * var(--keyboard-ring-width));
  --blue: #60a5fa;
  --danger: #ef4444;
  --fg-on-green-tint: #dfffe2;
  --fg-on-green-tint-hover: #ffffff;
  --fg-go: #4ade80;
  --fg-success: #a7f3d0;
  --fg-success-2: #a7f3b0;
  --fg-success-3: #d1fae5;
  --fg-success-deep: #15803d;
  --fg-error: #fca5a5;
  --fg-error-2: #f87171;
  --fg-error-deep: #b91c1c;
  --fg-warn: #fbbf24;
  --fg-warn-2: #fcd34d;
  --fg-warn-3: #facc15;
  --fg-warn-deep: #b45309;
  --fg-amber: #f59e0b;
  --fg-notam-title: #f8dcae;
  --fg-info-hover: #dbeafe;
  --fg-fab: #eaf6ec;
  /* Status pills (tinted fill + text). */
  --pill-green-fg: #bbf7d0;
  --pill-green-fg-2: #86efac;
  --pill-red-fg: #fecaca;
  /* Red tag in a job card note, and the list's red "!" - opaque in light, so a
     tinted note or a selected row underneath cannot pull them under 4.5:1. */
  --issue-tag-red-bg: rgba(239, 68, 68, 0.28);
  --flag-mark-red-bg: rgba(239, 68, 68, 0.28);
  --pill-amber-fg: #fef08a;
  --pill-sand-fg: #fef3c7;
  --pill-orange-bg: rgba(249, 115, 22, 0.20);
  --pill-orange-fg: #fed7aa;
  --pill-apricot-bg: rgba(251, 146, 60, 0.22);
  --pill-blue-fg: #bfdbfe;
  --pill-blue-fg-2: #93c5fd;
  --pill-violet-bg: rgba(167, 139, 250, 0.22);
  --pill-violet-fg: #ddd6fe;
  --pill-teal-bg: rgba(45, 212, 191, 0.20);
  --pill-teal-fg: #99f6e4;
  --pill-closed-bg: rgba(100, 116, 139, 0.14);
  /* Controls and buttons. */
  --control-bg: rgba(2, 8, 15, 0.62);
  --select-bg: #07111c;
  /* The one arrow every dropdown draws - native <select>s and the two custom
     pickers alike (see DROPDOWN CONTROLS). A native select cannot carry a
     pseudo-element, so the mark has to be a background image, and a background
     image cannot read a custom property: it is a token instead, one value per
     theme, drawn in --muted. */
  --select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z' fill='%239aa8bb'/%3E%3C/svg%3E");
  /* The Plan list's Sort on a phone is this mark alone (up and down arrows), on the
     trigger of the Sort menu - a background image for the same reason as the
     arrow above, drawn in --fg-soft: it is the whole of the control, not a hint
     beside a value, so it takes a text colour rather than the arrow's grey. */
  --sort-icon: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cdd8e6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 16-4 4-4-4'/%3E%3Cpath d='M17 20V4'/%3E%3Cpath d='m3 8 4-4 4 4'/%3E%3Cpath d='M7 4v16'/%3E%3C/svg%3E");
  --control-scheme: dark;
  /* Dropdown geometry (one set, both themes): the shared field padding, the
     pinned line box that makes all three kinds the same height, and the right
     inset that keeps the value clear of the arrow. */
  --dropdown-pad-y: 0.625rem;
  --dropdown-pad-x: 0.6875rem;
  --dropdown-pad-arrow: 1.6875rem;
  --dropdown-line: 1.25rem;
  --autofill-fill: 0 0 0 62.5rem #0a1119 inset;
  /* The main action. Bright enough to be the one thing on a card that says "press me": white on it is 5.2:1 (4.5 on hover, under brightness(1.08)), and it stands 2.4:1 off a secondary button and 3.4:1 off the card, where the navy it replaced (#1f3b65) was 1.2:1 off a secondary one. */
  --button-bg: #2563eb;
  /* A SELECTED tab or segment is not a main action, so it keeps the navy the main button used to be (it must not turn bright blue just because it once borrowed --button-bg). */
  --subtab-active-bg: #1f3b65;
  /* The gap between a button and its keyboard ring. The ring (--keyboard-ring) is only 1.9:1 off the bright main button's fill, so a strip of the card has to show between them. A gap under one whole pixel is not drawn at all: 0.0625rem is 0.85px at the 85% root and rendered as none. 0.1875rem is 2.55px at that root and 2.625px at the phone's, drawn as 2px at both. The gap is about the two colours, not the ring's width, so the thinner ring (2026-11-01) keeps it; ring and gap now reach 0.34375rem (4.7px) out from the button, where they reached 0.375rem. One value for both themes since then: the light theme's ring is 2.5:1 off its navy main button, and with the old 0.0625rem (drawn as none) it touched it. */
  --button-focus-offset: 0.1875rem;
  --button-fg: white;
  --button-disabled-bg: #1e2a3b;
  /* The count on the alerts bell: a white number on red, 4.8:1 (the --danger text red is for text on the page, 3.8:1 under a white label). */
  --badge-bg: #dc2626;
  /* The chosen row of an open dropdown, on the green tint it sits on (the popover or the phone sheet, hovered or not): 4.7 to 5.1:1. */
  --select-picked-fg: #59d164;
  --secondary-bg: #26364b;
  --secondary-bg-hover: #31465f;
  --secondary-fg: #ffffff;
  --danger-bg: #3a1d23;
  --on-green: #06210b;
  --on-green-2: #04121e;
  --on-solid: #fff;
  /* Notices and callouts. */
  --banner-bg: linear-gradient(90deg, #b45309, #d97706, #b45309);
  --banner-fg: #fff7ed;
  --notice-warn-bg: rgba(31, 20, 5, 0.94);
  --notice-error-bg: rgba(35, 10, 12, 0.94);
  --notam-finding-bg: rgba(120, 78, 8, 0.14);
  --amber-rule: #f4b740;
  /* Map chrome: panels and controls floating over the basemap. The basemap itself is not themed. */
  --map-bg: #020617;
  --map-empty-bg: rgba(3, 10, 18, 0.92);
  --map-float-bg: rgba(7, 17, 28, 0.92);
  --map-float-bg-2: rgba(7, 17, 28, 0.9);
  --map-control-bg: rgba(15, 23, 42, 0.92);
  --map-control-bg-2: rgba(15, 23, 42, 0.9);
  --map-control-bg-3: rgba(17, 24, 39, 0.85);
  --map-badge-bg: rgba(2, 6, 23, 0.9);
  --map-toggle-bg: rgba(8, 17, 28, 0.82);
  --legend-panel-bg: rgba(2, 6, 23, 0.96);
  --legend-header-bg: rgba(30, 41, 59, 0.5);
  --legend-tab-bg: rgba(30, 41, 59, 0.6);
  --legend-tab-active: #2563eb;
  --on-map: #ffffff;
  --map-label-shadow: 0 0 0.1875rem rgba(0, 0, 0, 0.85);
  /* Map credits: the one quiet line in a map's bottom corner that names the map's providers (app.js createMapCreditControl). Follows the theme, like the map's other controls, but lighter than them: translucent enough to sit back over imagery, opaque enough to read over snow or bush in either theme. The text keeps 4.5:1 against the chip over pure white and pure black imagery in both themes (logs-map smoke check 18): at this translucency the dark theme's text needs to be this light. */
  --map-credit-bg: rgba(7, 17, 28, 0.64);
  --map-credit-fg: #e2e8f0;
  --map-credit-border: rgba(148, 163, 184, 0.18);
  /* Road paint for the LINZ aerial hybrid (app.js linzRoadPalette). It paints over an aerial photo, which looks the same in either theme, so these have no light values. Cream and amber on a dark brown casing: a road never reads as a white flight track or a blue job boundary. */
  --map-road-minor: #f3dfa8;
  --map-road-major: #f5b83d;
  --map-road-casing: #4b3a1e;
  --map-road-track: #4b3a1e;
  --map-road-track-halo: rgba(243, 223, 168, 0.55);
  --map-parcel-line: rgba(226, 186, 112, 0.7);
  --map-parcel-shadow: rgba(40, 32, 18, 0.35);
  /* The property overlay (map preview; app.js propertyPalette): neighbouring boundaries, buildings and house numbers over New Zealand. Fuchsia, which nothing else on these maps is (tracks white, spray green, areas blue, cut-outs red, roads cream and amber), so it is never read as a track or a job area. It paints over imagery and over LINZ's map, which look the same in either theme, so these have no light values. */
  --map-property-line: #e879f9;
  --map-property-line-shadow: rgba(46, 16, 101, 0.6);
  --map-property-building: rgba(217, 70, 239, 0.2);
  --map-property-building-line: #c026d3;
  --map-property-number: #ffffff;
  --map-property-number-halo: #701a75;
  --thumb-border: rgba(7, 17, 28, 0.92);
  /* Fixed colours that mean the same thing in both themes: risk grades (they must match the risk picker), the amber of the NOTAM layer and time slider, status dots, progress. */
  --risk-go: #22c55e;
  --risk-manage: #eab308;
  --risk-nogo: #ef4444;
  --dot-neutral: #9ca3af;
  --dot-idle: #64748b;
  --on-risk: #0b1622;
  --on-risk-2: rgba(0, 0, 0, 0.72);
  --on-risk-3: rgba(11, 22, 34, 0.7);
  --amber-solid: #f59e0b;
  --on-amber: #0b1220;
  --progress-fill: #34d399;
  --tab-indicator-alt: #22d3ee;
  --ring-selected: #f8fafc;
  --ring-selected-cell: 0 0 0 0.125rem rgba(248, 250, 252, 0.4);
  /* Elevation. Dark uses deep, soft shadows and a green glow on the active item; light uses one tight shadow for things that float, and none for things that sit on the page. */
  --shadow-card: inset 0 0.0625rem 0 rgba(255,255,255,0.03), 0 1.125rem 3rem rgba(0,0,0,0.16);
  --shadow-nav: 1.375rem 0 3.375rem rgba(0, 0, 0, 0.24);
  --shadow-map: 0 1.5rem 4.375rem rgba(0, 0, 0, 0.32);
  --shadow-page: 0 0.125rem 0.625rem rgba(0, 0, 0, 0.45);
  --shadow-map-float: 0 1rem 2.25rem rgba(0,0,0,0.34);
  --shadow-map-controls: 0 0.75rem 1.75rem rgba(0,0,0,0.24);
  --shadow-map-notice: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.35);
  --shadow-fab: 0 0.625rem 1.5rem rgba(0, 0, 0, 0.4);
  --shadow-menu: 0 0.875rem 2.25rem rgba(0,0,0,0.45);
  --shadow-badge: 0 0.75rem 2rem rgba(0, 0, 0, 0.45);
  --shadow-dropdown: 0 0.75rem 1.875rem rgba(0, 0, 0, 0.45);
  --shadow-dropdown-2: 0 0.75rem 2rem rgba(0, 0, 0, 0.4);
  --shadow-calendar: 0 1rem 3rem rgba(0, 0, 0, 0.55);
  --shadow-alerts: 0 1rem 2.75rem rgba(0, 0, 0, 0.5);
  --shadow-float-panel: 0 1.125rem 3rem rgba(0,0,0,0.32);
  --shadow-sheet: 0 -0.25rem 1.5rem rgba(0, 0, 0, 0.4);
  --shadow-thumb: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.45);
  --glow-active: 0 0 1.125rem rgba(89, 209, 100, 0.45);
  --glow-active-alt: 0 0 1.125rem rgba(34, 211, 238, 0.45);
  --backdrop-float: blur(0.75rem);
  --backdrop-bar: blur(0.5rem);
  --backdrop-toggle: blur(0.125rem);
  /* The vendor logo's filter, and colours app.js reads for charts. */
  --brand-logo-filter: none;
  --chart-text: #94a3b8;
  --chart-grid: rgba(148, 163, 184, 0.12);
  --chart-legend: #cdd8e6;
  --chart-slice-border: rgba(2, 8, 15, 0.6);
  --chart-default-text: #666;
  --chart-default-grid: rgba(0,0,0,0.1);
  /* The airspace card's header band is the colour the map draws its zone in (app.js sets --zc from the map's
     own table), the same in both themes. Its ink is white or the dark ink, whichever reads on that colour
     (lib/airspace-info.js airspaceBandInk, data-ink="dark"; the zone card gate checks 4.5:1 on every one).
     --zcard-fallback is the map's "other" grey, for a band app.js did not colour. */
  --zcard-on-band: #ffffff;
  --rgb-zcard-on-band: 255, 255, 255;
  --zcard-ink-dark: #0f172a;
  --rgb-zcard-ink-dark: 15, 23, 42;
  --zcard-fallback: #94a3b8;
}

html[data-theme="light"] {
  /* Colour channels. */
  --rgb-slate: 31, 45, 58;
  --rgb-overlay: 31, 45, 58;
  --rgb-mist: 31, 45, 58;
  --rgb-green: 27, 111, 48;
  --rgb-green-2: 27, 111, 48;
  --rgb-emerald: 13, 110, 80;
  --rgb-blue: 29, 95, 168;
  --rgb-red: 180, 35, 24;
  --rgb-red-soft: 180, 35, 24;
  --rgb-red-deep: 180, 35, 24;
  --rgb-amber: 158, 98, 0;
  --rgb-orange: 166, 90, 0;
  --rgb-yellow: 158, 98, 0;
  /* Page and surfaces. */
  --bg: #eef1f4;
  --rgb-bg: 238, 241, 244;
  --bg-deep: #eef1f4;
  --body-bg: var(--bg);
  --shell-bg: var(--bg);
  --rotate-overlay-bg: var(--bg);
  --login-sky-1: rgba(27, 111, 48, 0.07);
  --login-sky-2: rgba(29, 95, 168, 0.08);
  --nav-bg: #fbfcfd;
  --panel: #ffffff;
  --panel-2: #fbfcfd;
  --card-bg: #ffffff;
  --float-panel-bg: #ffffff;
  --sheet-bg: #ffffff;
  --popover-bg: #ffffff;
  --popover-bg-2: #ffffff;
  --popover-bg-3: #ffffff;
  --menu-bg: #ffffff;
  --popup-bg: #ffffff;
  --tab-bar-bg: #ffffff;
  --table-head-bg: #f2f4f7;
  --surface-raised: #f1f3f6;
  --surface-raised-2: #e7ebef;
  --tile-bg: #f5f7f9;
  --tile-bg-2: #f5f7f9;
  --tile-bg-3: #f5f7f9;
  --well-bg: #e8ecef;
  --inset-bg: rgba(31, 45, 58, 0.04);
  --drop-bg: rgba(31, 45, 58, 0.03);
  --active-item-bg: #e6ebf0;
  --scroll-fade: linear-gradient(180deg, rgba(31, 45, 58, 0.1), rgba(31, 45, 58, 0));
  /* Dark on purpose: white status-bar text needs a dark page behind it. */
  --status-strip-bg: #07111c;
  /* Scrims behind modals, sheets and the calendar. */
  --scrim: rgba(14, 24, 34, 0.34);
  --scrim-2: rgba(14, 24, 34, 0.34);
  --scrim-3: rgba(14, 24, 34, 0.34);
  --scrim-4: rgba(14, 24, 34, 0.34);
  /* Lines. */
  --line: rgba(31, 45, 58, 0.12);
  --control-border: #818b95;
  --control-border-2: #818b95;
  --control-border-3: #818b95;
  --control-border-4: #818b95;
  --control-border-hover: #66717c;
  --control-border-hover-2: #66717c;
  --frame-border: #818b95;
  /* Text, strongest to faintest. */
  --fg-strong: #1c2935;
  --fg-strong-2: #1c2935;
  --text: #1c2935;
  --fg-title: #1c2935;
  --fg-title-2: #1c2935;
  --fg-bright: #1c2935;
  --fg-bright-2: #1c2935;
  --fg-body: #2b3743;
  --fg-body-2: #2b3743;
  --fg-neutral: #2b3743;
  --fg-calendar: #2b3743;
  --fg-nav: #3e4a57;
  --fg-control-2: #3e4a57;
  --fg-soft: #3e4a57;
  --fg-label: #3e4a57;
  --fg-copy: #4a5663;
  --fg-meta: #58646f;
  --fg-desc: #58646f;
  --fg-empty: #58646f;
  --fg-when: #58646f;
  --muted: #58646f;
  --fg-muted-2: #58646f;
  --fg-subtle: #58646f;
  --fg-desc-2: #58646f;
  --fg-faint: #5c6874;
  --fg-faint-2: #5c6874;
  --fg-faint-3: #5c6874;
  --fg-disabled: #8a939c;
  /* Accent and state text. */
  --green: #1b6f30;
  --green-soft: rgba(27, 111, 48, 0.12);
  --focus-ring: #1b6f30;
  --keyboard-ring: #338848;
  --blue: #1d5fa8;
  --danger: #b42318;
  --fg-on-green-tint: #155a26;
  --fg-on-green-tint-hover: #0f4a1f;
  --fg-go: #1b6f30;
  --fg-success: #1b6f30;
  --fg-success-2: #1b6f30;
  --fg-success-3: #155a26;
  --fg-success-deep: #1b6f30;
  --fg-error: #b42318;
  --fg-error-2: #b42318;
  --fg-error-deep: #a61b1b;
  --fg-warn: #8a5300;
  --fg-warn-2: #8a5300;
  --fg-warn-3: #8a5300;
  --fg-warn-deep: #8a4a06;
  --fg-amber: #8a5300;
  --fg-notam-title: #6e4600;
  --fg-info-hover: #174a86;
  --fg-fab: #2b3743;
  /* Status pills (tinted fill + text). */
  --pill-green-fg: #1a5e2a;
  --pill-green-fg-2: #1a5e2a;
  --pill-red-fg: #9a1c13;
  --issue-tag-red-bg: #f3d3d0;
  --flag-mark-red-bg: #f3d3d0;
  --pill-amber-fg: #6b4400;
  --pill-sand-fg: #6b4400;
  --pill-orange-bg: rgba(166, 90, 0, 0.16);
  --pill-orange-fg: #7f3b05;
  --pill-apricot-bg: rgba(166, 90, 0, 0.16);
  --pill-blue-fg: #174a86;
  --pill-blue-fg-2: #174a86;
  --pill-violet-bg: rgba(84, 66, 160, 0.14);
  --pill-violet-fg: #46388a;
  --pill-teal-bg: rgba(13, 110, 100, 0.15);
  --pill-teal-fg: #0d5750;
  --pill-closed-bg: #e8ebee;
  /* Controls and buttons. */
  --control-bg: #ffffff;
  --select-bg: #ffffff;
  --select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z' fill='%2358646f'/%3E%3C/svg%3E");
  --sort-icon: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233e4a57' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 16-4 4-4-4'/%3E%3Cpath d='M17 20V4'/%3E%3Cpath d='m3 8 4-4 4 4'/%3E%3Cpath d='M7 4v16'/%3E%3C/svg%3E");
  --control-scheme: light;
  --autofill-fill: 0 0 0 62.5rem #ffffff inset;
  --button-bg: #1f3b65;
  --subtab-active-bg: #1f3b65;
  --button-fg: #ffffff;
  --button-disabled-bg: #d5dbe0;
  --badge-bg: #b42318;
  --select-picked-fg: #145a26;
  --secondary-bg: #e6ebf0;
  --secondary-bg-hover: #dbe1e7;
  --secondary-fg: #2b3743;
  --danger-bg: #f8e4e1;
  --on-green: #ffffff;
  --on-green-2: #ffffff;
  /* Notices and callouts. */
  --banner-bg: #8a4506;
  --notice-warn-bg: #fff5e0;
  --notice-error-bg: #fdece9;
  --notam-finding-bg: rgba(158, 98, 0, 0.08);
  --amber-rule: #b87700;
  /* Map chrome. */
  --map-bg: #e2e6ea;
  --map-empty-bg: rgba(255, 255, 255, 0.94);
  --map-float-bg: rgba(255, 255, 255, 0.97);
  --map-float-bg-2: rgba(255, 255, 255, 0.97);
  --map-control-bg: #ffffff;
  --map-control-bg-2: #ffffff;
  --map-control-bg-3: #ffffff;
  --map-badge-bg: rgba(255, 255, 255, 0.97);
  --map-toggle-bg: rgba(255, 255, 255, 0.95);
  --legend-panel-bg: #ffffff;
  --legend-header-bg: #f2f4f7;
  --legend-tab-bg: #f2f4f7;
  --legend-tab-active: #1d5fa8;
  --map-credit-bg: rgba(255, 255, 255, 0.72);
  --map-credit-fg: #2b3945;
  --map-credit-border: rgba(31, 45, 58, 0.12);
  --thumb-border: #ffffff;
  /* Fixed colours that mean the same thing in both themes. */
  --progress-fill: #0f8a5f;
  --tab-indicator-alt: #0e7490;
  --ring-selected: #1c2935;
  --ring-selected-cell: 0 0 0 0.125rem rgba(28, 41, 53, 0.5);
  /* Elevation. */
  --shadow-card: none;
  --shadow-nav: none;
  --shadow-map: none;
  --shadow-page: 0 0.0625rem 0.25rem rgba(16, 26, 36, 0.12);
  --shadow-map-float: 0 0.125rem 0.5rem rgba(16, 26, 36, 0.12);
  --shadow-map-controls: 0 0.125rem 0.5rem rgba(16, 26, 36, 0.12);
  --shadow-map-notice: 0 0.125rem 0.5rem rgba(16, 26, 36, 0.12);
  --shadow-fab: 0 0.125rem 0.5rem rgba(16, 26, 36, 0.12);
  --shadow-menu: 0 0.25rem 0.875rem rgba(16, 26, 36, 0.14);
  --shadow-badge: 0 0.25rem 0.875rem rgba(16, 26, 36, 0.14);
  --shadow-dropdown: 0 0.25rem 0.875rem rgba(16, 26, 36, 0.14);
  --shadow-dropdown-2: 0 0.25rem 0.875rem rgba(16, 26, 36, 0.14);
  --shadow-calendar: 0 0.5rem 1.5rem rgba(16, 26, 36, 0.14);
  --shadow-alerts: 0 0.5rem 1.5rem rgba(16, 26, 36, 0.14);
  --shadow-float-panel: 0 0.5rem 1.5rem rgba(16, 26, 36, 0.14);
  --shadow-sheet: 0 -0.125rem 0.75rem rgba(16, 26, 36, 0.14);
  --shadow-thumb: 0 0.0625rem 0.1875rem rgba(16, 26, 36, 0.3);
  --glow-active: none;
  --glow-active-alt: none;
  --backdrop-float: none;
  --backdrop-bar: none;
  --backdrop-toggle: none;
  /* The vendor logo's filter, and colours app. */
  --brand-logo-filter: invert(1) hue-rotate(180deg);
  --chart-text: #58646f;
  --chart-grid: rgba(31, 45, 58, 0.12);
  --chart-legend: #3e4a57;
  --chart-slice-border: #ffffff;
  --chart-default-text: #58646f;
  --chart-default-grid: rgba(31, 45, 58, 0.12);
}

/* Theme plumbing: rules that exist only to read a token, so colours the
   markup carries itself (a white logo PNG) follow the theme too. Their dark
   values equal what the markup already said, and the secondary button's text
   is the white it inherited from `button`. */
.orgswitcher-brand-logo { filter: var(--brand-logo-filter); }
.secondary-button { color: var(--secondary-fg); }

/* Light-only adjustments. Shape, not colour: on a dark ground a tinted fill
   is enough to find a control, on a white one it needs an edge. Kept here,
   not scattered, so the dark theme cannot be touched by them. */
html[data-theme="light"] button:disabled { color: var(--fg-faint); }
html[data-theme="light"] .secondary-button:not(:disabled) {
  box-shadow: inset 0 0 0 0.0625rem var(--control-border);
}
html[data-theme="light"] .danger-button:not(:disabled) {
  box-shadow: inset 0 0 0 0.0625rem rgba(var(--rgb-red), 0.45);
}

/* Read-only fields. The job card's view mode, a platform admin's profile and
   the like show real data in disabled controls. Dimmed by opacity they stay
   readable on near-black, but on white the same 0.6 takes body text to 4:1
   and a grey-filled trigger to 2:1 - too faint to read in the sun. In the
   light theme a read-only field is a flat grey well with full-strength text:
   plainly not editable, still legible. */
html[data-theme="light"] .form-field input:disabled,
html[data-theme="light"] select:disabled,
html[data-theme="light"] .form-field textarea:disabled,
html[data-theme="light"] .searchable-select-trigger:disabled,
html[data-theme="light"] .multiselect-dropdown-trigger:disabled,
html[data-theme="light"] .date-trigger.is-disabled {
  opacity: 1;
  /* The colour only: the `background` shorthand also wiped the dropdown arrow,
     so a disabled dropdown lost it in the light theme and kept it in the dark. */
  background-color: var(--surface-raised);
  border-color: rgba(var(--rgb-slate), 0.16);
  color: var(--fg-copy);
}
html[data-theme="light"] .date-trigger.is-disabled:not(.is-empty) .date-trigger-text { color: var(--fg-copy); }

/* A finished corrective action is de-emphasised, not erased: its Done pill
   says it is finished, so the row keeps legible text instead of fading. */
html[data-theme="light"] .action-row.done { opacity: 1; }
html[data-theme="light"] .action-row.done .action-desc { color: var(--muted); }

/* ==== END THEME TOKENS ==== */

* { box-sizing: border-box; }

/* App-wide control theming. accent-color tints native controls (checkboxes,
   radios, range sliders, progress) with the brand green; the scrollbar rules
   replace the browser-default bars with a thin, dark-matching one everywhere
   that scrolls. scrollbar-width/-color inherit, so setting them on the root
   covers every scroll container (Firefox); the ::-webkit-scrollbar rules do the
   same for Chrome/Edge/Safari. */
/* Global UI density. The whole layout is authored in rem, so the root
   font-size sets the scale: 85% of the browser default on a desktop (the old
   0.85 zoom, but crisp and zoom-free), 87.5% on a phone (html.mobile,
   below). Nudge a percentage to make that layout more/less compact; both
   respect a user's own browser font-size preference. */
html {
  font-size: 85%;
  accent-color: var(--green);
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.4) transparent;
}
/* A phone reads at 14px: 87.5% of the browser default, the owner's choice on
   2026-10-31 after the 16px base read too large. That is a step up from the
   desktop's 85% (body text 13.6px, a 0.75rem label 10.2px) without the
   oversized look, and one percentage scales the whole phone layout with it:
   --touch-min (3.25rem) is 45.5px, and the tab bar and its reserve follow.
   Every field you type in keeps 16px text whatever the base (the touch-target
   rules below), because iOS zooms the page when a field under 16px is focused.
   Desktop stays at 85%.
   Page zoom is back on (the viewport meta no longer locks it), so a pinch
   zooms a card page and a map's own pinch zooms only the map. touch-action
   keeps the app feel: pan and pinch, but no double-tap zoom on a button
   tapped twice. It sits on html.mobile, not html, so "Request desktop site"
   (no html.mobile, a 1280px viewport) keeps double-tap zoom, its main way in. */
html.mobile {
  font-size: 87.5%;
  touch-action: manipulation;
}
::-webkit-scrollbar { width: 0.6875rem; height: 0.6875rem; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(var(--rgb-slate), 0.35);
  border-radius: 0.5rem;
  border: 0.125rem solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--rgb-slate), 0.55);
  background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }

/* Global guard for the [hidden] attribute. Many classes here set their own
   display (grid/flex/block), which outranks the browser's native
   [hidden] { display: none } and leaves elements on screen even after JS sets
   `.hidden = true`. This one !important rule makes the attribute authoritative
   everywhere, so hiding a field/panel/row via `hidden` always works and we don't
   need a per-class [hidden] guard each time. Nothing in the app re-shows an
   element via a class while keeping the attribute, so this is safe. */
[hidden] { display: none !important; }

html, body { height: 100%; }

/* The root paints the deepest background tone so any letterboxing outside the
   layout (e.g. the safe-area side bars when a phone rotates to landscape)
   matches the app instead of flashing white. */
html { background: var(--bg-deep); }

/* SAFE AREA. The viewport meta says viewport-fit=cover, so a phone lets the page
   run to the very edges of the screen - under the status bar and the Dynamic
   Island, behind the home indicator, into the notch in landscape. These four
   tokens are the page's own margins against that; everything that sits at an
   edge reads them rather than env() directly.

   Left and right apply everywhere: they replace the letterbox bars a phone used
   to draw itself in landscape, so a browser tab lays out as it did before the
   meta changed. Top and bottom apply only to the installed app (html.standalone,
   set by theme.js): a browser tab draws its own bars there, and counting them
   again would only add a gap. Padding on the root, not on the shell, so the bars
   keep the page's deepest colour as they always did. Fixed-position bars and
   sheets are outside it and read the tokens themselves.
   100vw is the whole screen, insets included, while the page inside the root's
   padding is narrower by both side tokens. A phone rule that sizes anything
   from 100vw takes var(--safe-left) and var(--safe-right) off it, or the box
   comes out that much too wide in landscape on a notched phone (the Logs map
   notice covered the Layers button); the install gate pins it. */
:root {
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-top: 0px;
  --safe-bottom: 0px;
}
html.standalone {
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}
html {
  padding-left: var(--safe-left);
  padding-right: var(--safe-right);
}

/* THE INSTALLED IPHONE APP'S SHORT VIEWPORT. On the iOS releases WebKit bug
   301994 lists (26.5.x, the iOS 27 beta), the home-screen app's layout viewport
   stops one top inset (59pt on most phones) short of the screen's bottom, and iOS
   draws nothing of the page below it: the band there is the system's own, filled
   with <html>'s background (--bg-deep) as it was at launch, and a box run on into
   it is simply not drawn there (the first fix did that, and the tab bar lost its
   labels). theme.js measures the gap and puts html.viewport-short on the page,
   with the gap as --viewport-gap on <html>'s style, only when a fixed box stops
   short of the screen by the top inset. The page stays inside the viewport; the
   class changes two things:
   - the home indicator sits in the band, not over the page, so --safe-bottom is
     only what of the bottom inset the gap leaves (0 on every phone measured:
     34 of 59), and the tab bar, the sheets and the sign-in view stop padding for it;
   - the tab bar takes the band's colour (THE SHORT VIEWPORT'S TAB BAR, after its
     base rule), so the bar and the band read as one bar to the screen's bottom.
   Nothing sets the class in a browser tab, on Android or on a desktop.
   Since THE INSTALLED IPHONE APP'S FULL HEIGHT (below) this is the fallback: it
   applies only while iOS keeps the viewport short in spite of it.
   The two lengths are typed so that theme.js can read them as pixels (a second
   opinion: it reads env() from its probe's padding first): an untyped custom
   property would hand back the env() text. */
@property --safe-top-length {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --safe-bottom-length {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
html {
  --safe-top-length: var(--safe-top);
  --safe-bottom-length: var(--safe-bottom);
}
html.standalone.viewport-short {
  --safe-bottom: max(0px, env(safe-area-inset-bottom, 0px) - var(--viewport-gap, 0px));
}

/* THE INSTALLED IPHONE APP'S FULL HEIGHT. The short viewport above is iOS's
   answer only while the document is no taller than it, and "html, body
   { height: 100% }" kept the document exactly that tall. So in the iPhone
   home-screen app (html.ios-app, set by theme.js in <head> from the same test
   as html.viewport-short, on an iPhone or iPod only; never on an iPad, in a
   browser tab, on Android or on a desktop) <html> is made at least as tall as
   the screen, and iOS then grows the
   viewport to the whole screen. The fixed bars reach the bottom, theme.js's
   probe measures no gap and html.viewport-short goes by itself.
   - Measured on a physical iPhone in this state (get-bb/bb 2356aee3, 393 x 852,
     top inset 59): innerHeight, 100svh, 100dvh and 100% all read 793 and 100lvh
     reads the full 852. With <html> 100lvh tall, innerHeight and 100dvh came
     back as 852. So 100lvh is the screen here, and the cap below does not
     defeat the rule.
   - min(100lvh, 100% + the top inset), not 100lvh alone (frappe-ui #1289): with
     an opaque status bar (the iOS 26.1 state, top inset 0) iOS reports 100lvh
     taller than the viewport (WebKit 316008), and the page would be that much
     too tall. There the rule is just 100%; held sideways (no top inset) the
     same; once the viewport is the whole screen 100% is the screen, 100lvh is
     too, and the rule changes nothing. Before or after, it never asks for more
     than the screen. The inset is read through --safe-top, like every other
     rule here: html.ios-app is always html.standalone, where it is the inset.
   - min-height, so html's own height: 100% stays, and body is pinned to the
     viewport (100dvh: the short one while iOS keeps it, the screen once it grows,
     as the shell is). If iOS draws the page past a short viewport without growing
     it (kindleops/rei-automation d1d3efb7 found fixed boxes still short on a
     physical iPhone), the extra strip is <html>'s own --bg-deep, the colour the
     short viewport's tab bar already takes, and nothing of the page moves.
   - No overflow on <html>. The viewport is already overflow: hidden (body's,
     carried up to it because html's is visible), and bb's device-tested fix had
     html { overflow: hidden }, so a hidden viewport does not stop iOS growing
     it. Setting it on <html> would stop body's being carried up and make body a
     scroll box of its own. While the viewport is short the page can still be
     scrolled by the browser itself (a focused field brought into view): theme.js
     puts that back to 0 whenever no keyboard is up. overscroll-behavior: none
     stops the page itself bouncing; the scrolling lists inside it keep their own.
   - No other rule sets height or min-height on <html> (only "html, body
     { height: 100% }" above): one that came later or ranked higher on a phone
     would undo this without a sound. ux-ios-full-height-frontend.mjs fails on it. */
html.ios-app {
  min-height: min(100lvh, calc(100% + var(--safe-top)));
  overscroll-behavior: none;
}
html.ios-app body {
  height: 100dvh;
}

/* app.js (safeViewport) places menus, dropdown panels and the date calendar by
   hand, and a position has to be clamped to the safe area, not the screen. CSS
   can't hand the tokens' pixels over, so this empty box is padded by all four
   and its computed padding is read. It draws nothing and takes no room. */
.safe-area-probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  margin: 0;
  border: 0;
  padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
  visibility: hidden;
  pointer-events: none;
}

/* The installed iPhone app draws its status bar black-translucent: the page
   shows through it and the clock and battery are always white. Light theme puts
   a near-white page behind them (1.1 : 1 on the Dashboard, 2.4 : 1 over a sheet's
   scrim), so in the installed app the strip the bar sits on - exactly
   --safe-top high, which is 0 anywhere that has no status bar to clear - is
   painted dark. Over everything, the rotate prompt included (z-index: 100000).
   Dark theme needs nothing: its page is already dark behind white text. */
html.standalone[data-theme="light"]::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--safe-top);
  z-index: 100001;
  background: var(--status-strip-bg);
  pointer-events: none;
}

body {
  margin: 0;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background:
    var(--body-bg);
  color: var(--text);
  overflow: hidden;
}

/* The sidebar's width lives in a custom property, not straight in
   grid-template-columns, so the collapse toggle can transition it smoothly:
   grid track sizes are not reliably animatable across browsers when set
   directly, but a length typed with @property is - the browser interpolates
   --nav-rail-w frame by frame, and grid-template-columns (declared once
   below, reading the variable) simply recomputes from whatever it currently
   is. app.js's afterNavWidthSettles listens for this transition's end (by
   this property's own name) before resizing any map or chart on screen. */
@property --nav-rail-w {
  syntax: "<length>";
  inherits: false;
  initial-value: 15.875rem;
}

.app-shell {
  /* dvh tracks the real visible height on mobile (100vh is the URL-bar-hidden
     "large viewport", which left the shell's bottom hanging off-screen);
     identical to 100vh on desktop. Plain vh kept as the fallback. */
  height: 100vh;
  height: 100dvh;
  display: grid;
  --nav-rail-w: 15.875rem;
  grid-template-columns: var(--nav-rail-w) 1fr;
  grid-template-rows: auto auto 1fr;
  grid-template-areas:
    "banner banner"
    "update update"
    "nav main";
  /* The top is the status bar's in the installed app, so the banner, the update
     bar and the sticky header all start below it. 0 in a browser tab. */
  padding-top: var(--safe-top);
  background:
    var(--shell-bg);
  transition: --nav-rail-w 220ms ease;
}

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

.app-shell[hidden] { display: none; }

.app-superuser-banner {
  grid-area: banner;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: var(--banner-bg);
  color: var(--banner-fg);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: center;
}

.app-superuser-banner[hidden] { display: none; }

/* "Update ready": a deploy landed while this tab was open, so the page in front
   of the person is older than the one the server would give them. One slim row
   above the nav and the page, in the banner's own grid rows: it takes no height
   until it is shown, and pushes the shell down as the banner above it does
   (static/lib/update-watch.js decides when, app.js wires the button). Blue, the
   information colour - amber above is for platform admin and red is for
   trouble, and this is neither. */
.app-update-bar {
  grid-area: update;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 0.375rem 1rem;
  background: rgba(var(--rgb-blue), 0.16);
  border-bottom: 0.0625rem solid rgba(var(--rgb-blue), 0.4);
  color: var(--fg-body);
  font-size: 0.8125rem;
  font-weight: 600;
  text-align: center;
}

.app-update-bar[hidden] { display: none; }

.app-update-bar-button {
  min-height: 0;
  padding: 0.25rem 0.875rem;
  font-size: 0.8125rem;
}

.login-view {
  min-height: 100vh;
  min-height: 100dvh;
  /* Centred in what the status bar and the home indicator leave. */
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
  /* The app's own side gutter, so a phone narrower than the card keeps 1rem
     either side of it. */
  padding-inline: 1rem;
  display: flex;
  justify-content: center;
  /* A card taller than the window scrolls inside the view, since body never
     scrolls: the two-factor setup screen (its QR code and key) is taller than
     most laptop windows and every phone. The view is exactly the window's
     height (min- and max-height both 100dvh) and scrolls when its card does
     not fit. Vertical centring is the card's own auto block margins
     (.login-view > .login-card, THE SIGN-IN PAGE'S LOOK), not align-items:
     center, which would push the top of a tall card above the scroll range
     where nothing can reach it. */
  align-items: flex-start;
  max-height: 100vh;
  max-height: 100dvh;
  overflow-y: auto;
  /* Plain page colour: the shell's two glows are drawn over it by .login-sky
     (THE SIGN-IN PAGE'S LOOK), which moves them. */
  background: var(--bg);
}

.login-view[hidden] { display: none; }

/* The small-screen "desktop required" lockout that used to live here has been
   removed - the mobile shell (see the html.mobile section at the end of this
   file) replaces it. */

.login-card {
  width: 100%;
  max-width: 22.5rem;
}

.login-card .app-brand {
  justify-content: center;
  margin: 0 0 2.25rem;
}

.login-card button {
  margin-top: 1.125rem;
}

/* The sign-in card and the standalone auth pages (set-your-password invite,
   account-lock, password reset) are one panel: the same width and the same
   generous padding (THE SIGN-IN PAGE'S LOOK, Josh 2026-11-02: "taller login
   panel"), so moving from one to the next does not change the card's frame. */
#loginCard,
#acceptInviteView .login-card,
#securityLockView .login-card,
#passwordResetView .login-card {
  max-width: 27rem;
  padding: 3rem 2.25rem 2.25rem;
}
#acceptInviteView .login-title,
#securityLockView .login-title,
#passwordResetView .login-title {
  margin: 0 0 0.75rem;
}
#acceptInviteView .card-copy,
#securityLockView .card-copy,
#passwordResetView .card-copy {
  margin-bottom: 1.25rem;
}
#acceptInviteView .form-field,
#securityLockView .form-field,
#passwordResetView .form-field {
  margin-bottom: 0.875rem;
}

/* Login-page legal links (Terms of Use / Privacy Policy). */
.login-legal {
  margin-top: 1.25rem;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
}
.login-legal-sep { color: var(--fg-faint-3); }
.login-legal-link,
.legal-back-link,
.inline-link {
  margin: 0;                 /* override .login-card button margin */
  /* Text links, not controls: the shared button min-height would grow the line
     box around .inline-link, which sits inside a paragraph. */
  min-height: 0;
  padding: 0;
  background: none;
  border: 0;
  width: auto;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.login-legal-link:hover,
.legal-back-link:hover,
.inline-link:hover { color: var(--text); }

/* Pre-auth legal documents (Terms / Privacy) — wider, scrollable card. */
#termsView .login-card,
#privacyView .login-card {
  max-width: 40rem;
  padding: 2rem;
  max-height: 90vh;
  max-height: 90dvh;
  display: flex;
  flex-direction: column;
}
.legal-back-link {
  align-self: flex-start;
  margin-bottom: 1rem;
}

/* Sits under the full-width Verify on the two-factor step, centred, so it
   reads as the way out rather than a second action competing with it. */
.login-mfa-back {
  display: block;
  margin: 0.625rem auto 0;
}
.legal-title {
  display: block;
  font-size: 1.25rem;
}
.legal-scroll {
  overflow-y: auto;
  margin-top: 0.75rem;
  padding-right: 0.5rem;
}
.legal-scroll .support-section:first-of-type { border-top: 0; }
.legal-fill {
  color: var(--muted);
  font-style: italic;
}

/* Inviting org's branding on the set-your-password page. */
.invite-org-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4375rem;
  margin: 0 0 1.5rem;
  text-align: center;
}
.invite-org-logo {
  max-height: 3.5rem;
  max-width: 12rem;
  object-fit: contain;
}
.invite-org-name {
  font-size: 1.125rem;
  font-weight: 750;
  color: var(--fg-body);
}
.invite-org-sub {
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Centred, as the Sign in button and the hint below it are (Josh, 2026-10-19);
   a two-line message wraps centred. .login-notice below is the same line. */
.login-error {
  color: var(--danger);
  font-size: 0.875rem;
  margin-top: 0.875rem;
  text-align: center;
}

.login-error[hidden] { display: none; }

/* One notice on the sign-in card (ux:U2): "Session expired", "Trusted device
   removed" and "Offline, queued logs are safe" are all this, with role="status"
   in the markup. They read as the red line .login-error is: danger-coloured
   text with nothing round it (Josh, 2026-10-19). U2 drew them as a slate
   bordered box, quietly, on the reasoning that an expiry is information and not
   an error; on the card that box looked like a third text field under Email and
   Password. Same colour, size, gap and alignment as .login-error (centred),
   so the card does not move when a notice gives way to an error or the other
   way round. */
.login-notice {
  margin: 0.875rem 0 0;
  color: var(--danger);
  font-size: 0.875rem;
  text-align: center;
}

.login-notice[hidden] { display: none; }

/* THE SIDEBAR MUST DO TWO THINGS, AND THE OBVIOUS FIX FOR ONE BREAKS THE OTHER.

   1. KEEP THE ALERTS BELL ON SCREEN. The bell lives in .app-nav-footer at the
      bottom of this column, and it is the only route to alerts in the app.
      `body` is overflow:hidden and .app-shell a fixed 100dvh grid, so a column
      taller than the viewport had its foot cut off: measured in Chromium, an
      org admin (9 nav links - admins are exactly who has the bell) lost it
      below about 640px of viewport height, which is a 1366x768 laptop.

   2. NEVER CLIP. The alerts panel opens upwards from the bell and is wider than
      the sidebar, so it has to spill out over the page.

   `overflow-y: auto` HERE fixes (1) and breaks (2): an overflow other than
   `visible` on one axis forces the other axis to clip too, so the panel was
   cut off at the sidebar's right edge (Josh, 2026-09-28, with a screenshot).
   So this column never takes an overflow. `min-height: 0` lets it shrink to
   its grid row instead of growing to its content, and the NAV LINKS scroll
   instead (.app-nav-links below) - which also keeps the organisation and the
   bell pinned in view on a short window, rather than scrolling away with the
   links. nav-reach-gate.mjs asserts both halves. */
.app-nav {
  grid-area: nav;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: 1.75rem 1.125rem 1.5rem;
  background:
    var(--nav-bg);
  border-right: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  box-shadow: var(--shadow-nav);
}

/* Pinned to the sidebar's top-right corner, on the brand row and centred on
   the logo mark (mark 3.375rem, toggle 1.75rem: half the difference is the
   0.8125rem); its right edge is the nav's own content edge. Out of the flow, so
   it takes no row of its own and the links start right under the brand. The
   look is .chevron-toggle's; this rule only places it. Points left while the
   menu is open, to the edge it collapses toward. */
.app-nav {
  position: relative;
}

.app-nav-collapse-toggle {
  position: absolute;
  top: 2.5625rem;
  right: 1.125rem;
}

/* Keeps the brand text clear of the toggle: 1.75rem of button plus air. The
   row keeps the 3.375rem height the toggle is centred on (top: 2.5625rem). */
.app-nav .app-brand {
  min-height: 3.375rem;
  margin-right: 2.25rem;
}

/* Once collapsed it points at the rail's other edge: "expand" from here. */
.app-shell.nav-collapsed .app-nav-collapse-toggle {
  --chev-x: -0.125rem;
  --chev-rot: -135deg;
}

.app-shell.nav-collapsed {
  --nav-rail-w: 4.25rem;
}

.app-shell.nav-collapsed .app-nav {
  padding-left: 0.625rem;
  padding-right: 0.625rem;
}

/* No room beside the 2.25rem mark in a 4.25rem rail, so the toggle drops
   centred just below it. The brand's 0.25rem side margin puts the mark 0.125rem
   right of the rail's middle, hence -0.75rem rather than -0.875rem. The mark
   ends 4rem down and the brand's 2.625rem margin leaves the toggle's 1.75rem
   and a little air before the first link. */
.app-shell.nav-collapsed .app-nav-collapse-toggle {
  top: 4.375rem;
  left: 50%;
  right: auto;
  margin-left: -0.75rem;
}

/* Hidden the same way .visually-hidden is (never display:none): a screen
   reader still reads the label - app.js's navShowTooltip puts it back on
   screen, for a sighted mouse or keyboard user, on hover or focus. */
.app-shell.nav-collapsed .app-brand-text,
.app-shell.nav-collapsed .app-nav-link span:not(.nav-icon),
.app-shell.nav-collapsed .app-nav-context-text {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.app-shell.nav-collapsed .app-brand {
  justify-content: center;
  margin-right: 0;
}

/* The collapsed rail's own content width (4.25rem less .app-nav's collapsed
   padding and .app-brand's own margin) leaves about 2.75rem: the mark is
   fitted into a 2.25rem box with a little air either side, rather than either
   widening the rail just for this or letting it overflow into .app-main. */
.app-shell.nav-collapsed .app-brand-mark {
  width: 2.25rem;
  height: 2.25rem;
}

.app-shell.nav-collapsed .app-brand-mark .brand-art {
  width: 100%;
  height: auto;
}

.app-shell.nav-collapsed .app-nav-link,
.app-shell.nav-collapsed .app-nav-context {
  justify-content: center;
  padding: 0 0.5rem;
  gap: 0;
}

/* A 4.25rem sidebar has no room for the organisation logo and the bell side by
   side, so the footer stacks them - logo above bell, both centred - and the
   panel hangs from the bell's left edge as it does expanded. */
.app-shell.nav-collapsed .app-nav-footer {
  flex-direction: column;
  gap: 0.625rem;
}
/* Stacked and centred in the rail: the bell's 44px target is centred on its
   chip, its overhang (0.5625rem a side) over the rail's padding. The gap above
   is at least the target's vertical overhang (0.625rem), so the invisible target
   stops where the organisation button ends and never takes a click meant for
   its lower edge (the alerts-mine gate holds the two together). */
.app-shell.nav-collapsed .app-nav-footer .alerts-bell {
  justify-content: center;
  margin: -0.625rem -0.5625rem;
}
.app-shell.nav-collapsed .app-nav-footer .alerts-bell .nav-badge { right: 0.25rem; }

/* One shared tooltip node (app.js), moved and re-labelled per item rather
   than one per link - it only ever exists while the sidebar is collapsed.
   Fixed, not absolute: its position is read from the hovered/focused
   element's own getBoundingClientRect, which is already viewport-relative. */
.app-nav-tooltip {
  position: fixed;
  z-index: 70;
  transform: translateY(-50%);
  padding: 0.375rem 0.625rem;
  border-radius: 0.375rem;
  background: var(--popover-bg);
  color: var(--fg-strong);
  border: 0.0625rem solid var(--line);
  box-shadow: var(--shadow-menu);
  font-size: 0.8125rem;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
}

.app-nav-tooltip[hidden] { display: none; }

/* The DroneOps lockup (docs/BRAND.md): the mark and the wordmark as two
   images, so the collapsed rail can keep the mark and hide the word the way it
   hides every label. Both SVGs share the lockup's vertical frame, so at one
   height they sit exactly as in logo-lockup-*-v2.svg, and the gap is the
   lockup's own (49.4 of its 255 units). --brand-h is the lockup's height. */
.app-brand {
  --brand-h: 1.875rem;
  display: flex;
  align-items: center;
  gap: calc(var(--brand-h) * 0.1937);
  margin: 0 0.25rem 2.625rem;
}

.app-brand-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}

.app-brand-text { display: flex; min-width: 0; }

.app-brand .brand-art {
  display: block;
  height: var(--brand-h);
  width: auto;
  max-width: 100%;
}

/* One artwork per theme, chosen by the stylesheet: navy ink on the light
   theme, white on the dark one (dark is the default before theme.js runs). */
html:not([data-theme="light"]) .brand-art-light,
html[data-theme="light"] .brand-art-dark {
  display: none;
}

/* The links are what scroll on a short window, so the footer with the
   organisation and the alerts bell stays pinned at the bottom (see .app-nav).
   A scroller clips, so the inline padding with a matching negative margin moves
   its clipping edge outside the links: the active link's green glow and every
   keyboard focus ring extend past the link itself, and cropping a focus ring
   is an accessibility regression, not a cosmetic one. Block padding without a
   negative margin, so the scroller never overlaps the collapse button above it
   and takes its clicks. `align-content: start` keeps the rows their own height
   when there is room to spare. */
.app-nav-links {
  display: grid;
  gap: 0.625rem;
  align-content: start;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.375rem;
  margin-inline: -0.375rem;
  /* Tabbing to a link that is scrolled out of view, the browser scrolls only
     until the LINK'S OWN edge meets the edge of the list - so the padding above
     stays hidden and the top or bottom of the focus ring is cut off. Measured
     in Chromium walking the list by keyboard: -2.5 to -2.9px of ring room
     without this, +2.1 to +2.3px with it. */
  scroll-padding-block: 0.375rem;
  /* Not inherited, so the root's thin scrollbar never reached this one: on
     Windows a classic 15px scrollbar with arrow buttons took the sidebar. */
  scrollbar-width: thin;
}

.app-nav-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.875rem;
  min-height: 3.625rem;
  padding: 0 1.125rem;
  border-radius: 0.5rem;
  color: var(--fg-nav);
  text-decoration: none;
  font-size: 1.125rem;
  background: transparent;
  transition: background 120ms ease, color 120ms ease;
}

.app-nav-link[hidden] { display: none; }

.app-nav-link:hover {
  background: rgba(var(--rgb-overlay), 0.055);
  color: var(--fg-strong);
}

.app-nav-link.active {
  color: var(--fg-strong);
  background: var(--active-item-bg);
}

.app-nav-link.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0.25rem;
  border-radius: 0.5rem 0 0 0.5rem;
  background: var(--green);
  box-shadow: var(--glow-active);
}

.app-nav-link.disabled { cursor: default; }

.app-nav-action {
  border: 0.0625rem solid rgba(var(--rgb-green), 0.4);
  font: inherit;
  cursor: pointer;
  margin-bottom: 0.625rem;
  color: var(--fg-on-green-tint);
  background: rgba(var(--rgb-green), 0.12);
}

.app-nav-action:hover {
  background: rgba(var(--rgb-green), 0.22);
  color: var(--fg-strong);
}

.nav-icon {
  width: 1.75rem;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Inherit the link's colour so the icon (a currentColor SVG) brightens with
     the active/hover state - and renders identically on every platform,
     unlike the emoji glyphs it replaced. */
  color: inherit;
}
.nav-icon svg {
  width: 1.25rem;
  height: 1.25rem;
  display: block;
}

/* The organisation and the person, with the alerts bell on their right (Josh,
   2026-09-27). A row, so the bell sits beside the name rather than under it;
   the name block takes the space that is left and ellipsises, the bell never
   shrinks. */
.app-nav-footer {
  margin-top: auto;
  padding-top: 0.625rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.app-nav-footer .alerts-menu {
  flex: 0 0 auto;
}

/* Compact here, and only here: every pixel the bell takes in a 15.875rem sidebar
   comes off the organisation's name beside it, which is the one thing in the
   footer that has to be readable. Trimmed like this the name keeps about
   fourteen characters before it ellipsises, against eleven at the phone bar's
   sizing. The phone has the whole screen width and keeps the touch-sized bell. */
.app-nav-footer .alerts-bell {
  font-size: 1rem;
}
/* THE BELL'S TOUCH TARGET IS 44 PX HERE TOO. Since every member has the bell (P3)
   it is their only way to their alerts, and this layout also serves a tablet
   that asks for the desktop site - but the chip a person sees stays the compact
   one above. So the BUTTON is the 44px target (3.25rem at the 85% root), with no
   background of its own, and the chip is drawn on the icon inside it, where it
   was. The extra lies where nothing else is: above and below, inside the
   footer row's height, and to the right, over the sidebar's own padding - the
   negative margins take it back out of the layout, so the organisation's name
   keeps every pixel it had. */
.app-nav-footer .alerts-bell {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  min-width: 3.25rem;
  min-height: 3.25rem;
  padding: 0;
  margin: -0.625rem -1.125rem -0.625rem 0;
  background: transparent;
}
.app-nav-footer .alerts-bell-icon {
  padding: 0.4375rem 0.5rem;
  border-radius: 0.625rem;
  background: var(--secondary-bg);
}
/* The badge rides the chip's corner, not the target's: 0.625rem down and
   1.125rem in from the button's, less its own 0.3125rem overhang. */
.app-nav-footer .alerts-bell .nav-badge {
  top: 0.3125rem;
  right: 0.8125rem;
}
.app-nav-footer .app-nav-context {
  padding-left: 0.625rem;
  padding-right: 0.5rem;
}

.app-nav-context {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  /* In the footer row: take what is left beside the bell, and be allowed to
     shrink to it (a flex item's floor is its content otherwise, which pushed
     the bell out of the sidebar on a long organisation name). */
  flex: 1 1 auto;
  min-width: 0;
  /* Symmetric vertical padding so the org/user content sits centred inside the
     hover highlight (previously top:0 / bottom:12px left it hugging the top). */
  padding: 0.75rem 1.125rem;
  background: transparent;
  border: 0;
  text-align: left;
  font: inherit;
  cursor: pointer;
  border-radius: 0.5rem;
}

.app-nav-org-logo {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 0.375rem;
  object-fit: contain;
  flex: none;
}

.app-nav-org-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 0.375rem;
  background: rgba(var(--rgb-slate), 0.14);
  font-size: 0.9375rem;
  flex: none;
}

.app-nav-org-fallback[hidden] { display: none; }

.app-nav-context-text {
  min-width: 0;
  flex: 1;
}

.app-nav-context:hover {
  background: rgba(var(--rgb-overlay), 0.045);
}

.login-switch-link {
  text-align: center;
  margin: 1rem 0 0;
}

.login-switch-link a {
  color: var(--green);
  text-decoration: none;
  font-weight: 650;
}

.login-switch-link a:hover {
  text-decoration: underline;
}

.app-nav-org {
  color: var(--text);
  font-weight: 750;
  font-size: 0.9375rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-nav-username {
  color: var(--muted);
  font-size: 0.8125rem;
  margin-top: 0.125rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-main {
  grid-area: main;
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100%;
  overflow: hidden;
  /* The bottom gutter used to come from the empty .app-footer that sat last in
     this flex column, not from here - so removing that element left the
     full-height pages with their bottom card border, and its radius, cut off by
     the window edge. Stated here now, where the style guide puts desktop
     gutters. html.mobile .app-main overrides the whole shorthand. */
  padding: 1.125rem 1.375rem 1.125rem 1.75rem;
}

/* The page header is no longer a title bar (Josh, 2026-09-27: the highlighted
   nav tab says which page you are on). What is left is the visually hidden h1,
   the phone's sticky bar - the organisation's name and the alerts bell, both
   display: none on a desktop - and the page notice, which floats and takes no
   height. So on a desktop it is NOTHING, and it has to cost nothing: no margin
   of its own. (It is positioned, so the notice has something to hang from: see
   the ux:U0b block.) */
header {
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
}

.header-top-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

h1 {
  margin: 0 0 0.125rem;
  font-size: 1.4375rem;
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-weight: 820;
}

.app-page {
  display: block;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.app-page[hidden] {
  display: none !important;
}

.placeholder-page {
  max-width: 45rem;
}

/* THE MAP NEVER MOVES (Josh, 2026-09-27: "rather than moving the map when the
   logs list is folded out, just display the log list over the map without moving
   it"). TWO tracks, always - the Jobs pane, then the map - and the log list is
   laid over the map's own cell rather than taking a track of its own. Showing
   and hiding it changes no track width, so the map is never re-laid, never
   resized, and never re-fits its view.
   It used to be a third track that came and went, which is also why the widths
   are variables: the collapsed state re-declared the whole track list, and a
   version that forgot to lose a track with the column laid the map into the
   empty middle one, where it came out zero-wide. There is nothing left to get
   wrong - app/frontend-smoke/flights-layout-gate.mjs holds the shape. */
.layout {
  display: grid;
  /* The Jobs pane's width, and the width of the list that floats over the map.
     A LENGTH for the list now, not a grid track: it is positioned in the map's
     cell, where a track function has no meaning. */
  --filters-col: 28.75rem;
  --flights-col: 22.5rem;
  grid-template-columns: var(--filters-col) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 0.75rem;
  height: 100%;
}
.layout > .filters-column { grid-area: 1 / 1; }
/* Both the map and the list are placed in the second cell, explicitly. Auto
   placement will not do it: the list is placed first (it carries a definite
   position), which leaves the map to be auto-placed past it into an implicit
   SECOND ROW, off the bottom of the page. */
.layout > .map-shell { grid-area: 1 / 2; }

/* The log list, over the map's left edge: the map's own cell, its own width,
   pushed to the start of the cell, and above the map. z-index works without
   position here because it is a grid item. It stretches to the cell's full
   height, so its left, top and bottom edges line up with the map's - and its
   own scrolling list (#flightLayerList below) is what runs long, so nothing
   about the page scrolls with it.
   z-index 4: over the map's imagery and over MapLibre's own control corners (2),
   which is what makes the corner rules further down necessary; under the map's
   top bar (5), its loading badge (9) and the "map couldn't load" cover (10),
   each of which is moved clear of the list rather than drawn across it. */
.flights-column {
  grid-area: 1 / 2;
  justify-self: start;
  width: var(--flights-col);
  max-width: 100%;
  z-index: 4;
  overflow-y: hidden;
}
/* Hidden outright, not shrunk to a strip: the map is whole again, and the Jobs
   header's chevron is the one control that brings the list back (see
   applyFlightsCollapsed). The Jobs pane has no collapsed width because it no
   longer collapses. */
.layout.flights-collapsed .flights-column { display: none; }

/* Readable over satellite imagery, and lifted off it. Same construction as the
   Airspace map's legend card, which is the app's other panel that floats on a
   map: the float background plus a blur behind it, because a flat translucent
   panel over a high-contrast aerial is what makes a list of log names hard to
   read. */
.flights-column .flights-list-card {
  background: var(--map-float-bg);
  backdrop-filter: var(--backdrop-float);
  box-shadow: var(--shadow-map-float);
}

/* WHAT THE LIST WOULD OTHERWISE BURY. Everything below is a piece of the map's
   own furniture that lives where the list now sits, moved clear of it for as
   long as the list is shown - not hidden, and not drawn across. Each reads
   --flights-col, so one width governs the list and everything that steps around
   it.
   Only while the list is shown: `:not(.flights-collapsed)`, which is also how
   the phone is left alone - the class stays on there for the whole visit,
   because on a phone the list is a bottom sheet and these rules must not fire. */
/* The scale bar is MapLibre's bottom-left corner, which is the list's bottom
   edge. It steps to the right of the list; the credit keeps the bottom-right
   corner, where the list never reaches. */
.layout:not(.flights-collapsed) .maplibregl-ctrl-bottom-left {
  left: var(--flights-col);
}
/* The top bar and the loading badge are both centred on the map. Centred on the
   part of the map you can still SEE: its middle is half the list's width to the
   right of the map's own. The bar is capped to what is left as well, so a long
   bar wraps to a second row instead of running under the list. */
.layout:not(.flights-collapsed) .map-context-bar,
.layout:not(.flights-collapsed) .map-loading-badge {
  left: calc(50% + var(--flights-col) / 2);
}
.layout:not(.flights-collapsed) .map-context-bar {
  max-width: calc(100% - var(--flights-col) - 7rem);
}
/* "The map couldn't load" covers the map and its controls on purpose - but its
   own second line is "Your flight list and filters still work", so it must not
   cover the list saying so. It starts where the list ends. */
.layout:not(.flights-collapsed) #mapUnavailable {
  left: var(--flights-col);
}

/* The filters card is sized to its own content and never shrinks, so on a
   laptop-height viewport it ran past the bottom of the page and took the
   "Upload log(s)" button - this page's primary action - off screen entirely.
   The column scrolls instead. */
.filters-column {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

/* Filters card header: title + the show/hide chevron, aligned like the flights
   list header beside it so the two headings sit on one line. */
.filters-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.625rem;
}

/* Both headers get a fixed height and their title's margin zeroed, so the two
   headings - and the chevron on the Jobs one - line up whatever each header is
   currently holding.
   A HEIGHT, not a min-height: a header with controls came out at 28.9px and one
   without at the 25.5px minimum, so the two sat 1.7px apart. 2.125rem is the
   taller of the two, and clears everything a header holds (tallest: 23.8px). */
.filters-card-header,
.flights-list-header {
  height: 2.125rem;
}
/* .card-qualified: `.card strong` (later in the file, same specificity) gives
   every card heading a bottom margin, and it was winning - the header centred
   the heading WITH that margin, lifting it 4px above the collapse chevron. */
.card .filters-card-header > strong,
.card .flights-list-header > strong {
  margin: 0;
}

/* Keeps the summary card + upload button anchored to the bottom of the
   sidebar as a unit, via margin-top: auto on the group itself - so the
   button stays bottom-anchored even when the summary card inside is hidden
   (an auto margin on the summary card alone would stop working the moment
   it's removed from the flex layout). On a computer Upload logs then ends on
   the column's own bottom edge (see html:not(.mobile) #uploadLogButton). */
.filters-bottom-group {
  display: flex;
  flex-direction: column;
  margin-top: auto;
}

.filters-column .summary-card {
  /* Never shrinks/clips - JS (updateSummaryCardVisibility) hides it outright
     with [hidden] once the sidebar's actual content no longer fits, rather
     than letting it squash down into a half-visible sliver. */
  flex: 0 0 auto;
}

.filters-column .summary-card[hidden] {
  display: none;
}

/* The Summary card has no heading on the page any more (Josh, 2026-11-02:
   "remove the 'summary' label"; the group keeps the name for a screen reader,
   see index.html). The grid's own margins were spacing for a heading above
   it, so without one they go and the card's padding is the whole gap, the
   same on all four sides. The tiles themselves are untouched. */
html:not(.mobile) .filters-column .summary-card > .stats-grid {
  margin: 0;
}

/* The Jobs card runs down the column to meet the Summary card (or the Upload
   button, while Summary is hidden), with the job list taking the height it
   gains - it used to stop at its own content and leave a gap above them.
   Grows, never shrinks: updateSummaryCardVisibility() in app.js hides Summary
   when the column overflows, and growing only into FREE space leaves that
   test measuring what it always did - the card's own minimum (the job list's
   floor, see .filters-card .job-filter-list) against the column. */
.filters-card {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.flights-list-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  margin-bottom: 0;
}

.flights-list-card #flightLayerList {
  flex: 1;
  max-height: none;
  min-height: 0;
  overflow-y: auto;
}

.flights-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.625rem;
}

/* The list's own actions, under the list. Equal widths (flex-basis 0, so the
   text length stops deciding the size) on one row, and the row is capped and
   centred rather than stretched edge to edge - two buttons spanning a 420px
   column would read as a toolbar rather than a pair of choices.

   It is a sibling of #flightLayerList inside the flex-column card, so the list
   keeps flex: 1 and this takes its natural height. Hiding the list hides the
   whole column, this with it. */
.flights-list-footer {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  padding: 0.625rem 0 0.125rem;
  flex: 0 0 auto;
}

.flights-list-footer .secondary-button {
  flex: 1 1 0;
  max-width: 11rem;
  padding: 0.3125rem 0.75rem;
  font-size: 0.75rem;
}

.flights-list-header-actions {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.flights-list-header-actions .secondary-button {
  /* Compact chips (§5) — row/list-level actions, at the same 0.75rem as every
     other compact chip in the app; the shared `button` min-height keeps them a
     usable height. */
  width: auto;
  padding: 0.25rem 0.75rem;
  font-size: 0.75rem;
}

/* How much each flight row says. "Name only" is for the days a spray pilot has
   two hundred of them and is looking for one. */
/* "Details" - the flight list's density, as a checkbox. */
.flights-details-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  flex: none;
  font-size: 0.75rem;
  color: var(--fg-label);
  cursor: pointer;
  white-space: nowrap;
}
.flights-details-toggle input {
  margin: 0;
  accent-color: var(--green);
  cursor: pointer;
}

/* Name only: the name's line alone. The needs-review pill and the loading /
   failed status are on that line (renderFlightLayerList), so trimming the
   rest never hides a warning. */
.flights-list-card.density-name .queue-item .queue-meta {
  display: none;
}

.flights-list-card.density-name .queue-item {
  padding-top: 0.375rem;
  padding-bottom: 0.375rem;
}

/* THE CHEVRON BUTTON - one look, shared by the Jobs header's show / hide control
   (.flights-collapse-toggle) and the sidebar's collapse control
   (.app-nav-collapse-toggle), so the two cannot drift apart (Josh, 2026-09-29).
   A bare chevron, larger, with no box round it. DRAWN, not typed: the "‹" / "›"
   characters the Flights script still writes (for anything reading the text) are
   punctuation - at 24px a "‹" is 4 x 9px of ink - so the text is hidden and the
   chevron is two borders turned 45deg. Still a real button, with the app's focus
   ring; the 1.75rem square is the hit area, and it fits the fixed-height column
   header (2.125rem).
   Each control sets only where it sits and which way it points, through three
   custom properties: --chev-x / --chev-y nudge the drawn point (x centres the
   point rather than the square; y is 0 unless the neighbour's text needs it) and
   --chev-rot is 45deg pointing left, -135deg pointing right. */
.chevron-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  min-height: 0;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0.375rem;
  color: var(--fg-label);
  font-size: 0;
  --chev-x: 0.125rem;
  --chev-y: 0rem;
  --chev-rot: 45deg;
}
.chevron-toggle::before {
  content: "";
  width: 0.625rem;
  height: 0.625rem;
  border-left: 0.140625rem solid currentColor;
  border-bottom: 0.140625rem solid currentColor;
  transform: translate(var(--chev-x), var(--chev-y)) rotate(var(--chev-rot));
}
.chevron-toggle:hover {
  color: var(--fg-strong);
}

/* Jobs header: pointing left while the list is shown, right while it is hidden.
   The y lift puts the chevron on the middle of the headings' capitals, which sit
   2.2px above the middle of their line box (measured, Jobs and Flights alike) -
   so it lines up with the heading beside it. */
.flights-collapse-toggle {
  --chev-y: -0.1625rem;
}
.layout.flights-collapsed #filtersCollapseToggle {
  --chev-x: -0.125rem;
  --chev-rot: -135deg;
}

#jobsPage .settings-layout {
  align-content: start;
}

.settings-layout {
  display: grid;
  gap: 1rem;
  max-width: 40rem;
  overflow-y: auto;
  height: 100%;
  /* Pack cards to the top: a grid at 100% page height would otherwise stretch
     each card to fill its row track, leaving a band of dead background inside a
     bordered card when the content is short (Profile, Organisation, etc — §4).
     Entity-list panels (Clients/Aircraft) and the Users master-detail set their
     own display (flex/block) and are unaffected by this grid property. */
  align-content: start;
}

/* Entity-list pages (Clients, Aircraft; sections of Settings until 2026-11-02,
   with these rules): the primary card fills the fixed-height panel and its
   scrolling list flexes to fill the card, right down to the + Add button - no
   short capped list floating above empty panel (guide §4/§7/§9). Mirrors the
   Users panel's personnel-list-wrap treatment. */
:is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] {
  display: flex;
  flex-direction: column;
}
:is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] > .card {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
:is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] .records-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
/* The lists sit INSIDE that scroller now (the active rows, then the collapsed
   group of inactive / retired ones), so the scroller is the one box that
   scrolls and the lists are plain blocks in it. */
:is(#clientsPage, #aircraftPage) .records-scroll .queue-list {
  max-height: none;
  overflow: visible;
}
/* Documents & Reporting, desktop only (the phone page scrolls and boxes nothing).
   (Written when the page showed both lists in one card; each tab shows one now, so the
   share of the card between two lists no longer applies, and what is left is the floors:
   a list is never squeezed to a sliver on a short window.)
   A row is one line and its meta, --doc-row-h (48 px at the 85% root), and a list is
   boxed in its section and scrolls there. Floors, so no list is ever squeezed to a
   sliver while the other is long (the 0px bug class): every list that holds a row
   keeps one whole row; two or more keep two, five or more keep five, each while the
   card has the height - they give way (down to one row) before the two floors would
   overflow the card, which is --doc-half: half of what is left of the window once the
   page chrome and both sections' headings and filters are paid for. A section's floor
   is its list's floor plus its own heading and filters (--doc-sec-chrome, which
   follows whether the type chips or the report filters are showing), and for the
   reports section the padding and rule that divide the two, or the list overhangs
   the section and paints into the card's padding. Both lists long share the card
   equally (a zero basis), and a short one is only as tall as its rows. */
#documentsPage {
  --doc-row-h: 3.5rem;
  --doc-row-gap: 0.375rem;
  --doc-pitch: calc(var(--doc-row-h) + var(--doc-row-gap));
}
html:not(.mobile) #documentsPage {
  --doc-half: max(var(--doc-row-h), calc((100vh - 29rem) / 2));
}
html:not(.mobile) #documentsPage .document-section {
  --doc-floor: 0rem;
  --doc-sec-chrome: 2rem;
  min-height: calc(var(--doc-sec-chrome) + var(--doc-floor));
}
html:not(.mobile) #documentsPage #uploadedDocumentsSection:has(> .document-filterbar:not([hidden])) {
  --doc-sec-chrome: 4.9rem;
}
html:not(.mobile) #documentsPage #generatedReportsSection {
  --doc-sec-chrome: 4.6rem;
}
html:not(.mobile) #documentsPage #generatedReportsSection:has(> .document-filterbar:not([hidden])) {
  --doc-sec-chrome: 7.5rem;
}
html:not(.mobile) #documentsPage .document-section:has(> .queue-list > .queue-item) {
  --doc-floor: var(--doc-row-h);
}
html:not(.mobile) #documentsPage .document-section:has(> .queue-list > .queue-item:nth-child(2)) {
  --doc-floor: min(calc(2 * var(--doc-pitch) - var(--doc-row-gap)), var(--doc-half));
}
html:not(.mobile) #documentsPage .document-section:has(> .queue-list > .queue-item:nth-child(5)) {
  --doc-floor: min(calc(5 * var(--doc-pitch) - var(--doc-row-gap)), var(--doc-half));
}
html:not(.mobile) #documentsPage .document-section:has(> .queue-list > .queue-item:nth-child(6)) {
  flex: 1 1 0;
}
html:not(.mobile) #documentsPage .document-scroll {
  min-height: var(--doc-floor);
}
/* An empty list is one line of text: as tall as that line, never grown into
   the slack (dead space) and never shrunk below it (clipped to 0px). */
html:not(.mobile) #documentsPage .document-section:has(> .queue-list.empty) {
  flex: 0 0 auto;
}
html:not(.mobile) #documentsPage .queue-list.empty {
  flex: 0 0 auto;
}
/* The + Add buttons stay content-width (§5). Both panels carry them in the
   card's header row now (.records-head, under the P8 anchor below), top right
   on every layout, so neither needs the below-the-list placement that left
   them at the foot of a long phone page. */
.settings-action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: flex-start;
}
/* Full-height list filling is a desktop concern. On mobile the page is the
   single scroller and every panel flattens to natural height, so revert these
   panels to natural flow: no flex column, no card fill, no nested list
   scroller (guide §10). */
html.mobile :is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] {
  display: block;
}
html.mobile :is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] > .card {
  display: block;
  flex: none;
}
html.mobile :is(#clientsPage, #aircraftPage) .settings-layout[data-records-panel] .records-scroll {
  flex: none;
  overflow-y: visible;
}

/* .settings-layout sets its own `display`, which would otherwise silently
   defeat the native [hidden] UA rule for the tab panels toggled via hidden. */
.settings-layout[hidden] {
  display: none;
}

/* Settings, and the three pages made of its former sections (Account, Clients,
   Aircraft, 2026-11-02): a rail (Settings, Account) beside the panel on show. */
:is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) {
  display: flex;
  flex-direction: row;
  gap: 1.5rem;
  min-height: 0;
}

:is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) .settings-layout {
  flex: 1;
  height: auto;
  min-height: 0;
}

/* Preferences & Compliance: pack the cards to the top so the grid doesn't
   stretch them apart and leave dead space inside each card (guide §4). */
#accountPage .settings-layout[data-account-panel="preferences"],
#settingsPage .settings-layout[data-settings-panel="compliance"] {
  align-content: start;
}
/* The 1rem grid row-gap already spaces stacked cards; drop the card's own
   margin-bottom so grid-gap + margin don't stack into an oversized, non-uniform
   gap (§4). Applies to every settings panel — the container's gap owns spacing. */
:is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) .settings-layout > .card {
  margin-bottom: 0;
}

/* Containers that space their cards with a grid/flex `gap` zero the card's own
   margin, so the gutter is the gap and nothing else - otherwise the two stack
   and the vertical channel comes out wider than the horizontal one. */
.jobs-page-layout > .card,
.dashboard-main > .card,
.dashboard-charts-row > .card,
.dashboard-bottom-row > .card,
.reports-layout > .card,
.reports-preview > .card {
  margin-bottom: 0;
}

/* Calendar feed actions are a normal settings button row, not a modal footer:
   content-width peers (§5), not stretched full-width by .modal-actions-row. */
#calendarFeedActive .modal-actions-row button { flex: 0 0 auto; }

/* A dividing line in a section rail between sections for different people or
   purposes: above Platform in Settings (shown to platform admins only), and
   between the person's own sections and Help in Account. Since 2026-11-02 the
   rails are short enough to need no group labels (Settings holds the company-wide
   sections alone; the person's own are on the Account page). */
.settings-tabs-spacer {
  margin-top: 0.375rem;
  padding-top: 0.375rem;
  border-top: 0.0625rem solid var(--line);
}

/* Support and Feedback take the full width like every panel (settings:canvas):
   Support's guide and policy sit side by side under the contact banner, and
   Feedback's form beside the list of requests. */

/* Privacy policy card on the Support page. */
.privacy-policy { margin-top: 1.125rem; }
.privacy-updated {
  margin: 0.125rem 0 0.625rem;
  font-size: 0.75rem;
  color: var(--muted);
}
/* Support guide & privacy policy: collapsible module sections. */
.support-card { max-width: none; }
.support-section { border-top: 0.0625rem solid var(--line); }
.support-card > .support-section:first-of-type { border-top: 0; }
.support-section > summary {
  cursor: pointer;
  padding: 0.75rem 0.25rem;
  /* Emphasis style (§3) - inheriting the 1rem page context drifts off-scale. */
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.support-section > summary::-webkit-details-marker { display: none; }
.support-section > summary::before {
  content: "\25B8";
  color: var(--muted);
  font-size: 0.85em;
  transition: transform 0.15s ease;
}
.support-section[open] > summary::before { transform: rotate(90deg); }
.support-body { padding: 0 0.25rem 0.875rem 1.375rem; color: var(--muted); font-size: 0.875rem; }
.support-body p { margin: 0 0 0.625rem; line-height: 1.55; }
.support-body ul { margin: 0 0 0.625rem; padding-left: 1.125rem; }
.support-body li { margin-bottom: 0.375rem; line-height: 1.5; }
.support-body strong { color: var(--text); }
.support-body em { color: var(--text); font-style: italic; font-weight: 500; }
.support-body a { color: var(--green); }

/* Feature requests panel: both cards hug their content (§4); the panel column
   scrolls when the request list grows past the page. */
#accountPage .settings-layout.feature-requests-layout {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.feature-request-form-card { flex: none; }
.feature-request-list-card { flex: none; }

/* Feature requests: the user's own list + platform table cell. */
.feature-request-row { padding: 0.75rem 0; border-top: 0.0625rem solid var(--line); }
.feature-request-row:first-child { border-top: 0; }
.feature-request-body { color: var(--text); white-space: pre-wrap; line-height: 1.5; font-size: 0.875rem; }
.feature-request-meta { margin-top: 0.25rem; font-size: 0.8125rem; color: var(--muted); }
.feature-request-cell { white-space: pre-wrap; max-width: 30rem; }

#riskTemplateNewButton,
#riskMatrixEditButton { width: auto; }
#riskTemplateNewButton { margin-top: 0.75rem; }

/* Platform admin uses the full panel width (settings:canvas: every panel does),
   so the wide audit-log table fits without a horizontal scrollbar. */

/* The rail scrolls: it sits inside an overflow:hidden page box, so on a short
   window its bottom entries ("Feature requests", "Support") were cut off at the
   page edge with no scrollbar and no way to reach them. */
.settings-tabs {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: none;
  /* Wide enough that the longest label ("Risk assessment", 136px including the
     tab's own padding at the 85% root scale) sits comfortably rather than
     3px off the edge, with room for a longer one before it wraps. */
  width: 13.75rem;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}
/* Platform ops dashboard runs full-width - drop the tab column on desktop.
   On mobile the tabs are a horizontal chip row (not a column), so keep them. */
#settingsPage.settings-fullwidth-panel .settings-tabs { display: none; }
html.mobile #settingsPage.settings-fullwidth-panel .settings-tabs { display: flex; }

/* Platform account: the sidebar carries the platform sections directly, so the
   settings tab column and the in-panel platform sub-tab strip are redundant.
   Desktop only - mobile has no sidebar, so it keeps these chip rows to navigate. */
body.platform-admin-view #settingsPage .settings-tabs,
body.platform-admin-view #settingsPage .platform-subtabs { display: none; }
html.mobile body.platform-admin-view #settingsPage .settings-tabs,
html.mobile body.platform-admin-view #settingsPage .platform-subtabs { display: flex; }

/* Styled like the main sidebar nav's tabs (.app-nav-link) rather than as
   discrete outlined buttons - transparent by default, a subtle hover
   highlight, and an active state with a glowing green left accent bar. */
.settings-tab {
  position: relative;
  width: 100%;
  min-height: 2.5rem;
  text-align: left;
  padding: 0.625rem 1rem;
  border: 0;
  border-radius: 0.5rem;
  color: var(--fg-nav);
  background: transparent;
  font-size: 1.0625rem;
  transition: background 120ms ease, color 120ms ease;
}

.settings-tab:hover {
  background: rgba(var(--rgb-overlay), 0.055);
  color: var(--fg-strong);
}

.settings-tab.active {
  color: var(--fg-strong);
  background: var(--active-item-bg);
}

.settings-tab.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0.25rem;
  border-radius: 0.5rem 0 0 0.5rem;
  background: var(--green);
  box-shadow: var(--glow-active);
}

/* ==========================================================================
   SETTINGS SHARED
   The primitives every Settings tab and dialog uses (design 5.1), written once:
   the "Saved" beside a control, the section title, the scope line, the link
   into another section, the loading / empty / failed rows of a list, and the
   dialog whose action row stays on screen. Rules that belong to ONE tab go
   under that tab's anchor at the foot of this block (and, for the phone, at the
   foot of the html.mobile Settings block). The packages: P1 shell and
   navigation, P2 your account, P3a organisation save path, P3b organisation
   layout, P4 billing, P5 users, P6 compliance, P7 risk and checklists, P8
   clients and aircraft, P9 support and feedback.
   ========================================================================== */

/* "Saved" or the error, beside a control that saves as soon as it changes
   (flashSaved). The element is a live region that exists, empty, before its text
   changes, so the margin is on :not(:empty) and an empty one takes no room. No
   motion. */
.save-status {
  font-size: 0.8125rem;
}
.save-status:not(:empty) {
  margin-inline-start: 0.5rem;
}
.save-status.is-saved { color: var(--green); }
.save-status.is-error { color: var(--danger); }

/* The title of a card in Settings, as a real heading. Reproduces to the pixel
   what `.card > strong` has always computed to (1.25rem, 1.2 line, 0.625rem
   under), so converting <strong> to <h2 class="card-title"> changes nothing on
   screen and gives a screen reader a heading to jump to. */
.card-title {
  display: block;
  margin: 0 0 0.625rem;
  font-size: 1.25rem;
  line-height: 1.2;
  font-weight: bolder;
}

/* The first line of a section: who it affects. Same rhythm as .card-copy. */
.scope-line {
  margin: -0.125rem 0 1rem;
  color: var(--muted);
  font-size: 0.875rem;
}

/* A button that reads as a link, to another Settings section
   (data-settings-link, handled by one delegated listener). */
.settings-link {
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  width: auto;
  font: inherit;
  color: var(--blue);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}
.settings-link:hover:not(:disabled) { color: var(--fg-info-hover); filter: none; }
.settings-link:disabled { background: none; cursor: not-allowed; }

/* A list that is not showing rows (setListState): loading, empty, or failed
   with Retry. One row replaces the list's contents. */
.list-state {
  color: var(--fg-empty);
  font-size: 0.875rem;
  list-style: none;
}
li.list-state,
div.list-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 0;
}
tr.list-state > td {
  padding: 0.75rem 0;
}
.list-state-error .list-state-text { color: var(--fg-error); }
.list-state-retry,
.list-state-action {
  min-height: 0;
  margin-inline-start: 0.625rem;
  padding: 0.25rem 0.75rem;
  font-size: 0.8125rem;
}
/* The list a Retry hands the keyboard to (keepFocusInList): focus lives there,
   Tab goes on into the rows, so no ring round the whole box. */
[data-focus-host]:focus { outline: none; }

/* A dialog whose action row stays on screen while the fields scroll. Opt in
   with `.modal-card.pinned-actions` and this shape inside it:
     .modal-header, then <form> holding .modal-body (the fields) and
     .modal-actions (the buttons), or .modal-body and .modal-actions directly.
   Generalised from #jobCreateCard, which does the same for the job wizard. A
   long form used to push Save off the bottom of a laptop screen; the card is
   capped at 82vh by .modal-card, the body takes what is left and scrolls, and
   the buttons are a flex sibling that never moves. Buttons stay content-width
   (`.modal-actions` is the wrapper, as everywhere). `scroll-padding-bottom` keeps
   a field that takes focus clear of the row's edge. */
.modal-card.pinned-actions {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-card.pinned-actions > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.modal-card.pinned-actions .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-padding-bottom: 1rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
  /* The scroll box clips on every side, which would cut a focus ring at the
     edge of the first and last field: pad it back out and cancel the padding
     with a negative margin so the fields stay where they were. */
  padding: 0.25rem 0.375rem 0.5rem;
  margin: -0.25rem -0.375rem 0;
}
.modal-card.pinned-actions .modal-actions {
  flex: 0 0 auto;
  margin: 0;
  padding-top: 0.75rem;
  border-top: 0.0625rem solid var(--line);
}
/* The phone's sheet is a full-height card that scrolls as a whole; a pinned one
   scrolls its body instead. The home indicator is the overlay's to clear (it
   pads the sheet in from every edge), so the row needs only its own gap. */
html.mobile .modal-overlay:not(.popover):not(.modal-confirm) > .modal-card.pinned-actions {
  overflow: hidden;
  padding-bottom: 1.125rem;
}

/* ---- Per-package anchors (desktop) --------------------------------------- */
/* settings:P1 */
/* Shell and navigation: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* "Unsaved changes" and "Saved" beside the Save of a Platform block (outbound
   email, backups): a hint and a status, not messages. The status is its own class
   and not .save-status, whose look it repeats, because the foundation gate counts
   every .save-status in the page and expects the one flashSaved makes. */
.platform-unsaved,
.platform-saved {
  font-size: 0.8125rem;
}
.platform-unsaved { color: var(--fg-amber); }
/* Empty, it is still a flex item and its gap would widen the row. */
.platform-saved:empty { display: none; }
.platform-saved.is-saved { color: var(--green); }
.platform-saved.is-error { color: var(--danger); }

/* settings:P2 */
/* Your account: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Profile > Details. The email and the roles are facts shown as text, not
   fields: the email changes through its own dialog (it needs the password),
   the roles through an admin. The label copies the form-field caption so the
   two read as one form. */
.profile-email-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.5rem 0.75rem;
  margin-bottom: 0.75rem;
}
.profile-email-row .profile-fact {
  flex: 1 1 12rem;
  min-width: 0;
}
.profile-fact {
  display: grid;
  gap: 0.375rem;
  align-content: start;
}
.profile-fact-label {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.profile-fact-value {
  display: block;
  min-width: 0;
  color: var(--fg-body);
  font-size: 0.875rem;
  /* An address is one unbroken token; on a phone it wraps rather than run
     out of the card. */
  overflow-wrap: anywhere;
}
.profile-fact .form-hint { margin-top: 0; }
.profile-roles { margin-top: 0.875rem; }
#profileEmailMessage { margin: 0 0 0.75rem; }
/* "Unsaved changes" / "Saved" beside the one Save, on its line. */
.profile-save-row { align-items: center; }
.save-status.is-dirty { color: var(--muted); }

/* Profile > Password and security: one card, a hairline between its parts.
   A hidden part draws nothing ([hidden] is authoritative), and Password is
   always first and always shown, so every visible part after it is ruled. */
.profile-policy-line { color: var(--fg-desc); }
.profile-security-section + .profile-security-section {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
}
.profile-section-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
  font-size: 1rem;
  line-height: 1.3;
}
.profile-section-title .status-pill { font-size: 0.6875rem; }
.profile-security-section .card-copy { margin-bottom: 0.625rem; }
.profile-security-section .modal-actions-row button { flex: 0 0 auto; }
.profile-security-section .form-message { margin-top: 0.625rem; }
.profile-recovery-status {
  margin: 0 0 0.625rem;
  color: var(--fg-copy);
  font-size: 0.875rem;
}
.profile-recovery-status.is-low { color: var(--fg-warn); }
.profile-recovery-status.is-none { color: var(--fg-error); }
/* The rest of a trusted device, one tap away: when it was trusted and until
   when, where it was last seen, and the browser's own description. */
.trusted-device-details {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  color: var(--fg-meta);
}
.trusted-device-details summary {
  cursor: pointer;
  width: fit-content;
}
.trusted-device-fact {
  margin-top: 0.25rem;
  overflow-wrap: anywhere;
}

/* App: a choice that saves itself, with its "Saved" beside it. */
.pref-row {
  display: flex;
  align-items: flex-end;
  gap: 0.75rem;
}
.pref-row + .pref-row { margin-top: 0.75rem; }
.pref-row > .form-field {
  flex: 1 1 auto;
  min-width: 0;
}
.pref-row > .save-status {
  flex: 0 0 auto;
  margin: 0 0 0.5rem;
  min-width: 3.5rem;
}
.pref-row-check { align-items: center; }
/* The map preview switch (a listed account only) sits under the two map dropdowns. */
.map-preview-row { margin-top: 0.9rem; }
.pref-row-check > .save-status { margin: 0; }
/* The map preview: the choice drawn at the size the maps draw it (the size is
   set from app.js in CSS pixels, which is what a map line and point use). */
.map-pref-swatch {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 2.25rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: var(--inset-bg);
}
.map-pref-swatch-track {
  display: block;
  width: 70%;
  height: 2px;
  border-radius: 1rem;
  background: var(--green);
}
.map-pref-swatch-point {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgb(var(--rgb-amber));
}

/* Remove all devices is the trusted-devices part's only action and is hidden
   until there is more than one device; its empty row would leave a blank band
   above the next part. */
#trustedDevicesCard .modal-actions:has(> #trustedDevicesRevokeAllButton[hidden]) { display: none; }

/* Change password: its row already sits 1rem below the form (.modal-actions),
   and the older margin on the Save button made it shorter than Sign out and
   Change organisation beside it in the forced dialog (a stretched flex item
   loses its own margins from the row's height). */
#passwordChangeOverlay #passwordSaveButton { margin-top: 0; }

/* Two-factor setup: Copy key, and on a phone the authenticator app itself. */
.authenticator-setup-off { color: var(--fg-warn); }
.authenticator-setup-helpers {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin: 0.25rem 0 0.75rem;
}

/* settings:P3a */
/* Organisation save path: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The organisation half of Settings until its record has loaded, or when it
   could not be loaded: dimmed, and inert from script, so no switch can be
   changed from a copy the server never gave. aria-busy says so while it loads. */
.org-settings-loading {
  opacity: 0.55;
  pointer-events: none;
}
/* "Couldn't load organisation settings" + Retry. Sits outside the inert
   section it speaks for, and spans the panel if the panel lays out in columns. */
.org-settings-load-error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  grid-column: 1 / -1;
  margin-bottom: 0.75rem;
  padding: 0.625rem 0.875rem;
  border: 0.0625rem solid rgba(var(--rgb-red), 0.4);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-red), 0.08);
  color: var(--danger);
  font-size: 0.875rem;
}
.org-settings-load-error .secondary-button {
  min-height: 0;
  padding: 0.25rem 0.75rem;
  font-size: 0.8125rem;
}
.card > .org-settings-load-error {
  margin: 0.5rem 0;
}

/* settings:P3b */
/* Organisation layout and calendar: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Organisation panel: seven sections with a heading each, and a short "On this
   page" list beside them. The sections take the rest of the panel's width, and
   on a wide page each one splits into two columns of cards (settings:canvas). */
.org-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(8.5rem, 11rem);
  gap: 2rem;
  align-items: start;
}
.org-sections {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  min-width: 0;
}
.org-section {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
}
.org-section > .card { margin: 0; }
.org-section-heading {
  margin: 0;
  padding-bottom: 0.375rem;
  border-bottom: 0.0625rem solid var(--line);
  font-size: 1.5rem;
  line-height: 1.2;
  font-weight: 700;
}
.org-section-heading:focus:not(:focus-visible) { outline: none; }

/* "On this page": sticks to the top of the panel as it scrolls. */
.org-jump {
  position: sticky;
  top: 0;
  align-self: start;
}
.org-jump-title {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.org-jump-list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  border-left: 0.0625rem solid var(--line);
}
.org-jump-list li { display: flex; }
.org-jump-link {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0 0 0 -0.0625rem;
  padding: 0.3125rem 0.75rem;
  border: 0;
  border-left: 0.1875rem solid transparent;
  border-radius: 0;
  background: none;
  color: var(--muted);
  font-size: 0.875rem;
  font-weight: 500;
  text-align: left;
}
.org-jump-link:hover:not(:disabled) { background: none; color: var(--text); filter: none; }
.org-jump-link[aria-current="location"] {
  border-left-color: var(--green);
  color: var(--text);
}
/* Below this width the panel has no room beside the 40rem column (measured: the
   panel is 714px wide at a 1180px window and needs about 690 for column, gap and
   list), and the list would push a horizontal scrollbar into it. (px, like every
   media query here.) */
@media (max-width: 1179px) {
  .org-layout { grid-template-columns: minmax(0, 1fr); }
  .org-jump { display: none; }
}

/* A switch with its "Saved" beside it, and the one-line consequence under it,
   lined up with the text of the switch. */
.org-control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 0.625rem;
}
.card-copy + .org-control-row,
.card-title + .org-control-row { margin-top: 0; }
.org-control-row .filter-checkbox-label { flex: 0 1 auto; }
.org-control-row + .form-hint { margin-left: 1.625rem; }
.org-control-row:has(.filter-checkbox-nested) + .form-hint { margin-left: 3.25rem; }

/* A field that cannot be saved as it stands (a blank question, a period out of
   range): marked, with the reason under it. */
.form-field.org-invalid span { color: var(--danger); }
.form-field.org-invalid input { border-color: var(--danger); }
.org-field-error {
  color: var(--danger);
  font-size: 0.8125rem;
}
.org-field-hint {
  color: var(--muted);
  font-size: 0.75rem;
}
.org-dirty-hint {
  color: var(--fg-warn);
  font-size: 0.8125rem;
}
.org-dirty-hint[hidden] { display: none; }

/* The four retention periods that only feed the preview. */
.retention-tag {
  margin-left: 0.25rem;
  padding: 0 0.375rem;
  border: 0.0625rem solid var(--line);
  border-radius: 62.5rem;
  color: var(--muted);
  font-size: 0.6875rem;
  font-style: normal;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
}
#dataPrivacyCard > .data-privacy-section:first-of-type { margin-top: 0; }

/* Job calendar (Integrations): the state line under the switch, and a button
   row that takes no room while every button in it is hidden. */
.calendar-admin-state { margin: 0.625rem 0 0.75rem; }
.calendar-admin-actions:not(:has(> button:not([hidden]))) { display: none; }
.calendar-feed-exposure { margin: 0.75rem 0 0; }
.calendar-disabled-note { margin-bottom: 0; }

/* settings:P4 */
/* Billing: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ---- Billing tab -----------------------------------------------------------
   The panel (Quoting and invoicing, the defaults form, the charge-out rules) and
   the rule dialog. The master switch is #organizationFinancialsEnabled; the
   organisation save path sets data-financials="on|off" on #billingSettingsSection
   and everything below that is "off" reads it. */

/* The consequence of the switch sits under it, in line with its label. */
#billingQuotingCard .billing-consequence {
  margin: 0.25rem 0 0.875rem 1.625rem;
  font-size: 0.875rem;
}

/* With quoting and invoicing off, the two cards that only matter once it is on
   are dimmed and say so. They stay editable, so rules can be set up first. The
   note takes no room while the switch is on. */
.billing-off-note {
  display: none;
  margin: -0.125rem 0 0.875rem;
  color: var(--fg-copy);
  font-size: 0.875rem;
}
#billingSettingsSection[data-financials="off"] .billing-off-note { display: block; }
#billingSettingsSection[data-financials="off"] .billing-needs-financials > :not(.card-title):not(.billing-off-note) {
  opacity: 0.55;
}

/* One field and the reason it was refused, under it. */
.billing-field { min-width: 0; margin-bottom: 1rem; }
.form-grid > .billing-field { margin-bottom: 0; }
.billing-field > .form-field { margin-bottom: 0; }
.billing-field > .form-hint { margin-top: 0.375rem; }
.billing-field-error {
  margin: 0.375rem 0 0;
  color: var(--danger);
  font-size: 0.8125rem;
}
.billing-field input[aria-invalid="true"] { border-color: var(--danger); }

/* Save, then "Saved" or "Unsaved changes" beside it. Air above, so the button
   no longer touches the field over it. */
.billing-save-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-top: 1rem;
}
.billing-save-row .save-status:not(:empty) { margin-inline-start: 0; }
.billing-unsaved { color: var(--fg-copy); font-size: 0.8125rem; }

/* The rules list's rows are styled where they always were (.charge-rule-row
   and its parts, further down): a button row has to be told to fill its card,
   and the shared block above must stay free of that. What this tab added: */
#chargeRulesList { margin-bottom: 0.25rem; }
.charge-rule-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; }
/* A rate prices the work; a surcharge is laid on top of one. */
.charge-rule-tag {
  display: inline-block;
  padding: 0.125rem 0.4375rem;
  border-radius: 62.4375rem;
  font-size: 0.6875rem;
  font-weight: 750;
  white-space: nowrap;
}
.charge-rule-tag-rate { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.charge-rule-tag-surcharge { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }

/* The rule dialog: the fields scroll, the sentence that says what the rule does
   and the buttons stay put (.pinned-actions). Condition groups are real
   fieldsets, boxed and multi-column so they have room to breathe. */
#chargeRuleForm .billing-field { margin-bottom: 1rem; }
#chargeRuleForm .form-grid > .billing-field { margin-bottom: 1rem; }
#chargeRuleForm .form-field > span {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.75rem;
  letter-spacing: 0.03em;
}
#chargeRuleForm .charge-rule-conditions-heading {
  margin: 0.25rem 0 0;
  font-size: 1rem;
}
#chargeRuleForm .charge-rule-conditions-intro { margin: 0.375rem 0 0.875rem; }
#chargeRuleForm .charge-rule-group {
  min-width: 0;
  margin: 0 0 1rem;
  padding: 0;
  border: 0;
}
#chargeRuleForm .charge-rule-group > legend {
  display: block;
  margin-bottom: 0.5rem;
  padding: 0;
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
#chargeRuleForm .form-field[for="chargeRuleOpsMatch"] { margin-bottom: 1rem; }
#chargeRuleForm .role-checkboxes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: 0.75rem 1.125rem;
  padding: 0.875rem 1rem;
  background: rgba(var(--rgb-slate), 0.05);
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
}
#chargeRuleForm .role-checkboxes label {
  align-items: center;
  font-size: 0.875rem;
  gap: 0.625rem;
}
#chargeRuleForm .role-checkboxes input { margin-top: 0; }
#chargeRuleForm .charge-rule-notam { margin-bottom: 1rem; }
#chargeRuleForm .filter-checkbox-label { margin-top: 0.25rem; }
.charge-rule-empty-group { grid-column: 1 / -1; color: var(--muted); font-size: 0.875rem; }
.charge-rule-preview {
  flex: 0 0 auto;
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.75rem;
  background: rgba(var(--rgb-slate), 0.08);
  border-radius: 0.5rem;
  color: var(--fg-body);
  font-size: 0.875rem;
}
/* Delete stands apart from Save, at the far end of the row. */
#chargeRuleForm .modal-actions .danger-button { margin-inline-start: auto; }

/* settings:P5 */
/* Users: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Users: the access review banner, the searchable grouped list, the detail's header
   and its facts, the role groups, and the save row. The phone's drill-in is under the
   phone anchor at the foot of the html.mobile Settings block. */
.access-review-actions {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex: none;
}
.access-review-message { flex: 1 1 100%; margin: 0; }

.personnel-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.personnel-list-head .card-title { margin: 0; }
.personnel-list-card > .scope-line { margin: 0.375rem 0 0.75rem; }

.personnel-list-tools {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
/* The same box as the Plan page's search, so the two read as one control. Its width is
   the column's: a flex item stretches across a column, so none is set. */
.personnel-search {
  box-sizing: border-box;
  min-width: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  margin: 0;
  outline: none;
}
.personnel-search:focus { border-color: var(--focus-ring); }
.personnel-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
.personnel-chip {
  min-height: 0;
  padding: 0.25rem 0.625rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 62.4375rem;
  background: transparent;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 650;
  white-space: nowrap;
}
.personnel-chip:hover:not(:disabled) {
  background: rgba(var(--rgb-slate), 0.12);
  filter: none;
}
/* aria-pressed is the state; colour only echoes it. */
.personnel-chip[aria-pressed="true"] {
  background: rgba(var(--rgb-green), 0.18);
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--fg-body);
}
.personnel-role-filter > summary {
  cursor: pointer;
  width: fit-content;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 650;
}
.personnel-role-filter[open] > summary { margin-bottom: 0.375rem; }
.personnel-list-count {
  margin: 0;
  color: var(--muted);
  font-size: 0.75rem;
}
.personnel-group + .personnel-group { margin-top: 0.75rem; }
.personnel-group-label {
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
  margin: 0 0 0.25rem;
  padding: 0 0.6875rem;
  color: var(--fg-label);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.personnel-group-count { color: var(--muted); font-weight: 600; }
.personnel-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}
.personnel-row-roles {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.personnel-no-match {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 0.6875rem;
  color: var(--fg-empty);
  font-size: 0.875rem;
}

/* The person's name and standing stay in view while the rest of the detail scrolls
   (desktop; on a phone the page is the scroller and the chip row is the only sticky
   row). Opaque, and pulled across the card's own padding so nothing shows above or
   beside it. */
html:not(.mobile) .personnel-detail-header {
  position: sticky;
  /* Sticky offsets are measured from the scroller's CONTENT edge, inside its padding,
     so the card's own 1.125rem is given back here (the same pairing as the Settings
     chip row's top and margin-top). With 0 the header sat 1.125rem lower than its flow
     box and covered the line under it. */
  top: -1.125rem;
  z-index: 5;
  margin: -1.125rem -1.125rem 0;
  padding: 1.125rem 1.125rem 0.625rem;
  background: var(--sheet-bg);
  border-radius: 0.5rem 0.5rem 0 0;
  border-bottom: 0.0625rem solid var(--line);
}
.personnel-detail-heading { min-width: 0; }
.personnel-detail-header .card-title {
  margin: 0;
  overflow-wrap: anywhere;
}
.personnel-detail-header .card-title:focus { outline: none; }
.personnel-invite-note { color: var(--muted); font-size: 0.8125rem; }
.personnel-invite-note.is-expired { color: var(--fg-warn); }

/* Name and phone as text; the email as text with its own Change action. */
.personnel-facts {
  display: grid;
  gap: 0.5rem;
  margin: 1rem 0 0;
}
.personnel-fact {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: 0.5rem;
  align-items: baseline;
}
.personnel-fact dt {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.personnel-fact dd {
  margin: 0;
  min-width: 0;
  color: var(--fg-body);
  font-size: 0.875rem;
  overflow-wrap: anywhere;
}
.personnel-fact dd.is-empty,
.personnel-email-note { color: var(--muted); }
.personnel-email-note { font-size: 0.8125rem; }
.personnel-email-text { margin-right: 0.5rem; }
.personnel-facts-hint { margin: 0.5rem 0 0; }
.personnel-email-edit {
  display: grid;
  justify-items: stretch;
  gap: 0.375rem;
  margin-top: 0.75rem;
}
.personnel-email-edit .settings-link { justify-self: start; }
.personnel-rate-grid {
  grid-template-columns: minmax(0, 18rem);
  margin-top: 1rem;
}

/* Roles: three headed groups. Real fieldsets, so a screen reader hears the group
   before the box. Full access first, to match the order roles read everywhere else
   (Admin leads), then what the person does, then what they may do. */
.personnel-roles {
  min-width: 0;
  margin: 1.125rem 0 0;
  padding: 0;
  border: 0;
}
.personnel-roles > legend {
  padding: 0;
  margin-bottom: 0.5rem;
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.role-groups {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
}
.role-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.role-group > legend {
  padding: 0;
  margin-bottom: 0.375rem;
  color: var(--fg-body);
  font-size: 0.8125rem;
  font-weight: 650;
}
.role-checkboxes .role-option-note {
  margin-top: 0.125rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

.personnel-save-row {
  align-items: center;
  gap: 0.75rem;
}
.save-status.is-dirty { color: var(--muted); }

/* settings:P6 */
/* Compliance: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A card title with an action beside it: Readiness rules and its Certification
   types link, Readiness violations and its Refresh. The title drops its own
   bottom margin so it centres on the button, and the head takes it instead. */
.compliance-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.625rem;
}
.compliance-card-head .card-title { margin: 0; }

/* The two rule lists show every row. The generic .queue-list caps at 21.875rem
   and scrolls inside the page, which cut the last row mid-way with no hint
   there were more and left a scroll box inside a scrolling panel. The ids, not
   the panel, so the Standing arrangements list keeps what it had. The button
   below gets room instead of sitting against the last row. */
#eligibilityRulesList,
#airspaceRulesList {
  max-height: none;
  overflow-y: visible;
}
#eligibilityRulesList + button,
#airspaceRulesList + button {
  margin-top: 0.75rem;
}

/* Alerts and exports: two jobs in one card, split by a hairline. */
.compliance-subtitle {
  margin: 0 0 0.5rem;
  color: var(--fg-body);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.2;
}
.compliance-block + .compliance-block {
  margin-top: 1.125rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
}

/* A certification a rule still names after the type was removed from the list. */
.cert-removed { color: var(--muted); }

/* settings:P7 */
/* Risk assessment and Checklists: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A template row is the name (a button for the keyboard) and its own Duplicate. */
.risk-template-row { cursor: pointer; }
.risk-template-open { min-width: 0; }
.risk-template-open:focus { outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: 0.25rem; border-radius: 0.25rem; }
.risk-template-duplicate { min-height: 0; padding: 0.3125rem 0.75rem; font-size: 0.75rem; }
/* "Start from" in the New template dialog, and the note on a copy. */
.risk-template-start { margin: 0.75rem 0 0; padding: 0; border: 0; min-width: 0; }
.risk-template-start legend {
  padding: 0;
  margin-bottom: 0.375rem;
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.risk-template-start-option { display: inline-flex; align-items: center; gap: 0.375rem; margin-right: 1.25rem; cursor: pointer; }
.risk-template-start-option input { margin: 0; }
.risk-template-start-meta { color: var(--muted); font-size: 0.75rem; }
.risk-template-copy-note { margin: 0.75rem 0 0; }
/* The matrix legend says what each grade asks of the crew. */
.risk-matrix-legend { flex-wrap: wrap; gap: 0.375rem 1rem; }
.risk-matrix-legend-item { display: inline-flex; align-items: center; gap: 0.375rem; color: var(--muted); font-size: 0.75rem; }
/* "Unsaved changes" / "Saved" beside the matrix Save, on the actions row. */
.risk-dialog-unsaved { align-self: center; margin: 0; }
#riskMatrixSaveStatus { align-self: center; }

/* settings:P8 */
/* Clients and Aircraft: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Clients and Aircraft (settings design P8). The anchor block above is left as
   it is (the foundation gate checks it); the rules for these two panels, their
   dialogs and the lists in them start here. */

/* The card's header row: the title on the left, the primary action(s) top right,
   the same on every layout. */
.records-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  margin-bottom: 0.625rem;
}
.records-head .card-title {
  margin: 0;
}
.records-head-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* The standalone-action margin on #addClientButton (further down) is for a button
   under a form; in the header row it pushed the button 1rem below its neighbours. */
.records-head #addClientButton {
  margin-top: 0;
}

/* The search box that appears once a list is longer than a screenful. */
.records-search {
  margin: 0 0 0.75rem;
}

/* "Inactive (2)" / "Retired (1)": the records put away, closed until asked for. */
.records-group {
  margin-top: 0.75rem;
}
.records-group > summary {
  cursor: pointer;
  padding: 0.375rem 0;
  color: var(--muted);
  font-size: 0.8125rem;
}
.records-group > summary:hover {
  color: var(--fg-body);
}
.records-group[open] > summary {
  margin-bottom: 0.5rem;
}

/* Tiles / List: two icon buttons beside the page's actions, the pressed one
   filled (state is the pressed attribute, and the fill, not colour alone). */
.records-view-switch {
  display: inline-flex;
  align-items: stretch;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  overflow: hidden;
}
.records-view-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  min-height: 2.25rem;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: var(--muted);
  cursor: pointer;
}
.records-view-btn + .records-view-btn { border-left: 0.0625rem solid var(--line); }
.records-view-btn svg { width: 1.0625rem; height: 1.0625rem; }
.records-view-btn:hover { color: var(--fg-body); }
.records-view-btn[aria-pressed="true"] { background: var(--green-soft); color: var(--green); }
/* The switch clips its children (rounded corners), which cuts an outside focus ring to a
   sliver: the ring is drawn inside the button, as everywhere else a ring would be clipped. */
.records-view-btn:focus { outline-offset: var(--keyboard-ring-inset); }

/* The list view: a row per record that scans like a table, the same columns under
   the heading row and in every list on the page (the groups follow the view). The
   record's name, then a column for each fact, then the badges. The columns are
   fractions with a zero floor, so a long value is cut short (its full text is the
   cell's title) instead of moving the columns of the rows around it. */
#clientsPage {
  --rec-cols: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 0.6fr);
}
#aircraftPage {
  --rec-cols: minmax(0, 1.3fr) minmax(0, 0.9fr) minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 0.7fr) minmax(0, 1.5fr);
}
.records-colhead {
  display: grid;
  grid-template-columns: var(--rec-cols);
  column-gap: 0.75rem;
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 0.4375rem 0.625rem;
  margin-bottom: 0.25rem;
  background: var(--table-head-bg);
  border-radius: 0.375rem;
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
:is(#clientsPage, #aircraftPage) .queue-item.rec-row {
  display: grid;
  grid-template-columns: var(--rec-cols);
  column-gap: 0.75rem;
  align-items: center;
  margin: 0;
  padding: 0.5rem 0.625rem;
  border: 0;
  border-bottom: 0.0625rem solid var(--line);
  border-radius: 0;
  background: transparent;
}
:is(#clientsPage, #aircraftPage) .queue-item.rec-row:hover { background: rgba(var(--rgb-overlay), 0.045); }
:is(#clientsPage, #aircraftPage) .rec-cells { display: contents; }
:is(#clientsPage, #aircraftPage) .rec-name { grid-column: 1; }
:is(#clientsPage, #aircraftPage) .rec-cell {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#clientsPage .rec-cell[data-col="contact"] { grid-column: 2; }
#clientsPage .rec-cell[data-col="email"] { grid-column: 3; }
#clientsPage .rec-cell[data-col="phone"] { grid-column: 4; }
#clientsPage .rec-pills { grid-column: 5; }
#aircraftPage .rec-cell[data-col="serial"] { grid-column: 2; }
#aircraftPage .rec-cell[data-col="model"] { grid-column: 3; }
#aircraftPage .rec-cell[data-col="hours"] { grid-column: 4; }
#aircraftPage .rec-cell[data-col="firmware"] { grid-column: 5; }
#aircraftPage .rec-pills { grid-column: 6; }
:is(#clientsPage, #aircraftPage) .queue-item.rec-row > .rec-pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 0.25rem;
  align-items: center;
  min-width: 0;
}
/* The list view on desktop: one column of rows, in place of the grid of tiles. */
html:not(.mobile) :is(#clientsPage, #aircraftPage)[data-records-view="list"] :is(#clientsList,
  #clientsInactiveList, #aircraftFleetList, #aircraftRetiredList) {
  display: block;
}

/* A status pill beside a dialog title ("Retired", "Inactive"). */
.records-title-pill {
  margin-inline-start: 0.5rem;
  vertical-align: middle;
}

/* A long name or summary wraps in the rows of these lists instead of pushing the
   pills out of the row, and the left block is allowed to shrink so it can. The
   defect and maintenance lists carry free text (a defect summary runs to 255
   characters), so their names wrap everywhere; the fleet and client rows wrap on
   the phone (below). */
#aircraftFleetList .queue-item > div:first-child,
#aircraftRetiredList .queue-item > div:first-child,
#aircraftDefectList .queue-item > div:first-child,
#aircraftMaintenanceList .queue-item > div:first-child,
#clientsList .queue-item > div:first-child,
#clientsInactiveList .queue-item > div:first-child {
  min-width: 0;
}
#aircraftDefectList .queue-name,
#aircraftMaintenanceList .queue-name {
  white-space: normal;
  overflow-wrap: anywhere;
}
/* The pill block, when a row has one: a client row has no right-hand block at
   all until it is inactive, so its only child (the name) must not be taken for
   one. */
#aircraftRetiredList .queue-item:not(.rec-row) > div:last-child:not(:first-child),
#clientsInactiveList .queue-item:not(.rec-row) > div:last-child:not(:first-child) {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.375rem;
  align-items: center;
}

/* The aircraft dialog: its three stat tiles share one row (they wrapped, leaving
   "Open defects" alone on a second line), the jump strip sits under them, and
   the retire / client-data section at the foot is apart from Save. */
#aircraftDetailStats {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
/* The action rows under the defect and maintenance lists touched the last row
   (0-1px between them); they take their own gap. */
#aircraftDetailOverlay .settings-action-row {
  margin-top: 0.625rem;
}
.records-jump {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.25rem 0 0.75rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
/* Cross-links on the two cards (D15): the client's jobs, the aircraft's flights. */
.records-jobs {
  margin-top: 1.25rem;
}
.records-jobs-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.records-jobs-head .modal-section-title {
  margin: 0;
}
.records-jobs-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.records-job-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.625rem;
  min-height: 0;
  margin: 0;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease;
}
.records-job-row:hover:not(:disabled) { background: rgba(var(--rgb-slate), 0.1); filter: none; }
.records-job-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}
.records-job-name,
.records-job-when {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.records-job-row .status-pill {
  flex: 0 0 auto;
}
.records-jobs-all {
  margin-top: 0.5rem;
}
html.mobile .records-job-row,
html.mobile .records-jobs-all {
  min-height: var(--touch-min);
}
.records-flights {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  margin: 0.625rem 0 0.5rem;
}
.records-flights .secondary-button {
  margin-left: auto;
}
.records-flights-count {
  font-size: 0.8125rem;
  color: var(--muted);
}
.records-danger {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
}
.records-danger .modal-section-title {
  margin-top: 0;
}
.records-danger-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* The line above Save ("Saves the details above ...") takes its own row, so the
   button always sits under it. */
.modal-actions .records-save-note {
  flex: 1 1 100%;
  margin: 0;
}

/* The ticks in the maintenance form say what they will do ("Reset service clock
   to 192 h and 2 Oct 2026", "Yearly service"). They are sentences, not field
   labels: the label caps (`.form-field span`) would turn "h" into "H". Same look
   as the tick in the defect dialog. */
#maintenanceEventResetLabel,
#maintenanceEventYearlyLabel {
  color: var(--fg-body);
  font-size: inherit;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

/* settings:P9 */
/* Support and feedback: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Support: the "Something wrong?" card at the top. Title and scope line on the
   left, the button on the right; they wrap onto two lines when it is narrow. */
.support-contact-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
}
.support-contact-text { min-width: 0; }
.support-contact-card .card-title { margin-bottom: 0.25rem; }
.support-contact-card .scope-line { margin: 0; }

/* Feedback: the Idea / Problem choice (the app's first radios, so the group is
   a real fieldset with a legend styled like a field label) and the filter in
   the platform inbox. The options sit in a wrapper div because a fieldset does
   not lay its children out as a flex row in every browser. */
.feedback-kind {
  margin: 0 0 0.75rem;
  padding: 0;
  border: 0;
  min-inline-size: 0;
}
.feedback-kind legend {
  padding: 0;
  margin-bottom: 0.25rem;
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.feedback-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
}
.feedback-options > label {
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.875rem;
}

/* How much of the 4000 characters is used; only shown near the limit. */
.feedback-counter {
  margin: 0.375rem 0 0.5rem;
  text-align: right;
  color: var(--muted);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}
.feedback-counter.is-limit { color: var(--fg-warn); }

/* Idea / Problem tag, in a member's own list and in the inbox. Same pill as the
   status badge beside it. */
.fr-kind-idea { background: rgba(var(--rgb-slate), 0.12); color: var(--fg-subtle); border-color: rgba(var(--rgb-slate), 0.3); }
.fr-kind-problem { background: rgba(var(--rgb-red-soft), 0.16); color: var(--fg-error); border-color: rgba(var(--rgb-red-soft), 0.4); }

/* The inbox card's header wraps: the title, then the filter, toggle and Refresh
   under it when the card is too narrow for them to sit beside it. */
[data-platform-subpanel="feature-requests"] .audit-log-header {
  flex-wrap: wrap;
}

/* The inbox's request cell: the tag, the text, and for a problem the page and
   app version it was sent with. */
.feature-request-cell .feature-request-kind { margin-bottom: 0.25rem; }
/* Specificity, not preference. `.data-table td` sets white-space: nowrap
   (0,1,1) and outranks the bare `.feature-request-cell` rule near the top of
   the file (0,1,0), so the max-width and pre-wrap there never applied: a
   multi-sentence report ran as one line under the Status picker and off the
   card, its line breaks collapsed. Leading with `.data-table td` takes the
   selector past it (the same trap `.incidents-table td.incident-desc-cell`
   documents). The phone block below re-states it, because the phone's own
   `td { white-space: normal }` is more specific again. */
.data-table td.feature-request-cell {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* A member's own list: a pasted URL or id with no spaces wraps inside the card
   instead of running off its edge. */
.feature-request-body { overflow-wrap: anywhere; }
.feature-request-context {
  margin-top: 0.25rem;
  color: var(--muted);
  font-size: 0.8125rem;
}
.feature-request-meta .fr-status-badge + .fr-status-badge { margin-inline-start: 0.375rem; }

/* settings:canvas */
/* SETTINGS CANVAS (2026-10-23). Josh: "the settings pages are kind of ugly with
   the narrow panels and all of the wasted screen realestate to the right ... the
   users page looks good". Users takes the whole width beside the rail and splits
   it into two panes; every other section now does the same, with one system:

   1. The panel is the whole width beside the rail (the 40rem column is gone).
   2. A host marked .settings-cols (a panel, or Billing's section) is a grid of
      --settings-cols columns: two once the panel is 56rem wide, one below that.
      Its children take one column each, in source order, so the reading order,
      the tab order and the phone order all stay the source order. A
      .settings-stack is one column of several cards; .settings-wide spans both.
      Organisation splits each section the same way (--org-cols) once the column
      beside its "On this page" list is 53rem wide. Its sections stay one under the
      next, each across the column: "On this page" marks the section whose top
      has reached the top of the panel, which two sections side by side would
      share.
   3. A list-shaped section (Clients, Aircraft, Checklists, Platform >
      Organisations) keeps its one card and lays the rows out as tiles that fill
      the width.
   Inside a wide card, running text keeps a reading measure (75ch); the card, not
   the text, is what fills the width. Not in Users, whose lines keep their width.

   Every rule here that lays anything out is desktop only (html:not(.mobile)), and
   a stack is display:contents everywhere it is not a desktop column, so on a
   phone its cards are the panel's own children, laid out exactly as they always
   were.

   The column counts are switched by window width, in px like every media query
   here, once for the full sidebar (216px) and once for the collapsed one (58px):
   the panel is the window less the sidebar, the page padding and the rail with
   its gap (250px), and the Organisation column a further 177px for its list. Not
   a container query: containment on #settingsPage repaints everything inside it
   on its own pixel grid, which moved the Users page and the rail by a pixel. If
   the root font scale (html font-size) changes, re-measure these widths.
   Users has no .settings-cols and keeps its own rules, untouched; the one rule
   here that would otherwise reach inside it (the reading measure) excludes it. */
:is(#settingsPage, #accountPage) {
  --settings-gap: 1rem;
  --settings-cols: 1;
  --org-cols: 1;
  --org-form-tracks: minmax(0, 1fr);
}
/* The panel 56rem (762px) wide: 1228px with the sidebar, 1070px without. */
@media (min-width: 1228px) { :is(#settingsPage, #accountPage) { --settings-cols: 2; } }
@media (min-width: 1070px) { .app-shell.nav-collapsed :is(#settingsPage, #accountPage) { --settings-cols: 2; } }
/* Organisation's column 53rem (723px) wide beside its list: 1366px, 1208px. */
@media (min-width: 1366px) {
  #settingsPage { --org-cols: 2; --org-form-tracks: minmax(0, 2fr) minmax(0, 3fr); }
}
@media (min-width: 1208px) {
  .app-shell.nav-collapsed #settingsPage { --org-cols: 2; --org-form-tracks: minmax(0, 2fr) minmax(0, 3fr); }
}

:is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) .settings-layout { max-width: none; }

.settings-stack { display: contents; }
/* The panel's gap spaces a stacked card on a phone, exactly as it spaced it as a
   direct child (#settingsPage .settings-layout > .card, .org-section > .card).
   Billing's cards sit in a plain block there and keep their own margin. */
#settingsPage .org-section > .settings-stack > .card,
:is(#settingsPage, #accountPage) .settings-layout > .settings-stack > .card {
  margin-bottom: 0;
}

html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols {
  display: grid;
  grid-template-columns: repeat(var(--settings-cols), minmax(0, 1fr));
  gap: var(--settings-gap);
  align-items: start;
  align-content: start;
}
html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols > .settings-stack {
  display: flex;
  flex-direction: column;
  gap: var(--settings-gap);
  min-width: 0;
}
html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols > .card,
html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols > .settings-stack > .card {
  margin: 0;
}
html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols > .settings-wide { grid-column: 1 / -1; }
/* The privacy policy's own offset from the guide above it is the grid gap now. */
html:not(.mobile) :is(#settingsPage, #accountPage) .settings-cols > .privacy-policy { margin-top: 0; }

/* Organisation: the cards in each section in --org-cols columns. One column is
   the section as it was: its heading and its cards, 0.75rem apart. */
html:not(.mobile) #settingsPage .org-section {
  display: grid;
  grid-template-columns: repeat(var(--org-cols), minmax(0, 1fr));
  align-items: start;
  align-content: start;
}
html:not(.mobile) #settingsPage .org-section > .org-section-heading { grid-column: 1 / -1; }
/* A section of one card: the card spans the section. */
html:not(.mobile) #settingsPage .org-section > .card:nth-child(2):last-child { grid-column: 1 / -1; }
html:not(.mobile) #settingsPage .org-section > .settings-stack {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
}
/* Name and logo side by side across the card, the logo's buttons and note given
   the wider share. In one column the logo keeps its rule and spacing above it
   ((2 - cols) is 1 there, 0 in two). */
html:not(.mobile) #settingsPage #organizationForm {
  display: grid;
  grid-template-columns: var(--org-form-tracks);
  column-gap: 1.5rem;
  align-items: center;
}
html:not(.mobile) #settingsPage #organizationForm > :not(.form-grid, .organization-logo-field) { grid-column: 1 / -1; }
html:not(.mobile) #settingsPage #organizationForm > .organization-logo-field {
  margin-top: calc((2 - var(--org-cols)) * 1rem);
  padding-top: calc((2 - var(--org-cols)) * 1rem);
  border-top-width: calc((2 - var(--org-cols)) * 0.0625rem);
}
/* Data and privacy keeps its three parts in order down the card; the retention
   periods spread across it, a field at least 18.5rem wide so a label and its
   "Report only" tag stay on one line (two fields a row, as before, when narrow). */
html:not(.mobile) #settingsPage #retentionBlock .form-grid {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 18.5rem), 1fr));
}

/* Rows as tiles: the list fills its card's width, a tile at least 24rem wide.
   A list with nothing in it is one line of text across the card. */
html:not(.mobile) :is(#settingsPage, #clientsPage, #aircraftPage) :is(#clientsList, #clientsInactiveList,
  #aircraftFleetList, #aircraftRetiredList, #checklistTemplateList, #platformOrgList) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 24rem), 1fr));
  gap: 0.4375rem 0.5rem;
  align-items: stretch;
}
html:not(.mobile) :is(#settingsPage, #clientsPage, #aircraftPage) :is(#clientsList, #clientsInactiveList,
  #aircraftFleetList, #aircraftRetiredList, #checklistTemplateList, #platformOrgList) > .queue-item {
  margin: 0;
}
html:not(.mobile) :is(#settingsPage, #clientsPage, #aircraftPage) :is(#clientsList, #clientsInactiveList,
  #aircraftFleetList, #aircraftRetiredList, #checklistTemplateList, #platformOrgList) > :not(.queue-item) {
  grid-column: 1 / -1;
}
/* Every organisation in view: the tiles take the room the short scrolling box
   used to leave empty under it. */
html:not(.mobile) #settingsPage #platformOrgList { max-height: none; overflow: visible; }
html:not(.mobile) :is(#clientsPage, #aircraftPage) .records-search { max-width: 28rem; }

/* The reading measure, in every panel but Users (the reference, left exactly as
   it was: its lines run the width of their pane). The exclusion sits in :where()
   so the rule's specificity, and so what it wins against elsewhere, is unchanged. */
html:not(.mobile) :is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) :is(.card-copy, .scope-line, .form-hint, .support-body p, .support-body ul):where(:not([data-settings-panel="users"] *)) {
  max-width: 75ch;
}

/* Platform > Email: the server settings, and beside them the test send (the
   Platform account has no rail, so its panel is wider than these widths assume). */
html:not(.mobile) #settingsPage [data-platform-subpanel="email"] {
  display: grid;
  grid-template-columns: repeat(var(--settings-cols), minmax(0, 1fr));
  column-gap: 2rem;
  align-items: start;
}
html:not(.mobile) #settingsPage [data-platform-subpanel="email"] > :not(form, .email-test-row) { grid-column: 1 / -1; }
html:not(.mobile) #settingsPage [data-platform-subpanel="email"] > .email-test-row {
  margin-top: calc((2 - var(--settings-cols)) * 1rem);
  padding-top: calc((2 - var(--settings-cols)) * 1rem);
  border-top-width: calc((2 - var(--settings-cols)) * 0.0625rem);
}
/* Preferences: a choice of three keeps the width it had in the 40rem column
   (the swatch and "Saved" stay beside it), however wide the column is. */
html:not(.mobile) #accountPage .pref-row > .form-field { max-width: 33rem; }
/* Platform > Backups and Airspace data: a form keeps a field width. */
html:not(.mobile) #settingsPage :is(#backupSettingsForm, .backup-last-run, #aipUploadForm, .aip-reingest) { max-width: 64rem; }

/* ---- end of SETTINGS SHARED ---------------------------------------------- */

/* ==========================================================================
   UX SHARED
   The primitives of the app-wide usability round (design 5.1), written once and
   reused by every package after the foundation: the row that is flashed when it
   is new, the "..." button on a row, the rows that act as buttons, a date a
   calendar will not let you pick. Rules that belong to ONE package go under that
   package's anchor at the foot of this block (and, for the phone, at the foot of
   the html.mobile UX block). The anchors are named for the packages: ux:U0b page
   notice and disabled style, U1a alerts and account, U1b Dashboard, U1c phone
   Back, U2 signed-out screens, U3 Plan list, U4a/U4b Plan card, U5a wizard, U5b
   risk dialog, U5c area editor and checklist, U6a/U6b/U6c Billing, U7a/U7b
   Flights, U8 Airspace, U9 Hazards and Occurrences, U10 Documents. (H0, K1 and M1
   leave a local anchor comment beside their own rules instead.)
   ========================================================================== */

/* A row that is a plain element acting as a button (makeRowFocusable: a div, a
   tr, an li with role="button"). Zero-specificity baselines under :where(), so
   any row's own rule wins and only a row with none is filled in: the pointer,
   and a quiet hover tint (--row-hover, the same wash .queue-item has always had,
   from the theme's channels, so it follows both themes). */
:root { --row-hover: rgba(var(--rgb-overlay), 0.045); }
:where([role="button"]:not(button, a, summary, input, textarea)) { cursor: pointer; }
/* Only where a pointer can hover: a tap on a phone leaves :hover stuck on the row
   until the next tap. The risk matrix cell draws its own swatch and takes none. */
@media (hover: hover) {
  :where([role="button"]:not(button, a, summary, input, textarea, .risk-matrix-cell)):hover {
    background-color: var(--row-hover);
  }
}

/* A row that has just been created or changed, found again by the eye: a green
   wash that fades back to the row's own background (the keyframe names only its
   start, so the end is whatever the row already is). Add the class once; the
   animation plays once; add it again after removing it to flash again. A person
   who has asked for less motion gets no flash at all. */
@keyframes row-flash {
  from { background-color: rgba(var(--rgb-green), 0.3); }
}
@media (prefers-reduced-motion: no-preference) {
  .row-flash { animation: row-flash 1.8s ease-out; }
}

/* The "..." on a row (attachRowMenuButton): content-width, quiet until the row is
   the one in play. On a mouse it shows on the hovered, focused or selected row
   (and while its own menu is open); it takes opacity, not display, so it stays a
   tab stop and the focus reveals it. The phone shows it always (UX SHARED, phone). */
.row-menu-button {
  flex: 0 0 auto;
  width: auto;
  min-height: 0;
  margin: 0;
  padding: 0.125rem 0.5rem;
  border: 0.0625rem solid transparent;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--muted);
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.row-menu-button:hover:not(:disabled),
.row-menu-button[aria-expanded="true"] {
  background: rgba(var(--rgb-slate), 0.18);
  color: var(--text);
}
html:not(.mobile) [data-row-menu] .row-menu-button { opacity: 0; }
html:not(.mobile) [data-row-menu]:is(:hover, :focus-within, .selected, [aria-current="true"], [aria-selected="true"]) .row-menu-button,
html:not(.mobile):where(:not(.using-pointer)) .row-menu-button:focus,
html:not(.mobile) .row-menu-button[aria-expanded="true"] {
  opacity: 1;
}

/* A date field with an earliest day (its own min, or data-min-from another field):
   the days before it stay on the calendar and cannot be picked, and a date that
   is already earlier is flagged on the trigger, in the same red the form fields
   use for a field that needs another look. */
.date-cal-day:disabled,
.date-cal-day.is-disabled {
  background: none;
  color: var(--fg-disabled);
  cursor: not-allowed;
  opacity: 0.55;
  text-decoration: line-through;
}
/* The hover wash is (0,3,0), above the disabled rule: a day that cannot be picked
   must not light up under the pointer. */
.date-cal-day:disabled:hover,
.date-cal-day.is-disabled:hover { background: none; }
.date-trigger[aria-invalid="true"],
.date-trigger[aria-invalid="true"]:hover:not(.is-disabled) { border-color: var(--danger); }

/* ---- Per-package anchors (desktop) --------------------------------------- */
/* ux:U0b */
/* Page notice and disabled style: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE PAGE NOTICE (design 5.2). #status is the live region and .page-notice the
   box around it, so the Dismiss button can sit beside the text: a write replaces
   everything INSIDE #status. The box is a child of the page's header and
   positioned against it: on a desktop the header has no height and sits at the
   top of the main area, so `top: 100%` is the top of the page, and the box is as
   wide as the page less whatever app.js (noticePlace) measures on a map page - a
   top bar to sit below, a legend and a control column to stay between. It never
   takes layout space.
   z-index 950: above the page and the maps, BELOW .modal-overlay (1000) and the
   confirm dialog (1100). app.js hides it while a dialog is open as well; the layer
   is the second guard. Idle it is a 1px clipped box and not display:none, so the
   live region is already in the accessibility tree when it first speaks. */
.app-main > header { position: relative; }
.page-notice {
  position: absolute;
  z-index: 950;
  top: calc(100% + var(--page-notice-y, -0.75rem));
  left: var(--page-notice-left, 0px);
  right: var(--page-notice-right, 0px);
  box-sizing: border-box;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  pointer-events: none;
}
.page-notice[data-show] {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  width: fit-content;
  max-width: min(40rem, calc(100% - var(--page-notice-left, 0px) - var(--page-notice-right, 0px)));
  height: auto;
  margin: 0 auto;
  padding: 0.5rem 0.5rem 0.5rem 0.875rem;
  overflow: visible;
  clip-path: none;
  white-space: normal;
  pointer-events: auto;
  background: var(--sheet-bg);
  border: 0.0625rem solid var(--frame-border);
  border-radius: 0.5rem;
  box-shadow: var(--shadow-menu);
}
.page-notice[data-show][data-kind="info"] { border-color: rgba(var(--rgb-green), 0.5); }
.page-notice[data-show][data-kind="error"] { border-color: rgba(var(--rgb-red), 0.6); }
.page-notice[data-show][data-kind="busy"] .status { color: var(--text); }
.page-notice[data-show] .status { flex: 1 1 auto; padding-top: 0.125rem; }
/* Work in progress: a small ring in front of the words. Generated content with no
   text, so it is not read out; the words and aria-busy carry the meaning. */
.page-notice[data-show][data-kind="busy"]::before {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: 0.125rem;
  border-radius: 50%;
  border: 0.125rem solid rgba(var(--rgb-slate), 0.25);
  border-top-color: var(--green);
  animation: map-loading-spin 0.8s linear infinite;
}
/* A menu the person has just opened (right-click, long-press, the "..." button) paints
   over a notice that is up: it is a gesture, the notice is not. Sits between the notice
   (950) and the dialogs (1000). `html` in front outranks .map-context-menu's own rule,
   which comes later in the file, without editing it. */
html .map-context-menu { z-index: 960; }
.page-notice-dismiss {
  flex: none;
  width: auto;
  min-height: 0;
  margin: 0;
  padding: 0.25rem;
  border-radius: 0.375rem;
  background: none;
  color: var(--muted);
  line-height: 0;
}
.page-notice-dismiss:hover:not(:disabled) {
  background: rgba(var(--rgb-overlay), 0.08);
  color: var(--text);
  filter: none;
}
@keyframes page-notice-in {
  from { opacity: 0; transform: translateY(-0.25rem); }
}
@media (prefers-reduced-motion: no-preference) {
  .page-notice[data-show] { animation: page-notice-in 0.16s ease-out; }
}
@media (prefers-reduced-motion: reduce) {
  .page-notice[data-show][data-kind="busy"]::before { animation: none; }
}
/* A disabled button, dark theme (design 5.2). It used to read as MORE alive than
   the enabled one: `button:disabled` painted a lighter grey than the secondary
   button's own fill and left the label white. Now the label dims to the faint
   text colour, the fill drops below every enabled button's (--button-disabled-bg,
   in the token block), and nothing lifts on hover (the hover rule already leaves
   :disabled alone). The mirror of the light theme's one-line rule further up. The
   two dropdown triggers and the date trigger are buttons too, but a disabled one
   is a read-only VALUE (the job card's view mode), not an action that is off: it
   keeps the colour it has, which the light theme rescues for them in its own rule. */
html:not([data-theme="light"]) button:disabled:not(.searchable-select-trigger):not(.multiselect-dropdown-trigger):not(.date-trigger):not(.job-goto-btn) {
  color: var(--fg-faint);
}

/* ux:U1a */
/* Alerts and account: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The alerts panel's groups: one heading per kind with its count, the rows, and a
   "Show all N" link past five. */
.alerts-dropdown-counts {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
}
.alert-group { margin-bottom: 0.625rem; }
.alert-group:last-child { margin-bottom: 0; }
.alert-group-title {
  margin: 0.375rem 0.125rem 0.375rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--muted);
}
.alert-group-toggle {
  display: inline-flex;
  align-items: center;
  margin: 0.125rem 0.125rem 0;
  font-size: 0.8125rem;
}
/* A row with nowhere to go (equipment): plain, no pointer, no hover wash. */
.alert-row.alert-row-static { cursor: default; }
.alert-row.alert-row-static:hover { background: var(--tile-bg); }
/* "22 Mar 2025" stays on one line however the sentence around it wraps. */
.alert-date { white-space: nowrap; }
/* Rows on screen but the last refresh failed. */
.alerts-stale {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  margin: 0 0.125rem 0.625rem;
  font-size: 0.8125rem;
  color: var(--fg-empty);
}

/* The account popover's identity block and rows. */
.orgswitcher-identity {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}
.orgswitcher-identity[hidden] { display: none; }
.orgswitcher-identity-name,
.orgswitcher-identity-email,
.orgswitcher-identity-role {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.orgswitcher-identity-name { font-weight: 700; color: var(--text); }
.orgswitcher-identity-email { font-size: 0.8125rem; color: var(--fg-soft); }
.orgswitcher-identity-role { font-size: 0.75rem; color: var(--muted); }
.orgswitcher-account-button { width: 100%; justify-content: flex-start; text-align: left; }
.orgswitcher-heading {
  margin: 0.25rem 0 -0.25rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--muted);
}
.orgswitcher-heading[hidden] { display: none; }

/* The page's main region takes focus when the page changes (app.js, droneops:pagechange):
   a programmatic stop, not a control, so no ring. */
.app-main:focus { outline: none; }

/* "Update ready": Later, beside Reload. */
.app-update-bar-later {
  min-height: 0;
  margin: 0;
  padding: 0.25rem 0.5rem;
  border: 0;
  background: none;
  width: auto;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}
.app-update-bar-later:hover { filter: none; text-decoration-thickness: 0.125rem; }

/* ux:U1b */
/* Dashboard: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The sizes the Dashboard is laid out from. A row of each list is measured (a name line, a
   second line, the padding), and three of them is the least a list may be given; the ring
   of a doughnut stays between the least that still reads and the most that is only bigger. */
#dashboardPage {
  --dash-week-row-h: 3.5rem;
  --dash-occ-row-h: 3.85rem;
  --dash-ring-min: 7.5rem;
  --dash-ring-max: 18.75rem;
}

/* The load notice is a live region that is always in the page. Empty it takes no room and
   cannot be seen (it is still there, so a failure that arrives is read out). */
.card-copy.dashboard-load-notice:empty {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}
.dashboard-load-notice .settings-link { margin-inline-start: 0.25rem; }

/* A headline figure that opens the page holding it: the same tile, as a link. It sets no
   display of its own, so the link is laid out as the tile it is (.stat-tile's, or the
   Dashboard headline's below). It once said display: block, and a.stat-tile-link outranks
   .stat-tile: the link lost the tile's 0.25rem gap between the label and the figure, so
   Upcoming and under way's "1" and Flights logged's "1,208" sat 0.25rem (3.5px on a phone)
   higher than the plain tiles' figures beside them in the same row. */
a.stat-tile-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
a.stat-tile-link:hover {
  background: rgba(var(--rgb-green), 0.12);
  border-color: rgba(var(--rgb-green), 0.4);
}
a.stat-tile-link:focus { outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: 0.125rem; }

/* A chart card with no ring: loading, failed (with Retry), or nothing flown yet (with the
   way to change that). It takes the body of the card, centred. */
.dashboard-chart-state {
  flex: 1 1 0;
  min-height: var(--dash-ring-min);
  display: flex;
  align-items: center;
  justify-content: center;
}
.dashboard-chart-state[hidden] { display: none; }
.dashboard-chart-state > .list-state { justify-content: center; text-align: center; padding: 0; }

/* Quick actions in the card's own width, not the window's (the sidebar can fold). The
   narrow answer is two even rows with the longer one first; a row of all of them comes
   once each button has about 10rem (the longest label with its icon and padding is just
   under that): for the full seven the card's content is 88rem across, which is a window
   of about 1500px with the sidebar open. */
.card.dashboard-actions { container-type: inline-size; container-name: dash-actions; }
.dashboard-actions-grid[data-count="1"] { --dash-action-cols: 1; }
.dashboard-actions-grid[data-count="2"] { --dash-action-cols: 2; }
.dashboard-actions-grid[data-count="3"] { --dash-action-cols: 3; }
.dashboard-actions-grid[data-count="4"] { --dash-action-cols: 4; }
.dashboard-actions-grid[data-count="5"] { --dash-action-cols: 3; }
.dashboard-actions-grid[data-count="6"] { --dash-action-cols: 3; }
.dashboard-actions-grid[data-count="7"] { --dash-action-cols: 4; }
@container dash-actions (min-width: 64rem) {
  .dashboard-actions-grid[data-count="5"] { --dash-action-cols: 5; }
}
@container dash-actions (min-width: 76rem) {
  .dashboard-actions-grid[data-count="6"] { --dash-action-cols: 6; }
}
@container dash-actions (min-width: 88rem) {
  .dashboard-actions-grid[data-count="7"] { --dash-action-cols: 7; }
}

/* ux:U1c */
/* Phone Back: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U2 */
/* Signed-out screens, questionnaire portal, User guide: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE SIGN-IN PAGE'S LOOK (branch ux-login-look; Josh, 2026-11-02: "subtle
   background animation, taller login panel, full width login button, larger
   logo, 'log in to..' instruction"). The sign-in card, and the invitation, lock
   and password reset cards with it where the same thing applies.
   ux-login-look-frontend.mjs holds it; scripts/login-look-mutations.sh proves
   that gate can go red.

   1. THE PANEL. All four cards are 27rem wide with 3rem of padding above
      (the "#loginCard, #acceptInviteView .login-card..." rule beside
      .login-card, near the top of the file; a phone narrows the sides). The
      sign-in card is also at least 40rem tall, a column, with the Terms and
      Privacy links pushed to its foot, and the space it gains sits above the
      legal links rather than between the fields. 40rem holds the password
      step and the plain code step, so the card does not move between them on
      a computer (45rem on a phone with the room for it, whose code step is
      taller: the ux:U2 phone block). Two screens are taller and the card grows
      for them: the code step with "Trust this device" (an organisation's
      option, off by default) and the two-factor setup with its QR code. They
      were not made the minimum, which would have pushed every password step
      into scrolling on a small laptop or a phone browser tab. A window under
      600px tall gets the old height back (last in this block); a card taller
      than the window scrolls inside the view at any height (.login-view).
   2. THE LOGO. The lockup (mark + wordmark, docs/BRAND.md) at about one and
      a half times the sidebar's: 2.875rem tall against 1.875rem, on every
      card that carries it. Its gap is the lockup's own (.app-brand), so the
      two images stay the drawn lockup at any height.
   3. THE HEADING. "Sign in to DroneOps", centred under the logo, in the words
      the button and every link in the app use (index.html says why).
   4. THE BUTTON. Full width by .btn-block (see #loginSubmitButton) and a
      little taller than the app's standard button.
   5. THE SKY. One decorative layer (index.html, .login-sky, after the six
      signed-out views) with two soft glows, the shell's green and blue, that
      drift across the top of the page and back: 46 and 58 seconds a way, so
      they never line up the same twice in a sitting. Only transform moves (no
      layout, no repaint: the browser moves two finished layers), so typing on
      a phone pays nothing for it. Shown only after a visible signed-out view
      (.login-view:not([hidden]) ~ .login-sky): signed in, every view is hidden
      and the layer is display: none, so nothing animates behind the app. Off,
      glows left where they start, for anyone whose system asks for reduced
      motion. A hidden tab draws no frames, so it needs no pause of its own.
      It paints at z-index 0 over the view's plain background and under the
      card, which sits at z-index 1. */
.login-view > .login-card {
  position: relative;
  z-index: 1;
  /* Centred down the view while it fits; when it does not, its top stays at
     the top of the view's scroll range (the .login-view rule says why). */
  margin-block: auto;
}
#loginCard {
  display: flex;
  flex-direction: column;
  min-height: 40rem;
}
#loginCard .login-legal { margin-top: auto; padding-top: 1.5rem; }

.login-card .app-brand { --brand-h: 2.875rem; }

#loginTitle {
  margin: 0 0 2rem;
  font-size: 1.75rem;
  font-weight: 650;
  text-align: center;
}
#loginCard .form-grid { gap: 1.125rem; }

.login-card .btn-block {
  margin-top: 1.5rem;
  padding-block: 0.8125rem;
}

.login-sky { display: none; }
.login-view:not([hidden]) ~ .login-sky {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  contain: strict;
}
.login-sky::before,
.login-sky::after {
  content: "";
  position: absolute;
  width: 110vmax;
  height: 110vmax;
  border-radius: 50%;
  will-change: transform;
}
.login-sky::before {
  left: -40vmax;
  top: -50vmax;
  background: radial-gradient(closest-side, var(--login-sky-1), transparent);
  animation: login-sky-drift-1 46s ease-in-out infinite alternate;
}
.login-sky::after {
  right: -45vmax;
  top: -55vmax;
  background: radial-gradient(closest-side, var(--login-sky-2), transparent);
  animation: login-sky-drift-2 58s ease-in-out infinite alternate;
}
@keyframes login-sky-drift-1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(24vw, 34vh, 0) scale(1.15); }
}
@keyframes login-sky-drift-2 {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to { transform: translate3d(-28vw, 30vh, 0) scale(0.95); }
}
@media (prefers-reduced-motion: reduce) {
  .login-sky::before,
  .login-sky::after { animation: none; }
}
/* A short window (a phone held sideways, a small laptop window): the panel gives
   back its extra height and some padding, so less of it has to be scrolled to
   (the view scrolls at any height: the .login-view rule). Last in the block, so
   it outranks the rules above it at the same specificity. */
@media (max-height: 600px) {
  #loginCard { min-height: 0; padding-block: 1.5rem; }
  .login-card .app-brand { margin-bottom: 1.25rem; }
  #loginTitle { margin-bottom: 1.25rem; }
}

/* `.login-card button { margin-top: 1.125rem }` (0-1-1) beat the link rules
   `.login-legal-link, .legal-back-link, .inline-link { margin: 0 }` (0-1-0), so
   every text link on the card carried a 18 px top margin: a gap inside the Terms
   intro, and a footer whose links sat below their separator dot. Scoped to the
   card so the link rules win again. Only the top margin: .legal-back-link keeps
   its own bottom one. */
.login-card .login-legal-link,
.login-card .legal-back-link,
.login-card .inline-link {
  margin-top: 0;
}
.login-card .login-mfa-back { margin-top: 0.625rem; }
.login-card .login-sign-in-link { margin-top: 0.25rem; font-size: 0.875rem; }

/* The forgotten-password line under the form, said quietly: "Forgot password?"
   where a reset link can be emailed, else who to ask. Neither shows until the
   server has said which, so the line holds its one-line height meanwhile and
   the card does not move when the answer lands. */
.login-hint {
  margin: 1rem 0 0;
  min-height: 1.5em;
  text-align: center;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--muted);
}
.login-hint[hidden] { display: none; }

/* The password reset card (#passwordResetView). The sent screen's two hints sit
   closer than a paragraph apart: they are one list of what to try. The code
   block follows the password rules; its link swaps code and recovery code, as
   at sign-in. "Send a new link" and "Try again" are the card's one action in
   their states, so they keep the card button's own spacing. */
/* The title and the line under it hold the person's address ("Link sent if <address> has
   an account", "For <address>"). An address has no spaces to break at, so a long one ran
   past the card's edge on a phone and scrolled the page sideways: it breaks anywhere
   instead. The heading takes the keyboard when a state has no control (app.js
   keepPasswordResetFocus), and is not a control: no ring. */
#passwordResetView .login-title,
#passwordResetView .card-copy { overflow-wrap: anywhere; }
#passwordResetTitle:focus { outline: none; }
.password-reset-sent .card-copy { margin-bottom: 0.5rem; }
.password-reset-sent .card-copy:last-child { margin-bottom: 0; }
.password-reset-code { margin-top: 1rem; }
#passwordResetView .password-reset-code .card-copy { margin-bottom: 0.625rem; }
/* Centred, as the sign-in card's own swap link is (.login-recovery-switch). */
#passwordResetView .password-reset-code .login-legal-link {
  display: block;
  width: fit-content;
  margin: 0 auto 0.25rem;
  font-size: 0.8125rem;
}

/* THE OTHER SIGNED-OUT CARDS CENTRE WHAT THE SIGN-IN CARD CENTRES (Josh, 2026-10-22:
   "the buttons on the new modals and emails are left justified instead of centered").
   The sign-in card's one action was centred then (#loginSubmitButton) and so are its red
   lines (.login-error, .login-notice, 2026-10-19); the invitation, the lock page and
   the password reset cards had copied the invitation's left-aligned buttons and
   messages. Body copy, headings and form fields stay as they were: it is the action
   and the line about it that read as a pair.

   Since the sign-in button went the full width of its card (THE SIGN-IN PAGE'S LOOK,
   above), so did each of these cards' one action, to match it, opted into with
   .btn-block in the markup (button-width-default):
     invitation  Activate my account
     lock page   Lock my account
     reset       Send link, Set password, Send a new link, Try again
   What is left is the dead invitation's way out, Sign in, a text link: centred at its
   content width by display:block (a button is inline-block, which ignores auto
   margins), width:fit-content (a block button would stretch to the card) and auto side
   margins, the id beating `.login-card button:not(.btn-block) { width: auto }`. [hidden]
   still hides it (the global guard is !important). The text link that goes back to sign
   in is centred already (.login-mfa-back). */
#acceptInviteSignInLink {
  display: block;
  width: fit-content;
  margin-inline: auto;
}
/* The red line about the action, centred like .login-error: a two-line message wraps
   centred. Success lines (green) are the same element and sit the same way. */
#acceptInviteView .form-message,
#securityLockView .form-message,
#passwordResetView .form-message {
  text-align: center;
}

/* A refused code: the six boxes take the danger colour, as an invalid input does. */
.otp-field.is-invalid .otp-box,
.otp-field.is-invalid.is-focused .otp-box.is-next { border-color: var(--danger); }
/* The slot the next digit lands in keeps its ring, in the danger colour: the focus
   rule below it (0-4-0) would otherwise paint the first box green and the mark
   would read as having cleared. */
.otp-field.is-invalid.is-focused .otp-box.is-next {
  box-shadow: 0 0 0 0.125rem color-mix(in srgb, var(--danger) 25%, transparent);
}
#loginForm input[aria-invalid="true"],
#acceptInviteForm input[aria-invalid="true"],
#passwordResetView input[aria-invalid="true"] { border-color: var(--danger); }

/* The enrolment key breaks between its groups of four, never inside one. */
.login-mfa-secret { word-break: normal; overflow-wrap: anywhere; }
.login-mfa-secret code { white-space: normal; letter-spacing: 0.04em; }

/* Show password under the invitation's two password fields. */
.login-show-password { margin: 0 0 0.875rem; }

/* The password rules tick as the password is typed: a mark in front of each,
   green once met, a quiet dot while open, nothing claimed for a rule only the
   server can check (the breach screen). The list is the same element the
   Change password dialog uses, so both read the same. */
.password-requirements li[data-state] {
  list-style: none;
  display: flex;
  gap: 0.375rem;
  align-items: baseline;
}
.password-requirements:has(li[data-state]) { padding-left: 0; }
.password-requirements li[data-state]::before {
  content: "\00b7";
  flex: 0 0 0.875rem;
  text-align: center;
  color: var(--muted);
}
.password-requirements li[data-state="met"] { color: var(--fg-body); }
.password-requirements li[data-state="met"]::before {
  content: "\2713";
  color: var(--green);
  font-weight: 700;
}

/* ux:U3 */
/* Plan list: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE SORT ICON, on a phone and a computer alike (a phone since 2026-11-01, the computer since
   Josh's 2026-11-02 "the same icon and flyout as on mobile"). Sort is the Sort menu's own trigger
   (the app's menu, bound to the same sort the column headings set: one control, so one source of
   truth) at the end of the search box's row, drawn as the up-and-down arrows alone, a square at
   the toolbar's control height (--plan-control-h: the shared dropdown box's on a computer, the
   phone's compact height on a phone; the toolbar blocks set it). The value it shows is still its
   text, hidden (moved out of the box and transparent), so its accessible name stays "Sort: Newest"
   - the label's word and the order in force - and changes as the order does. The word "Sort" is
   hidden the visually-hidden way for the same reason. Border, fill, height, hover and the focus
   mark stay the shared dropdown box's. The computer had a row of its own for it, the word "Sort"
   beside a menu showing the order, until 2026-11-02. */
.jobs-table-toolbar .jobs-sort-field .searchable-select-trigger {
  width: var(--plan-control-h);
  padding-left: 0;
  padding-right: 0;
  color: transparent;
  text-indent: 100%;
  text-overflow: clip;
  background-image: var(--sort-icon);
  background-position: center;
  background-size: 1.25rem 1.25rem;
}
.jobs-table-toolbar .jobs-sort-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
/* The menu it opens is the usual panel, hung from the icon's right edge: the icon ends its row, so
   a panel opening from its left edge runs past it (on a phone enhanceSelect slid it back from the
   screen's edge, 6px past the icon; on a computer it would run out over the job card). It opens
   below the icon unless the room is above (panelPlacement), and the order in force carries a tick. */
.jobs-table-toolbar .jobs-sort-field .searchable-select-panel {
  left: auto;
  right: 0;
}
.searchable-select:has(> #jobsSortSelect) .searchable-select-option {
  position: relative;
  padding-left: 1.75rem;
}
.searchable-select:has(> #jobsSortSelect) .searchable-select-option.is-selected::before {
  content: "\2713";
  position: absolute;
  left: 0.625rem;
  font-weight: 700;
}
/* "2 filters · Clear", once any filter is on: at the right end of the "Jobs" heading's row on a
   computer (the toolbar's head, below), at the right end of the Create job row on a phone (THE
   PLAN PAGE ON A PHONE, 3). It shared the computer's Sort row until 2026-11-02. */
.jobs-filter-cue {
  flex: none;
  color: var(--muted);
  font-size: 0.8125rem;
  white-space: nowrap;
}
.jobs-filter-cue[hidden] { display: none; }
.jobs-filter-cue .settings-link { font-size: inherit; }

/* A list with no rows says why inside the list's own box (setListState on the tbody). The
   list's body is a block of grid rows below 46rem, so the state row is a block too. */
#jobsTableBody > tr.list-state { display: block; }
#jobsTableBody > tr.list-state > td {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  padding: 0.875rem 0.75rem;
  border: 0;
}
/* Rows are on screen and something behind them failed: one line under the list. */
.jobs-list-stale { flex: none; }
.jobs-list-stale:empty { display: none; }
.jobs-list-stale .list-state { padding: 0.5rem 0 0; }

/* The one hint under the list: what a right-click on a row opens. A phone long-presses. */
.jobs-row-menu-hint {
  flex: none;
  margin: 0.5rem 0 0;
  color: var(--muted);
  font-size: 0.75rem;
  text-align: center;
}
.jobs-row-menu-hint[hidden] { display: none; }

/* The status filter lists all eight statuses without scrolling, so Cancelled, the last,
   is not hidden below the 15rem cap every other dropdown keeps. enhanceSelect's
   panelPlacement still shrinks it when the window is short. */
.all-jobs-card .searchable-select:has(> #jobsStatusFilter) .searchable-select-list {
  max-height: none;
}

/* Headings that sort are reachable by keyboard now; the ring says where it is. */
.jobs-table-head .job-sortable:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: var(--keyboard-ring-inset);
}

/* ux:U4a */
/* Plan card header and lifecycle: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The card header: the client's name with its status pill after it, then one stage
   line (jobStageLine) saying where the job stands - on a phone the card is a
   full-screen sheet over the list, so the header is the only place that can say it.
   The subtitle keeps the card-copy look; the wrapper carries the gap below it. */
.job-preview-subline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  margin: -0.125rem 0 0.375rem;
}
.job-preview-subline .card-copy {
  margin: 0;
  min-width: 0;
}
.job-preview-subline .status-pill {
  flex: none;
}
.job-preview-stage-line {
  margin: 0 0 0.625rem;
  color: var(--fg-copy);
  font-size: 0.875rem;
  overflow-wrap: anywhere;
  /* Two lines at most (review 2026-11-07 item 15): a 500-character sent-back
     reason crushed the header buttons. The rest unfolds on a tap (app.js
     syncJobStageLineClamp). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.job-preview-stage-line.is-clamped { cursor: pointer; }
.job-preview-stage-line.is-expanded {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}
.job-preview-stage-line.is-clamped:focus { outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: 0.125rem; border-radius: 0.25rem; }
/* The header's left side takes what the buttons leave, never the other way round:
   with no flex of its own it shrank in step with them, so a long line squeezed
   Continue draft and Submit for approval into narrow stacks. The buttons keep
   their own width and wrap as a row when the card is narrow. */
#jobPreviewContent > .modal-header > div:first-child {
  flex: 1 1 0%;
  min-width: min(14rem, 50%);
}
.job-preview-header-actions > button { flex: none; }
/* What a header action did, as a status line; the failure line is its sibling. */
.job-preview-action-status {
  margin: 0.5rem 0 0;
}
/* The Cancel card is short, and its reason menu is the one control that has to open
   past its edge: capped at 15rem and clipped to the card it showed six of the seven
   rows and hid "Other" behind a scroll nobody expects. Left to hang out it gets the
   screen, and with the cap lifted (as the report picker's is) enhanceSelect's own
   placement still shrinks it to the room there is. */
#jobCancelOverlay .modal-card {
  overflow: visible;
}
#jobCancelOverlay .searchable-select-list {
  max-height: none;
}
/* The two fields sit a row apart, as in every form card. */
#jobCancelForm {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* ux:U4b */
/* Plan card panels: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* READ-ONLY VALUES ARE PLAIN VALUES (P-2b, P-17). The card is display-only: its fields are
   disabled controls that were drawn as boxes (a border, a fill, a caret, a calendar icon, text
   dimmed to 60%), so every one looked like a field waiting to be filled, and the empty ones were a
   wall of blank boxes. Here a value is text under its label: no box, no caret, no icon, the normal
   text colour; an empty one reads "Not set" in the muted colour (the markup and the renderers put
   the words there). Scoped to the card's panels, so no form in a dialog changes; the field ids are
   untouched. The extra classes outrank both themes' read-only rules (a grey well in the light one). */
.job-preview-card .job-preview-tab-panel .form-field input:disabled,
.job-preview-card .job-preview-tab-panel .form-field textarea:disabled,
.job-preview-card .job-preview-tab-panel .form-field select:disabled,
.job-preview-card .job-preview-tab-panel .form-field .searchable-select-trigger:disabled,
.job-preview-card .job-preview-tab-panel .form-field .multiselect-dropdown-trigger:disabled,
.job-preview-card .job-preview-tab-panel .form-field .date-trigger.is-disabled {
  min-height: 0;
  padding: 0;
  border-color: transparent;
  background-color: transparent;
  box-shadow: none;
  opacity: 1;
  color: var(--text);
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
}
.job-preview-card .job-preview-tab-panel .form-field textarea:disabled {
  resize: none;
  field-sizing: content;
  min-height: 0;
}
.job-preview-card .job-preview-tab-panel .form-field .date-trigger.is-disabled .date-trigger-icon {
  display: none;
}
/* "Not set": the muted colour, wherever a read-only value is empty. */
.job-preview-card .job-preview-tab-panel .form-field input:disabled::placeholder,
.job-preview-card .job-preview-tab-panel .form-field textarea:disabled::placeholder {
  color: var(--muted);
  opacity: 1;
}
.job-preview-card .job-preview-tab-panel .form-field .searchable-select-trigger.is-placeholder:disabled,
.job-preview-card .job-preview-tab-panel .form-field .multiselect-dropdown-summary.multiselect-dropdown-placeholder,
.job-preview-card .job-preview-tab-panel .form-field .date-trigger.is-disabled.is-empty .date-trigger-text {
  color: var(--muted);
}

/* The billing line, on Overview for viewers with billing access. */
.job-billing-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.75rem;
  margin: 0.875rem 0 0.75rem;
  font-size: 0.875rem;
}
.job-billing-line-label {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.job-billing-line-status { color: var(--muted); }

/* A job with nothing to draw says what to do, where the map would have been. */
.job-location-none { margin: 0.5rem 0 0; }
#jobPreviewLocationMap.is-none { display: none; }

/* The job report's outcome, in its own row. */
#jobPreviewReportRow .form-message { margin: 0.5rem 0 0; }

/* Site details: a contact's links are .settings-link buttons, and Erase is a group of its own,
   to the far side of the row and in the colour of what it does. */
.sq-recipient-actions { flex-wrap: wrap; }
.sq-recipient-actions .settings-link { padding: 0.25rem 0; }
.sq-recipient-erase {
  margin-left: 1.25rem;
  padding-left: 1.25rem;
  border-left: 0.0625rem solid var(--line);
}
.sq-recipient-erase .settings-link { padding: 0.25rem 0; }
.settings-link.is-danger { color: var(--fg-error-2); }
.settings-link.is-danger:hover:not(:disabled) { color: var(--fg-error); }
/* The line after a decision: the fact, then the one thing still to do. */
.sq-decided { margin-bottom: 0; }
.sq-decided .settings-link { font-size: inherit; }
.sq-decided-error { color: var(--danger); }
/* A confirm that lists what goes and what stays: one fact to a line. */
#confirmDialogMessage.confirm-lines { white-space: pre-line; }
/* The overdue note's "cancel it" is a link inside a sentence. */
.job-section-flags .settings-link { font-size: inherit; }

/* ux:U5a */
/* Create and edit job wizard: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* What a draft needs and what Submit needs, once, under the steps (#jobCreateScope).
   The bullet it speaks of sits on the labels of the fields Submit asks for, while the
   form is a new job or a draft. Where the organisation requires an aircraft or a
   location those are marked too (is-required, set from the settings). The alt text
   after the slash keeps a screen reader from reading "bullet". */
#jobCreateScope {
  margin: 0.5rem 0 0.25rem;
  font-size: 0.8125rem;
}
#jobCreateCard.shows-required .form-field.is-required > span:first-child::after,
#jobCreateCard.shows-required .job-location-empty.is-required .job-location-empty-cta::after {
  content: " \2022";
  content: " \2022" / "";
  color: var(--fg-warn);
}

/* A refused Submit lists what is missing in the amber the flagged fields wear: one
   colour for one meaning (it needs filling in; it did not go wrong). */
.form-message.error.needs { color: var(--fg-warn); }

/* The step tabs of a form with something missing carry the card's own dot. */
#jobCreateTabs .job-preview-tab.has-gap::after {
  content: "";
  display: inline-block;
  width: 0.375rem;
  height: 0.375rem;
  margin-left: 0.3125rem;
  border-radius: 50%;
  background: rgb(var(--rgb-amber));
  vertical-align: 0.125rem;
}

/* "optional", "today unless changed": beside a label, quieter than it. */
#jobCreateCard .form-hint-inline {
  color: var(--muted);
  font-weight: 400;
}
/* The end date before the start, under the end date. */
.form-field > .form-field-note {
  color: var(--danger);
  font-size: 0.75rem;
  font-weight: 400;
}

/* The plan a flown job's logs are checked against, when it is not the dates above:
   one quiet line and its button, under the end date. */
.job-create-plan {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem 0.75rem;
}
.job-create-plan > small {
  color: var(--muted);
  font-size: 0.75rem;
}

/* The Create job chooser holds its place: the card grows from two choices to a
   client picker and the form, and centred it re-centred on every step, so what the
   person had just pressed moved from under the pointer. Anchored near the top it
   only grows downward. */
#jobStartOverlay {
  align-items: flex-start;
  padding-top: min(14vh, 7rem);
}

/* ux:U5b */
/* Risk dialog: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The score picker: five segments in a radio group (a chance or an effect, 1 to
   5), the picked number's word under it. The radio inputs cover their segment, so
   the whole segment is the hit area and the arrow keys work natively. */
.risk-score-group {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: 0.1875rem;
}
.risk-score-name { display: none; }
.risk-score-readout {
  min-height: 0.75rem;
  font-size: 0.625rem;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--muted);
}
/* An empty readout keeps its height, so a row does not jump when a score is picked. */
.risk-score-readout[data-empty="true"] { visibility: hidden; }
.risk-seg-row {
  display: inline-grid;
  grid-template-columns: repeat(5, 1.5rem);
  overflow: hidden;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
}
.risk-seg {
  position: relative;
  display: flex;
  height: 1.75rem;
  cursor: pointer;
  user-select: none;
}
.risk-seg + .risk-seg { border-left: 0.0625rem solid var(--control-border); }
.risk-seg input[type="radio"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.risk-seg input[type="radio"]:disabled { cursor: not-allowed; }
.risk-seg-face {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--fg-body);
  pointer-events: none;
}
.risk-seg input:not(:disabled):not(:checked):hover + .risk-seg-face { background: var(--row-hover); }
.risk-seg input:checked + .risk-seg-face {
  background: rgba(var(--rgb-blue), 0.3);
  color: var(--fg-strong);
  font-weight: 800;
  box-shadow: inset 0 -0.1875rem 0 var(--blue);
}
.risk-seg input:disabled + .risk-seg-face { opacity: 0.5; }
.risk-seg input:focus + .risk-seg-face {
  outline: var(--keyboard-ring-width) solid var(--blue);
  outline-offset: var(--keyboard-ring-inset);
}

/* The "Doesn't apply" tick: its words are a phone's (a computer has the column
   header), and the label is the thing to tap there. */
.risk-na-toggle { display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.risk-na-label { display: none; }

/* A phone's card has a status pill, a Details button and captions over the two
   notes boxes; none of them is drawn on a computer, where the columns say it. */
.risk-status-pill {
  display: none;
  padding: 0.125rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 62.4375rem;
  font-size: 0.75rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--muted);
}
.risk-col-toggle { display: none; }
.risk-detail-label { display: none; }

/* A "Doesn't apply" on a hazard the client reported or an agrichemical one is asked
   for a one-line reason, under the hazard's name. Amber until it is typed (the tag
   says "Reason needed" in words as well). The Edit/Done button that folds a decided
   card is a phone's; a computer's rows stay as they are. */
.risk-checklist .risk-reason-input,
.job-site-hazard-row .risk-reason-input {
  display: block;
  width: 100%;
  margin-top: 0.375rem;
  padding: 0.25rem 0.5rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 400;
}
.risk-checklist .risk-reason-input[hidden],
.job-site-hazard-row .risk-reason-input[hidden] { display: none; }
.risk-checklist .risk-reason-input.risk-reason-missing,
.job-site-hazard-row .risk-reason-input.risk-reason-missing { border-color: var(--fg-warn); }
.risk-checklist .risk-reason-input:focus-visible,
.job-site-hazard-row .risk-reason-input:focus-visible { border-color: var(--focus-ring); outline: none; }
.risk-row-edit { display: none; }

/* The reasons given for hazards marked "Doesn't apply", on the job card's Risk tab. */
.job-risk-reasons {
  margin: 0 0 0.75rem;
  padding-left: 1.125rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.job-risk-reasons[hidden] { display: none; }
.job-risk-reasons li { margin-bottom: 0.25rem; }
.job-risk-reasons strong { color: var(--fg-body); font-weight: 600; }

/* ux:U5c */
/* Area editor and checklist: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Under the draw map: what the two colours are, and how big what is drawn is.
   The swatches get their colours from the script (the map's own: the areas'
   fixed blue and the theme's cut-out colour), so they cannot drift from it. */
.job-area-draw-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 1rem;
  margin: 0 0 0.625rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.job-area-draw-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.job-area-draw-legend li {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.job-area-draw-swatch {
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 0.1875rem;
  border: 0.0625rem solid var(--swatch-border);
}
/* The property overlay's switch (map preview) and, with it on, what its two
   shapes are: a dashed boundary and a filled building, in the overlay's own
   colours. */
.map-property-hint {
  color: var(--muted);
  font-size: 0.75rem;
}
.map-property-hint:empty { display: none; }
.job-area-draw-property {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.job-area-draw-swatch.job-area-draw-swatch-boundary {
  width: 1rem;
  height: 0;
  border: 0;
  border-top: 0.125rem dashed var(--map-property-line);
  border-radius: 0;
}
.job-area-draw-swatch.job-area-draw-swatch-building {
  background: var(--map-property-building);
  border-color: var(--map-property-building-line);
}
.job-area-draw-key-property {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.job-area-draw-key-property .job-area-draw-swatch-building { margin-inline-start: 0.5rem; }
.job-area-draw-size {
  margin-inline-start: auto;
  color: var(--fg-copy);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.job-area-draw-size[hidden] { display: none; }

/* The hint under the checklist picker sat on top of it: the card copy's negative top
   margin was written for a heading above, not a field. */
.checklist-run-hint { margin-top: 0.5rem; }

/* The checklist item a Save stopped at: no answer yet, or a failure with no note.
   Amber, the colour for "this needs something", not the red of a failure. */
.checklist-run-item.checklist-run-item-gap {
  border-color: rgba(var(--rgb-amber), 0.8);
  box-shadow: 0 0 0 0.125rem rgba(var(--rgb-amber), 0.4);
}

/* ux:U6a */
/* Billing dialogs: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The quote and invoice dialogs (ux:U6a). The title carries a derived "Expired"
   pill; an accepted or converted quote opens with its lock said first; the line
   items have a header row and an amount per line, in one grid shared with the
   header; the total rides in the pinned action row. */
.billing-dialog-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
/* Doubled with .form-hint, whose later rule would otherwise set the type size. */
.form-hint.billing-lock-note {
  margin: 0 0 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-amber), 0.12);
  color: var(--fg-soft);
  font-size: 0.8125rem;
}
.form-hint.billing-picker-note,
.form-hint.billing-status-note {
  margin: -0.375rem 0 0;
  font-size: 0.8125rem;
}
.billing-picker-note .settings-link { font-weight: 600; }
.form-hint.billing-estimate-note { margin: 0.375rem 0 0; font-size: 0.8125rem; color: var(--fg-copy); }
.billing-dialog-card .line-items-cols,
.billing-dialog-card .line-item-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 4.5rem 6.25rem 5.75rem 2.25rem;
  gap: 0.5rem;
  align-items: center;
}
.billing-dialog-card .line-items-cols {
  margin-bottom: 0.25rem;
  font-size: 0.6875rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.billing-dialog-card .line-items-cols span:nth-child(4),
.billing-dialog-card .li-amount { text-align: right; }
.billing-dialog-card .li-amount {
  font-size: 0.875rem;
  color: var(--fg-copy);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.billing-dialog-card .line-item-row .line-remove {
  width: 2.25rem;
  padding: 0.25rem 0;
}
/* A locked quote's or invoice's lines and tax read as values, not boxes to
   type in (a locked issue date is its date control's disabled look). */
.billing-dialog-card .is-locked .line-item-row input[readonly],
.billing-dialog-card input[readonly]#quoteTaxInput,
.billing-dialog-card input[readonly]#invoiceTaxInput {
  background: transparent;
  border-color: var(--line);
  cursor: default;
}
.billing-dialog-card .is-locked .line-item-row input[readonly]:focus,
.billing-dialog-card input[readonly]#quoteTaxInput:focus,
.billing-dialog-card input[readonly]#invoiceTaxInput:focus { box-shadow: none; }
/* The total, first in the pinned row and on a line of its own above the
   buttons, so it is on screen however many lines the quote has. */
.billing-dialog-actions .billing-dialog-total {
  flex: 1 0 100%;
  margin: 0;
  text-align: right;
}
/* The Close job dialog's fallback for a job with no flight log. */
.job-close-no-logs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin: 0.5rem 0 0.875rem;
  color: var(--fg-copy);
  font-size: 0.875rem;
}

/* ux:U6b */
/* Billing PDFs and tax details: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Settings > Billing, the defaults block: the payment details the invoice PDF
   prints at its foot. A few lines, so the box opens at three and grows down
   only; a refused value is marked like the number fields beside it. */
#billingDefaultsForm textarea { resize: vertical; min-height: 4.5rem; }
.billing-field textarea[aria-invalid="true"] { border-color: var(--danger); }

/* ux:U6c */
/* Billing list, phone and tiles: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE BILLING LIST. Tiles that say what they are made of, one line for Xero,
   tabs that count, a bar that finds (status chips, a search, how many), rows
   that a keyboard reaches, and New and Export beside each other at the top of
   the list. The phone's version of every rule is under "ux:U6c phone". */

/* Tiles: the figure, then the count behind it. Overdue and Outstanding are
   buttons that open the Invoices list narrowed to them. */
.financials-summary .stat .stat-count {
  margin-top: 0.125rem;
  font-size: 0.75rem;
  color: var(--muted);
}
.financials-summary .stat.stat-action {
  width: auto;
  min-height: 0;
  margin: 0;
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
}
.financials-summary .stat.stat-action:hover:not(:disabled) {
  filter: none;
  border-color: rgba(var(--rgb-green), 0.55);
}

/* What the last action did, with Retry beside a refresh that failed. */
.financials-notice-row {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}
.financials-notice-row:not(:has(> :not([hidden]))) { display: none; }

/* Xero, one line. The name is dropped when the strip says its status could not
   be read, because that sentence names Xero itself. */
.xero-strip {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.5rem 0.875rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: var(--tile-bg);
  font-size: 0.875rem;
}
.xero-strip[hidden] { display: none; }
.xero-strip .xero-strip-name {
  display: inline;
  margin: 0;
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--fg-strong);
}
.xero-strip .xero-strip-name[hidden],
.xero-strip .xero-strip-sep[hidden] { display: none; }
.xero-strip-status {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-copy);
}
.xero-strip.is-unchecked .xero-strip-status { color: var(--fg-warn); }
.xero-strip-sep { color: var(--muted); }
.xero-strip .settings-link { flex: none; font-size: 0.875rem; }

/* The tab labels carry their counts: "Invoices (31)", "My actions (2)". The
   count is on the tab itself so a person finds their work without being told
   to look inside a panel nobody has opened. */
.side-tab-count {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--muted);
}
.side-tab.active .side-tab-count { color: var(--fg-copy); }
.side-tab-count[hidden] { display: none; }

/* The bar over a list: status chips, a search, how many are showing (with
   Clear when it is fewer than all), and the list's actions at the far end. */
.billing-toolbar {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin: 0 0 0.75rem;
}
.billing-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
.billing-chips[hidden],
.billing-search[hidden],
.billing-count-row[hidden] { display: none; }
.billing-chip {
  min-height: 0;
  padding: 0.375rem 0.875rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 62.4375rem;
  background: transparent;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 650;
  white-space: nowrap;
}
.billing-chip:hover:not(:disabled) {
  background: rgba(var(--rgb-slate), 0.12);
  filter: none;
}
/* aria-pressed is the state; the colour only echoes it. */
.billing-chip[aria-pressed="true"] {
  background: rgba(var(--rgb-green), 0.18);
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--fg-body);
}
.billing-toolbar .billing-search {
  flex: 1 1 12rem;
  width: auto;
  min-width: 10rem;
  max-width: 20rem;
}
.billing-count-row {
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
  color: var(--muted);
  font-size: 0.75rem;
}
.billing-count { margin: 0; }
.billing-count-row .settings-link { font-size: 0.75rem; }
.billing-count-sep[hidden],
.billing-count-row .settings-link[hidden] { display: none; }
.billing-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-inline-start: auto;
}
.billing-actions button { flex: none; width: auto; margin: 0; }

/* The rows. A quote or invoice opens from its number (a real button, so a
   keyboard reaches it) or from a click anywhere on the row; the "..." at the end
   holds what this person can do to it. */
.financial-table td.financial-cell-client,
.financial-table td.financial-cell-job {
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.financial-table td.financial-cell-job { color: var(--muted); }
.financial-table th.financial-col-total,
.financial-table td.financial-cell-total {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.financial-cell-status .status-pill + .status-pill { margin-inline-start: 0.375rem; }
.financial-table td.financial-cell-menu {
  width: 2.75rem;
  padding-inline: 0.25rem;
  text-align: right;
}
.financial-due-inline { display: none; }
/* A list with nothing to show (loading, failed, empty, no match) is one line, not
   a row of column headings over it; its action sits a space from its words. */
.financial-table:has(> tbody > tr.list-state) > thead { display: none; }
.financial-table .list-state-action { margin-inline-start: 0.75rem; }
/* Where the card is narrower than the table's eight columns (a 1280px window, a
   laptop at 1366), the job moves under the client and its column goes: the list
   fits with no sideways scroll. The card is the container; the phone has its own
   layout and no container. */
html:not(.mobile) .billing-panel > .card { container: billing-list / inline-size; }
@container billing-list (max-width: 70rem) {
  .financial-table th.financial-col-job,
  .financial-table td.financial-cell-job { display: none; }
  .financial-table td.financial-cell-client[data-job]::after {
    content: attr(data-job);
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--muted);
    font-size: 0.8125rem;
  }
}
.financial-row { cursor: pointer; }
.financial-row:not(.selected):hover { background: var(--row-hover); }
.financial-open {
  width: auto;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0.25rem;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 650;
  text-align: left;
  cursor: pointer;
}
.financial-open:hover:not(:disabled) {
  filter: none;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* ux:U7a */
/* Flights map and list: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The list's toggle, named: "Flights (283)" and a chevron, where it was a bare
   chevron whose only label was a tooltip. Content-width, label first (the chevron
   is the ::before, so the row runs reversed). */
.flights-collapse-toggle {
  width: auto;
  flex-direction: row-reverse;
  gap: 0.375rem;
  padding: 0 0.375rem;
  font-size: 0.75rem;
  font-weight: 650;
  color: var(--fg-label);
  white-space: nowrap;
}
.flights-collapse-toggle:hover { color: var(--fg-strong); }
.flights-toggle-label { font-variant-numeric: tabular-nums; }

/* A job in the Jobs list: its name (with the number) over its client and flight
   count, as the phone's rows read. */
.job-filter-option {
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
  min-width: 0;
}
.job-filter-option-name,
.job-filter-option-meta {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.job-filter-option.selected .job-filter-option-name,
.job-filter-option.selected .job-filter-option-meta { color: var(--fg-on-green-tint); }
.job-filter-option:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: 0.0625rem;
}

/* A filter box the job pick filled in says so: a small "From job" beside the box.
   The three dropdowns sit in a field; with a chip the field is a row, the dropdown
   keeping the rest of the width (its own box is the shared dropdown's, untouched)
   and the chip after it, so the chip never covers the text. The dates' chip sits by
   their caption. */
.flight-filter-field { position: relative; min-width: 0; }
.flight-filter-field.has-from-job {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.flight-filter-field.has-from-job > .searchable-select { flex: 1 1 auto; min-width: 0; }
.flight-filter-label-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: -0.25rem;
}
.flight-filter-label-row .flight-filter-label { margin-bottom: 0; }
.from-job-chip {
  flex: none;
  padding: 0.0625rem 0.4375rem;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.16);
  color: var(--fg-label);
  font-size: 0.6875rem;
  font-weight: 650;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
}

/* The Summary as a one-line strip, when the sidebar cannot hold the card (see
   updateSummaryCardVisibility): the same four figures, value over label, as the
   phone's band reads them. */
.summary-card.is-strip { padding: 0.625rem 0.875rem; }
.summary-card.is-strip .stats-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.5rem;
  margin: 0;
}
.summary-card.is-strip .stat-tile {
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  gap: 0.0625rem;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
.summary-card.is-strip .stat-tile-value {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.summary-card.is-strip .stat-tile-label {
  font-size: 0.5625rem;
  letter-spacing: 0.03em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A flight row's aircraft by name, its serial beside it as the secondary text. */
.flight-row-serial { color: var(--muted); font-weight: 400; }

/* The layers' colours beside their tick boxes on the map's bar. */
.flights-swatches { display: inline-flex; align-items: center; gap: 0.1875rem; }
.flights-swatch {
  display: inline-block;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 50%;
  border: 0.0625rem solid var(--swatch-border);
}
.flights-swatch-spray { background: var(--flight-spray); }
.flights-swatch-takeoff { background: var(--flight-takeoff); }
.flights-swatch-landing { background: var(--flight-landing); }

/* Export, the last segment of the map's bar. Its own divider on the left and the
   bar's right corners, as the map-type picker rounds its left (the bar cannot clip
   a corner for it: see .map-context-bar). */
.map-context-bar .map-bar-button {
  width: auto;
  min-height: 0;
  margin: 0;
  padding: 0.625rem 0.875rem;
  border: 0;
  border-left: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0 0.4375rem 0.4375rem 0;
  background: transparent;
  color: var(--fg-control-2);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
}
.map-context-bar .map-bar-button:hover:not(:disabled),
.map-context-bar .map-bar-button[aria-expanded="true"] {
  background: rgba(var(--rgb-overlay), 0.06);
  color: var(--fg-strong);
}
.map-context-bar .map-bar-button:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: var(--keyboard-ring-inset);
}

/* With nothing picked: one line over the middle of the part of the map you can
   see (the list, while it is open, covers the left of it). It takes no pointer. */
.flights-map-prompt {
  position: absolute;
  z-index: 6;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 0.625rem 1rem;
  border-radius: 0.75rem;
  background: var(--map-badge-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  box-shadow: var(--shadow-badge);
  color: var(--fg-bright);
  font-size: 0.9375rem;
  font-weight: 650;
  white-space: nowrap;
  pointer-events: none;
}
.layout:not(.flights-collapsed) .flights-map-prompt {
  left: calc(50% + var(--flights-col) / 2);
}

/* A log row is two columns: the text, then the "...". The text column is
   minmax(0, 1fr), not the 1fr that .queue-item-no-toggle gives it: a plain 1fr will
   not shrink below its content, the aircraft-and-job line is nowrap, and so a long
   one widened the column to its own length, shoved the "..." out of the card (a
   phone's 44 px one past the row's edge) and made the list scroll sideways, instead
   of being cut by its ellipsis. The id outranks .queue-item-no-toggle, wherever
   that sits in the file. */
#flightLayerList .queue-item-no-toggle {
  grid-template-columns: minmax(0, 1fr) auto;
}

/* On a desktop the "..." takes no column at all. In a 204 px card (a 1025 to 1100 px
   window) its column cost about 39 px of a 152 px text column and cut the date line,
   and with it the duration, on nearly every row ("Wed 19 Aug · 12:53 · ..."), where
   the same rows fitted before the "..." was added. So it sits over the end of the name's
   line instead, centred on it: the date and aircraft lines run the row's full width,
   and the name's line keeps a reserve for the button (its 2.25rem, the 0.25rem it sits
   in from the edge and a 0.25rem gap, less the row's own 0.625rem padding) so the button
   never covers the name, the needs-review pill or a loading / failed status. The reserve
   is on rows that have a "..." only. Quiet until the row is in play, as before: opacity,
   not display, so it is still a tab stop. A phone keeps the column (a 44 px target
   beside the text, not over it). */
html:not(.mobile) #flightLayerList .queue-item-no-toggle {
  position: relative;
  grid-template-columns: minmax(0, 1fr);
}
html:not(.mobile) #flightLayerList .row-menu-button {
  position: absolute;
  top: calc(0.75rem + 0.625rem);
  right: 0.25rem;
  transform: translateY(-50%);
}
html:not(.mobile) #flightLayerList [data-row-menu] .flight-row-title {
  padding-right: 2.125rem;
}
/* "Name only" rows are tighter (0.375rem above the name's line, not 0.75rem). */
html:not(.mobile) .flights-list-card.density-name #flightLayerList .row-menu-button {
  top: calc(0.375rem + 0.625rem);
}

/* ux:U7b */
/* Flights dialogs: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The upload dialog's drop zone is a real button (it takes Tab and Enter and
   opens the file chooser), so it has to say everything a bare button does not:
   full width, its own type, no content-width and no brightness lift. */
button.drop-hint {
  width: 100%;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 400;
  line-height: 1.4;
}
button.drop-hint:hover:not(:disabled) { filter: none; }

/* Stands where the Job list would be when there is nothing to pick. Not a .form-field: its `span` rule would dress
   the line and its link as uppercase labels, so the caption carries the label look itself. */
.upload-job-state { display: grid; gap: 0.375rem; margin-bottom: 0.875rem; }
.upload-job-state-label {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.upload-job-state .list-state { padding: 0.5rem 0; }
.upload-job-state .list-state > button.settings-link { font-size: 0.875rem; }

/* The upload dialog's status line is green while a batch goes well; with a failed or a refused log in it, amber. */
.upload-modal-status-text.upload-status-trouble { color: var(--fg-warn); }

/* Download and Delete on the flight sheet, and Re-check: plain content-width
   actions inside the scrolling body (the pinned row is only Save and what goes
   with it). */
.flight-detail-file-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
  margin: 1rem 0 0;
}

/* Save, then what goes with it, on one pinned row. The hint sits after Save, as
   it does beside every other Save in the app. */
.flight-detail-actions { align-items: center; }
.flight-detail-actions .org-dirty-hint { margin-inline-start: 0.25rem; }

/* The row a delete question is about, while the question is up. A log on the map is `.log-visible` (a green
   border) and a picked one `.selected` (a blue ring), and both rules come later in this file at the same
   specificity, so the mark would lose to them: each combination is named, and the ring is solid enough to read
   through the dimmed page (every flight in a job can carry the same file name, so the ring is what tells them
   apart). */
.queue-item.delete-pending,
.queue-item.log-visible.delete-pending,
.queue-item.selected.delete-pending,
.queue-item.log-visible.selected.delete-pending {
  border-color: rgb(var(--rgb-red));
  box-shadow: 0 0 0 0.25rem rgba(var(--rgb-red), 0.7);
}

/* The status pill of an upload row and, under it, the Retry of a log that failed and is still held. */
.queue-item-status {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.375rem;
}
.queue-retry-button {
  min-height: 0;
  padding: 0.25rem 0.75rem;
  font-size: 0.8125rem;
}

/* ux:U8 */
/* Airspace: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The layers card, minimised: a labelled "Layers" button in the phone's own
   pill (icon and word) instead of an unlabelled "+". Expanded, the same button
   is the small square "-" in the card's header. */
.map-legend-card.minimized .map-legend-minimize-button {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  width: auto;
  height: auto;
  min-height: 2rem;
  padding: 0.375rem 0.75rem;
  border-radius: 999px;
  border: 0.0625rem solid rgba(var(--rgb-green), 0.55);
  background: var(--map-float-bg);
  color: var(--fg-fab);
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.2;
  box-shadow: var(--shadow-fab);
  backdrop-filter: var(--backdrop-bar);
}
.map-legend-card.minimized .map-legend-minimize-button:hover { background: var(--map-float-bg-2); }
.map-legend-layers-icon {
  display: inline-flex;
  width: 1.05rem;
  height: 1.05rem;
  color: var(--green);
}
.map-legend-layers-icon svg { width: 100%; height: 100%; }

/* The source line and the "no charts" note sit inside the Airspace view label
   but are not part of its name (form-hint, which the dropdown's naming skips):
   the gap between the label's rows is already the flex gap. */
.airspace-chart-field .form-hint { margin-top: 0; }

/* The time bar's label is two lines: the instant the bar is asking about, and
   what is in effect then. Each ellipsises on its own (a block child does not
   inherit its parent's text-overflow). */
.airspace-timebar .airspace-time-label { min-width: 14rem; }
.airspace-time-instant,
.airspace-time-count {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
}
.airspace-time-count { font-size: 0.6875rem; color: var(--fg-subtle); }

/* The layers card leaves the time bar its strip: with NOTAMs on by default the bar is up
   on every visit, and an open card (650px and more) reached it on a 800px-tall window and
   was painted over. The card scrolls inside what is left; the bar's measured height
   (published on the map shell by app.js, syncAirspaceTimeBarHeight) and its foot
   (--airspace-timebar-foot: 1.5rem, more while the map credit is open) are subtracted,
   with a little air. */
#airspaceMapShell:has(#airspaceTimeBar:not([hidden])) .map-legend-card:not(.minimized) {
  max-height: calc(100% - 1.625rem - var(--airspace-timebar-foot) - var(--airspace-timebar-h, 0px));
}

/* The zone popup is what was asked for: it sits over the layers card (5) and the
   map's own buttons (2) rather than behind them, so it is always read and its close
   button reached. The time bar, the Layers button and the chart legend (6) come later
   in the page and stay over it; the placement keeps clear of those. */
.notam-popup-container { z-index: 6; }
/* ... and opaque, so what it sits over (the card's rows, a button) never reads through it. */
.notam-popup-container .maplibregl-popup-content { background: var(--sheet-bg); }

/* The zone popup shares the NOTAM popup's shell. A published authority is an
   address block, so its line breaks are kept. */
.notam-popup-val-multiline { white-space: pre-line; }
/* A point inside controlled airspace lists half a dozen zones of eight rows each:
   the NOTAM stack's one-card-tall box would show a card and a half. Taller here,
   but never more than half the window, so the popup stays on the map. */
.airspace-popup .notam-popup-scroll { max-height: min(26rem, 60vh); }
.notam-popup-subhead { border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.28); }
.notam-popup-note {
  margin: 0;
  padding: 0.375rem 0.75rem;
  font-size: 0.6875rem;
  color: var(--fg-subtle);
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
}

/* ----- The airspace cards and the list under a click ----------------------------------------------
   A click lists what is under the point (.airspace-list) and a row opens that thing's card (.zone-card-*):
   a header band in the colour the map draws the thing in (--zc, set by app.js from the map's own table, with
   data-ink="dark" where white would not read on it), the name and the code, a pill, Upper and Lower with the
   hours beside them, the frequency, the AIP's note under its headings, the NOTAMs that apply to a zone, and a
   footnote for the abbreviations. The popup's own close button sits at the band's right end. */
.zone-card-head, .zone-card-scroll, .airspace-list-row { --zc: var(--zcard-fallback); }
/* The ink on the band, its controls, the pill and the close button: white, or the dark ink on a light band. */
.zone-card-head[data-ink="dark"],
.zone-card-scroll[data-ink="dark"],
.notam-popup-container:has(.zone-card-head[data-ink="dark"]) {
  --zcard-on-band: var(--zcard-ink-dark);
  --rgb-zcard-on-band: var(--rgb-zcard-ink-dark);
}

.airspace-popup[data-view="card"] { min-width: min(17.5rem, 100%); }
/* The band runs to the popup's rounded corners. */
.notam-popup-container .maplibregl-popup-content { overflow: hidden; }
.notam-popup-container:has(.zone-card-head) .maplibregl-popup-close-button { color: var(--zcard-on-band); }
.notam-popup-container:has(.zone-card-head) .maplibregl-popup-close-button:hover { background: rgba(var(--rgb-zcard-on-band), 0.2); }
/* ux:DEC2 - the keyboard ring on the band is the band's own white, as the zone card's back arrow's was on main; the accent
   ring measured 1.0 to 1.6:1 against the light theme's bands. Inside the button, which sits in the corner of a box that clips. */
.notam-popup-container:has(.zone-card-head) .maplibregl-popup-close-button:focus {
  outline-color: var(--zcard-on-band);
  outline-offset: var(--keyboard-ring-inset);
}

.zone-card-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.625rem 2.75rem 0.625rem 0.5rem;
  background-color: var(--zc);
  color: var(--zcard-on-band);
}
.zone-card-mark,
.zone-card-back,
.zone-card-step {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  min-height: 2.25rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--zcard-on-band);
  font-size: 1rem;
}
.zone-card-back { cursor: pointer; }
.zone-card-back:hover:not(:disabled) { background: rgba(var(--rgb-zcard-on-band), 0.2); filter: none; }
:where(html:not(.using-pointer)) .zone-card-back:focus { box-shadow: none; outline-color: var(--zcard-on-band); }
html.mobile .zone-card-back { width: var(--touch-min); height: var(--touch-min); min-height: var(--touch-min); }
.zone-card-head .airspace-icon { width: 1.25rem; height: 1.25rem; }
.zone-card-titles { flex: 1 1 auto; min-width: 0; text-align: center; }

/* A card opened from a list of two or more (Josh, 2026-11-02: "the card should include '<' and '>'
   buttons to move through them rather than just going back to the list"). The band's top row is
   its controls: the way back to the list on the left, then the previous arrow, "2 of 5" and the
   next arrow in the middle, with the popup's close button in the corner; the name and code sit
   under them, so a name that wraps onto a second line never moves a control. The two outer
   columns share what the middle leaves, so the arrows and the name below are centred on the
   same line (the close button is over the right one, which holds nothing; the row is clear of
   the name, so the band's sides are even here, not kept clear for the button). The count keeps
   one width for any list under a hundred, "1 of 2" to "99 of 99" (tabular figures, a floor), so
   the arrows stay put as it counts. Each step keeps the popup's place, width and height (app.js,
   showAirspacePopup: the frame), so the arrows stay under the finger from card to card. An arrow
   at an end of the list is aria-disabled (it keeps the focus that stepped there) and dimmed. On a
   phone each control is the touch floor square, and the row centred is 0.5rem of padding each
   side, the way back and its twin column (6.5rem), the arrows (6.5rem) and the count's 3.5rem:
   17.5rem (245px at the 14px phone root), the card's own floor, inside a 320px phone's popup
   (304px at most); the frame is never placed narrower than the row (app.js measures it). */
.zone-card-head.has-steps {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "back steps ." "titles titles titles";
  align-items: center;
  gap: 0.125rem 0;
  padding: 0.125rem 0.5rem 0.625rem;
}
.zone-card-head.has-steps .zone-card-back { grid-area: back; justify-self: start; }
.zone-card-head.has-steps .zone-card-titles { grid-area: titles; }
.zone-card-steps { grid-area: steps; display: flex; align-items: center; justify-content: center; }
/* A short screen (a phone on its side has 126 to 164px of map over the time bar): one row, the name between the
   way back and the arrows, so the band takes a control's height and not two rows' and the card's body keeps
   what is left. The controls sit at the row's top, so a name that wraps still moves none of them; the close
   button keeps its corner. */
@media (max-height: 500px) {
  .zone-card-head.has-steps {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "back titles steps";
    align-items: start;
    padding: 0.125rem 2.75rem 0.375rem 0.5rem;
  }
  .zone-card-head.has-steps .zone-card-titles { align-self: center; }
}
.zone-card-position {
  min-width: 3.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
  white-space: nowrap;
}
.zone-card-step { cursor: pointer; }
/* Only where a pointer can hover: a tap leaves :hover on what is under the finger, and the arrow a step draws
   is under it, so a phone would wear the hover wash after every tap. */
@media (hover: hover) {
  .zone-card-step:hover:not([aria-disabled="true"]) { background: rgba(var(--rgb-zcard-on-band), 0.2); filter: none; }
}
.zone-card-step[aria-disabled="true"] { opacity: 0.4; cursor: default; }
:where(html:not(.using-pointer)) .zone-card-step:focus { box-shadow: none; outline-color: var(--zcard-on-band); }
/* The control row sits 0.125rem under the popup's top edge, which clips (the band runs to the rounded
   corners): a ring drawn outside the way back or an arrow lost its whole top side there, and ran into the
   name below. So these rings are drawn inside the button, as the close button's is beside them. */
.zone-card-head.has-steps :is(.zone-card-back, .zone-card-step):focus { outline-offset: var(--keyboard-ring-inset); }
html.mobile .zone-card-step { width: var(--touch-min); height: var(--touch-min); min-height: var(--touch-min); }
/* The frame a step keeps (app.js sets its width and height): the band and the footnote take what they
   need and the card's body has the rest, scrolling when it holds more. */
.airspace-popup.is-framed { grid-template-rows: auto minmax(0, 1fr) auto; }
.airspace-popup.is-framed > .notam-popup-scroll { max-height: none; }
/* The measuring box (never painted) lays every card of a list over the others, so it is the largest;
   each keeps its own height in it (not stretched to the box), so its band can be measured too. */
.airspace-card-stack { display: grid; }
.airspace-card-stack > .airspace-popup { grid-area: 1 / 1; align-self: start; }
.zone-card-name {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  line-height: 1.2;
  text-transform: uppercase;
  overflow-wrap: anywhere;
  color: inherit;
}
.zone-card-code { margin-top: 0.125rem; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.02em; overflow-wrap: anywhere; }

.zone-card-scroll {
  display: grid;
  gap: 0.5rem;
  align-content: start;
  padding: 0.625rem 0.75rem 0.75rem;
  font-size: 0.8125rem;
  color: var(--fg-body-2);
}
.zone-card-scroll:focus { outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: var(--keyboard-ring-inset); }
.zone-card-pills { display: flex; flex-wrap: wrap; gap: 0.375rem; }
.zone-card-pill {
  padding: 0.125rem 0.625rem;
  border-radius: 62.5rem;
  background: var(--zc);
  color: var(--zcard-on-band);
  font-size: 0.75rem;
  font-weight: 700;
}
.zone-card-pill.is-quiet { background: transparent; color: var(--fg-body-2); box-shadow: inset 0 0 0 0.0625rem rgba(var(--rgb-slate), 0.5); font-weight: 600; }
.zone-card-zoneline { font-size: 0.875rem; font-weight: 700; color: var(--text); }
.zone-card-limits { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.25rem 1.25rem; }
.zone-card-limit-col { display: grid; gap: 0.25rem; }
.zone-card-limit, .zone-card-hours, .zone-card-freq, .zone-card-callout { display: flex; align-items: flex-start; gap: 0.375rem; }
.zone-card-scroll .airspace-icon { flex: none; width: 1rem; height: 1rem; margin-top: 0.0625rem; color: var(--fg-subtle); }
.zone-card-limit { font-weight: 700; color: var(--text); }
.zone-card-hours { color: var(--fg-body-2); }
.zone-card-freq { font-weight: 600; color: var(--text); }
.zone-card-facts { display: grid; gap: 0.125rem; }
.zone-card-fact-key { font-weight: 600; color: var(--fg-subtle); }
.zone-card-lead { margin: 0; line-height: 1.45; color: var(--fg-label); white-space: pre-line; overflow-wrap: anywhere; }
.zone-card-callout {
  padding: 0.5rem 0.625rem;
  border-radius: 0.375rem;
  background: rgba(var(--rgb-red), 0.12);
  box-shadow: inset 0.1875rem 0 0 rgb(var(--rgb-red));
  color: var(--text);
}
.zone-card-scroll .zone-card-callout .airspace-icon { color: rgb(var(--rgb-red)); }
.zone-card-section { border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.22); padding-top: 0.5rem; }
.zone-card-section-title {
  margin: 0 0 0.25rem;
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.zone-card-bullets { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.25rem; }
.zone-card-bullets li { position: relative; padding-left: 0.875rem; line-height: 1.4; white-space: pre-line; overflow-wrap: anywhere; }
.zone-card-bullets li::before {
  content: "";
  position: absolute;
  left: 0.125rem;
  top: 0.5em;
  width: 0.3125rem;
  height: 0.3125rem;
  border-radius: 50%;
  background: var(--zc);
}
.zone-card-link { color: var(--blue); text-decoration: underline; text-underline-offset: 0.125rem; }
.zone-card-foot {
  margin: 0;
  padding-top: 0.5rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  font-size: 0.6875rem;
  color: var(--fg-subtle);
}
/* The NOTAMs that apply to a zone (Josh, 2026-11-05), drawn on the map or not: the window they are for, then
   one entry per NOTAM, a button that opens its card, striped in the colour the map draws its kind in (--nc);
   the ones that do not concern a drone fold under "N other NOTAMs". */
.zone-card-notams { display: grid; gap: 0.375rem; border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.22); padding-top: 0.5rem; }
.zone-card-notams > .zone-card-section-title { margin: 0; }
.zone-card-notams-window { margin: -0.25rem 0 0; font-size: 0.6875rem; color: var(--fg-subtle); }
.zone-card-notams-none { margin: 0; font-weight: 600; color: var(--text); }
.zone-card-notam-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.25rem; }
.zone-card-notam {
  display: grid;
  gap: 0.0625rem;
  width: 100%;
  min-width: 0;
  min-height: 0;
  padding: 0.375rem 0.5rem 0.375rem 0.75rem;
  border: 0;
  border-radius: 0.375rem;
  background: rgba(var(--rgb-slate), 0.1);
  box-shadow: inset 0.1875rem 0 0 var(--nc, var(--zcard-fallback));
  color: var(--text);
  font-size: 0.75rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  cursor: pointer;
}
.zone-card-notam:hover:not(:disabled) { background: rgba(var(--rgb-slate), 0.18); filter: none; }
.zone-card-notam > span { min-width: 0; }
.zone-card-notam-line1 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.5rem; }
.zone-card-notam-code { font-size: 0.8125rem; font-weight: 800; }
.zone-card-notam-type { color: var(--fg-subtle); }
.zone-card-notam-summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg-body-2); }
.zone-card-notam-when { font-size: 0.6875rem; color: var(--fg-subtle); }
.zone-card-notams-other { display: grid; gap: 0.25rem; }
.zone-card-notams-other > summary { cursor: pointer; font-size: 0.75rem; font-weight: 600; color: var(--fg-body-2); }
.zone-card-notams-other[open] > summary { margin-bottom: 0.25rem; }
/* The touch floor for the fold on a phone (html.mobile button covers the entries, not a summary); line-height,
   not flex, so the summary keeps its disclosure triangle. */
html.mobile .zone-card-notams-other > summary { min-height: var(--touch-min); line-height: var(--touch-min); }

.airspace-list { list-style: none; margin: 0; padding: 0; }
.airspace-list li + li { border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18); }
.airspace-list-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  min-height: 2.5rem;
  padding: 0.5rem 0.625rem 0.5rem 0.75rem;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: inset 0.25rem 0 0 var(--zc);
  color: var(--text);
  font-size: 0.8125rem;
  font-weight: 400;
  text-align: left;
}
.airspace-list-row:hover:not(:disabled) { background: rgba(var(--rgb-slate), 0.12); filter: none; }
.airspace-list-row:focus { box-shadow: inset 0.25rem 0 0 var(--zc); outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: var(--keyboard-ring-inset); }
.airspace-list-row .airspace-icon { flex: none; width: 1rem; height: 1rem; color: var(--fg-subtle); }
.airspace-list-text { flex: 1 1 auto; min-width: 0; display: grid; gap: 0.125rem; }
.airspace-list-line1 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.5rem; }
.airspace-list-code { font-size: 0.875rem; font-weight: 800; overflow-wrap: anywhere; }
.airspace-list-type { font-size: 0.75rem; color: var(--fg-subtle); }
.airspace-list-line2 { font-size: 0.75rem; color: var(--fg-body-2); overflow-wrap: anywhere; }

/* ux:U9 */
/* Hazards and Occurrences: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The bar over the hazard register and the occurrence list: status chips, the
   risk or severity filter, the sort, a search and the count with its "Clear".
   Hidden by the page while a list has no records at all. */
.safety-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin: 0 0 0.875rem;
}
.safety-filters[hidden] { display: none; }
.safety-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
.safety-chip {
  min-height: 0;
  padding: 0.375rem 0.875rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 62.4375rem;
  background: transparent;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 650;
  white-space: nowrap;
}
.safety-chip:hover:not(:disabled) {
  background: rgba(var(--rgb-slate), 0.12);
  filter: none;
}
/* aria-pressed is the state; the colour only echoes it. */
.safety-chip[aria-pressed="true"] {
  background: rgba(var(--rgb-green), 0.18);
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--fg-body);
}
.safety-filter-select {
  flex: 0 0 11rem;
  min-width: 0;
}
.safety-filters .safety-search {
  flex: 1 1 12rem;
  width: auto;
  min-width: 10rem;
  max-width: 20rem;
}
/* "3 of 12 hazards · 2 filters · Clear": at the end of the row, quiet. */
.safety-count-row {
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
  margin-inline-start: auto;
  color: var(--muted);
  font-size: 0.75rem;
}
.safety-count { margin: 0; }
.safety-count-row .settings-link { font-size: 0.75rem; }
.safety-count-sep[hidden],
.safety-count-row .settings-link[hidden] { display: none; }

/* A hazard row says what a click does: Edit for those who manage the register,
   View for everyone else, with a chevron that leans in on hover and focus. */
.hazard-opens {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 650;
}
.hazard-opens::after {
  content: "";
  width: 0.4375rem;
  height: 0.4375rem;
  border-top: 0.125rem solid currentColor;
  border-right: 0.125rem solid currentColor;
  transform: rotate(45deg);
}
.hazard-head:hover .hazard-opens,
:where(html:not(.using-pointer)) .hazard-head:focus .hazard-opens { color: var(--fg-strong); }
/* A corrective action that is past its date and not done. */
.action-overdue {
  color: var(--danger);
  font-weight: 650;
}
/* "CAA" before the date is the phone card's chip; the column header says it here. */
.incident-caa-word { display: none; }

/* The Risk pill under Likelihood and Consequence in the hazard form. Compound with
   .form-field (a grid, declared later in the file) and a div, so the field's label
   rules for its spans do not reach the pill. */
.form-field.hazard-risk-row {
  display: flex;
  align-items: center;
  margin-top: -0.25rem;
}
.hazard-risk-row .status-pill {
  padding: 0.25rem 0.625rem;
  font-size: 0.75rem;
}

/* Occurrence evidence: the files, the rows for a file on its way or turned away,
   and the "Add file" button, in the dialog and in the reporter's overview. */
.evidence-pending {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.evidence-pending:empty { display: none; }
.evidence-adder {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.evidence-pending-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.875rem;
}
/* A file that is on its way, waiting, or was turned away: its name and its button on
   the first line, what happened to it on the second (a reason can be a sentence).
   Compound with .evidence-item, which is declared later in the file. */
.evidence-item.evidence-item-pending {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.125rem 0.75rem;
}
.evidence-item-pending .evidence-pending-name { grid-column: 1; grid-row: 1; }
.evidence-item-pending .evidence-meta { grid-column: 1 / -1; grid-row: 2; }
.evidence-item-pending .evidence-dismiss { grid-column: 2; grid-row: 1; }
/* Inside a .form-field these are spans, which that field dresses as an uppercase label:
   a file's name and the words about it are not labels. */
.form-field .evidence-pending-name,
.form-field .evidence-meta {
  text-transform: none;
  letter-spacing: normal;
}
.form-field .evidence-pending-name {
  color: var(--fg-body);
  font-size: 0.875rem;
  font-weight: 600;
}
.form-field .evidence-meta {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
}
.form-field .evidence-meta.is-error { color: var(--danger); }
.evidence-dismiss {
  flex: 0 0 auto;
  padding: 0.25rem 0.75rem;
  font-size: 0.75rem;
}
.evidence-list > .list-state { padding: 0; }

/* ux:U10 */
/* Documents and Reporting: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The admin line that stands where Upload would be, for someone who may not add. */
.documents-admin-hint {
  margin: 0;
  max-width: 11rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.documents-admin-hint[hidden] { display: none; }

/* One row: a line and its meta, then the renewal pill and the row's "...". 48 px,
   not the two-line stack with a Delete button under it (85 px). Flex, not the
   queue-item grid, because the right-hand side is zero, one or two items. */
#documentsPage .document-row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-height: var(--doc-row-h);
  margin: 0 0 var(--doc-row-gap);
  padding: 0.3125rem 0.625rem;
  box-sizing: border-box;
}
#documentsPage .document-row-main { flex: 1 1 auto; min-width: 0; }
/* The category's icon at the start of the row. A theme token only, the same quiet tone as the
   nav's. */
.document-row-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  color: var(--fg-nav);
}
.document-row-icon svg { width: 100%; height: 100%; }
#documentsPage .document-row .queue-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.25;
}
#documentsPage .document-row .queue-meta {
  margin-top: 0.125rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.25;
}
#documentsPage .document-row .status-pill { flex: 0 0 auto; }
#documentsPage .document-row .row-menu-button { flex: 0 0 auto; }

/* The reports' filters: three menus and a way back, one row that wraps. */
.document-filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
}
.document-filterbar[hidden] { display: none; }
.document-filterbar .searchable-select { flex: 0 1 11rem; min-width: 8rem; }

/* A list that scrolls in its box shows there is more past an edge: a fade on the
   side with rows beyond it. The mask is alpha only, so any opaque colour will do;
   a theme channel keeps the colour gate quiet. */
.document-scroll.has-more-below {
  -webkit-mask-image: linear-gradient(to bottom, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
  mask-image: linear-gradient(to bottom, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
}
.document-scroll.has-more-above {
  -webkit-mask-image: linear-gradient(to top, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
  mask-image: linear-gradient(to top, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
}
.document-scroll.has-more-above.has-more-below {
  -webkit-mask-image: linear-gradient(to bottom, transparent, rgb(var(--rgb-overlay)) 1.75rem, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
  mask-image: linear-gradient(to bottom, transparent, rgb(var(--rgb-overlay)) 1.75rem, rgb(var(--rgb-overlay)) calc(100% - 1.75rem), transparent);
}

/* The lists inside this page's two pinned dialogs (New report: the report and its pilot /
   job / client / aircraft; Upload and Edit: category and "belongs to") draw their panels
   against the window (enhanceSelect, floating): the dialog's card and its scrolling body
   would otherwise clip a list to four rows, or one.
   Top, left, bottom and the width come from the script, which measures the trigger. */
.searchable-select.is-floating > .searchable-select-panel {
  position: fixed;
  right: auto;
}

/* ---- end of UX SHARED ----------------------------------------------------- */

.reports-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
  overflow-y: auto;
  height: 100%;
  /* Fill the fixed-height page and scroll internally, but pack cards to the top
     so a sparse tab (Financials, Jobs) doesn't stretch its single card to full
     page height — the card hugs its content and empty space is plain panel
     background below it, not a band inside a bordered card (§4). */
  align-content: start;
}

/* Documents & Reporting: side tabs, like Billing (.side-body, .side-tabs, .side-panel), with the
   hazard register, occurrence reporting and My actions on tabs of their own since 2026-11-05. Upload document and New report are in the header row of their
   own panel's card (.incidents-header), beside the line that says what the tab holds. */
#documentsPage {
  display: flex;
  flex-direction: column;
}
/* The page is a flex column here, so the frame takes what is left of it. */
#documentsPage .documents-page-layout {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
}
.documents-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* The card fills its panel, and has to: both sections below are `flex: 1 1 0`,
   which adds nothing to a content-sized card. Without it the card shrank to its
   headings and BOTH lists were drawn 0px high, every uploaded document and
   generated report clipped to nothing (2026-09-29, the page split). */
.documents-content {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  min-height: 0;
  margin-bottom: 0;
}
/* Uploaded documents and generated reports are two sections now, never one
   merged list (Josh, 2026-09-29): each gets its own heading, its own empty
   state, and a rule between them so the split is obvious at a glance, not
   just inferable from a filter chip. Each section is only as tall as its
   content needs (`flex: 0 1 auto`), and the generated reports take whatever
   the card has left (`flex: 1 1 auto`, below): with no uploads, the empty
   "No documents yet" is one line and the reports get the rest, not half the
   card. When both lists are longer than the card they shrink together and
   each scrolls in its share; every list that holds a row keeps at least one
   whole row and a list of two or more keeps two where the card has the height
   (min-heights, in the desktop block above), so a long list never squeezes the
   other to nothing. */
.document-section {
  display: flex;
  flex-direction: column;
  flex: 0 1 auto;
  min-height: 0;
}
html:not(.mobile) #generatedReportsSection {
  flex: 1 1 auto;
}
.document-section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.75rem;
  row-gap: 0.125rem;
  margin: 0 0 0.5rem;
}
.document-section-heading {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--fg-nav);
}
/* The count in a heading, and the one line beside it (what needs attention, how many
   filters are on, or the word for a save). Quiet type: the list is the content. */
.document-count { font-weight: 600; color: var(--muted); }
.document-section-line {
  margin: 0;
  font-size: 0.75rem;
  color: var(--muted);
}
.document-section-line:empty { display: none; }
.document-section-line .is-expired { color: var(--fg-error); font-weight: 650; }
.document-section-line .is-soon { color: var(--fg-amber); font-weight: 650; }
#documentsPage #documentList,
#documentsPage #reportList {
  flex: 1 1 auto;
  min-height: 0;
  /* .queue-list caps every other list at 21.875rem; here the list IS the slack
     its section absorbs, so the cap has to go or the section is drawn full
     height around a short list. */
  max-height: none;
  overflow-y: auto;
}

/* Reports page: a controls card on top, one scrolling preview below it. */
#reportsPage {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* The New report modal's report-type picker only: every report type fits
   without scrolling wherever the viewport allows it (Josh, 2026-09-29), not
   just the shared 15rem list every other dropdown in the app keeps
   (.searchable-select-list above). Lifting the cap here does not risk running
   off the screen on a short window - enhanceSelect's own panelPlacement
   (app.js) measures the room available each time the panel opens and shrinks
   the list back down (or flips it upward) when this one does not fit, exactly
   as it already does for every dropdown. Scoped to #reportSelect's own
   wrapper so its sibling #reportPicker (the pilot/aircraft/job/client filter)
   and every other dropdown in the app are untouched. */
#reportModal .searchable-select:has(> #reportSelect) .searchable-select-list {
  max-height: none;
}

/* Org default charge-out rates: one row per service type (label, basis, rate). */
.charge-rates-grid {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  margin: 0.75rem 0;
}

.charge-rate-row {
  display: grid;
  grid-template-columns: 8.75rem minmax(0, 1fr) 8.125rem;
  gap: 0.625rem;
  align-items: center;
}

.charge-rate-row > span {
  color: var(--fg-nav);
  font-size: 0.875rem;
}

/* The select takes the shared dropdown metrics; the rate input beside it
   matches them so the row stays level. */
.charge-rate-row input {
  box-sizing: border-box;
  width: 100%;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  margin: 0;
  outline: none;
}

@media (max-width: 620px) {
  .charge-rate-row { grid-template-columns: 1fr; }
}

/* Breathing room above the "+ New checklist" action, below the list/message. */
#checklistTemplateAddButton { margin-top: 0.75rem; }

/* Document library: the current-file note, the inline View button, and the
   in-app viewer modal. */

.document-current-file { font-size: 0.8125rem; color: var(--muted); }
.document-current-file a { color: var(--green); }

.form-hint { display: block; margin-top: 0.25rem; font-size: 0.6875rem; color: var(--muted); }

/* Rows that open the viewer are focusable buttons (makeRowFocusable sets
   role=button); show the pointer on those, per the clickable-row rule. */
#documentList .queue-item[role="button"],
#reportList .queue-item[role="button"] { cursor: pointer; }

/* Compound with .modal-card so this beats its max-width: 640px / max-height:
   82vh - the viewer needs to be large and scroll inside its body, not the card. */
.modal-card.document-viewer-card {
  width: min(81.25rem, 96vw);
  max-width: 81.25rem;
  height: 92vh;
  max-height: 92vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.document-viewer-header-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; align-items: center; }
/* Download is an <a> (needs href), Close, Edit and Delete are <button>s; the base
   `button` rule leaves buttons at the UA default 13px font while the <a> inherits
   16px, so pin all of them to one identical box. */
.document-viewer-header-actions a,
.document-viewer-header-actions button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 5.5rem;
  height: 2.25rem;
  margin: 0;
  padding: 0 0.875rem;
  border-radius: 0.5rem;
  background: var(--secondary-bg);
  color: var(--secondary-fg);
  font-size: 0.8125rem;
  font-weight: 750;
  line-height: 1;
  text-decoration: none;
}
.document-viewer-header-actions a:hover,
.document-viewer-header-actions button:hover { filter: brightness(1.08); }
.document-viewer-header-actions button[hidden] { display: none; }
.document-viewer-header-actions .danger-button { background: var(--danger-bg); color: var(--pill-red-fg); }
/* What the row said, and the notes saved with the document. */
.document-viewer-meta {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  margin: 0.375rem 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
}
.document-viewer-meta[hidden] { display: none; }
.document-viewer-notes { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--fg-body); }
.document-viewer-card > .form-message { margin: 0.5rem 0 0; }
/* Zoom for a PDF: fit width, smaller, larger. */
.document-viewer-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.document-viewer-tools[hidden] { display: none; }
.document-viewer-tools button {
  min-width: 2.25rem;
  height: 2.25rem;
  margin: 0;
  padding: 0 0.75rem;
  font-size: 0.8125rem;
  line-height: 1;
}
.document-zoom-level {
  min-width: 3rem;
  text-align: center;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-body);
}
.document-viewer-body {
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0.75rem;
  overflow: auto;
  display: flex;
  background: var(--well-bg);
  border-radius: 0.5rem;
}
.document-pdf-scroll {
  flex: 1;
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem;
  color: var(--muted);
}
/* A page's place in the scroll, at the size it will be drawn: the canvas is only
   there while the page is near the screen. Centred by its margins, not by the
   column, so a zoomed page wider than the viewer scrolls from its left edge. */
.document-pdf-slot {
  flex: none;
  margin-inline: auto;
  background: var(--paper);
  box-shadow: var(--shadow-page);
}
.document-pdf-page {
  display: block;
  background: var(--paper);
}
/* A page that would not draw while others did: it says so in its own place, on the
   viewer's well and not on paper, so the words keep a readable pair in both themes. */
.document-pdf-slot[data-state="failed"] {
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  box-shadow: none;
  border: 0.0625rem dashed var(--frame-border);
  box-sizing: border-box;
  color: var(--fg-body);
  font-size: 0.875rem;
}
/* A PDF that would not open: the file is still there to take. */
.document-viewer-fallback {
  margin: auto;
  max-width: 24rem;
  padding: 1.5rem 0.75rem;
  text-align: center;
  color: var(--fg-body);
}
.document-viewer-fallback p { margin: 0 0 0.75rem; font-size: 0.9375rem; }
.document-viewer-fallback .modal-actions { justify-content: center; margin-top: 0; }
.document-viewer-fallback-button {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 8rem;
  min-height: 2.75rem;
  padding: 0 1rem;
  text-decoration: none;
}
.document-viewer-body img { max-width: 100%; max-height: 100%; margin: auto; object-fit: contain; }
.document-viewer-body pre {
  flex: 1;
  margin: 0;
  padding: 0.875rem;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--fg-body);
  font-size: 0.8125rem;
  overflow: auto;
}

/* Reports: cancellation-reasons breakdown + overdue-service emphasis. */
.reports-breakdown { margin-top: 0.75rem; }
.reports-breakdown-title {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 0.375rem;
}
.reports-breakdown-row {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.3125rem 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  font-size: 0.875rem;
}
.reports-breakdown-row:last-child { border-bottom: 0; }
td.reports-overdue { color: var(--danger); font-weight: 600; }

/* Reports page: report picker -> preview -> export */
.reports-controls { margin-bottom: 1rem; }
.reports-controls-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
}
.reports-controls-row .form-field { margin: 0; }
.reports-control-report { min-width: 15rem; }
.reports-controls-row input[type="date"],
.reports-controls-row select,
/* A searchable picker's trigger stands in for its select in the layout, so
   the wrapper needs the same floor width. */
.reports-controls-row .searchable-select { min-width: 9rem; }
.reports-export-button { margin-left: auto; flex: none; }

/* The preview owns the page's scrolling. #reportsPage is a flex column inside
   an overflow:hidden .app-page, so without flex sizing and a scroller of its own
   any report taller than the viewport was hard-clipped mid-table with no
   scrollbar anywhere in the chain. */
.reports-preview {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}
.reports-report-header { margin-bottom: 0.25rem; }
.reports-report-header h2 { margin: 0; font-size: 1.375rem; }
.reports-report-subtitle { margin: 0.25rem 0 0; font-weight: 600; }
.reports-report-period { margin: 0.125rem 0 0; color: var(--muted); font-size: 0.85rem; }
.reports-section-heading {
  margin: 0.5rem 0 -0.25rem;
  font-size: 1rem;
}
.reports-note { color: var(--muted); font-size: 0.85rem; margin: 0; }

/* Report callout: the one section type that has to stop the eye. A note is
   deliberately muted so a reader scanning for figures slides over it; a callout
   is the opposite, and it exists because "this aircraft is grounded" cannot be
   allowed to read like a footnote. Colours are per-tone classes rather than an
   inline style so the four tones stay defined in one place, and they mirror
   TONE_COLORS in backend/core/report_sections.py. */
.reports-callout {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin: 0;
  padding: 0.875rem 1rem;
  border-radius: 0.5rem;
  border-left: 0.25rem solid var(--muted);
  background: rgba(var(--rgb-slate), 0.1);
}
.reports-callout-title { font-size: 0.95rem; }
.reports-callout-text { margin: 0; font-size: 0.875rem; }
.reports-callout-list { margin: 0; padding-left: 1.125rem; font-size: 0.875rem; }
.reports-callout-list li { margin: 0.125rem 0; }
.reports-callout-danger { border-left-color: var(--danger); background: rgba(var(--rgb-red), 0.12); }
.reports-callout-danger .reports-callout-title { color: var(--danger); }
.reports-callout-warn { border-left-color: var(--fg-amber); background: rgba(var(--rgb-orange), 0.12); }
.reports-callout-warn .reports-callout-title { color: var(--fg-amber); }
.reports-callout-ok { border-left-color: var(--green); background: var(--green-soft); }
.reports-callout-ok .reports-callout-title { color: var(--green); }
.reports-callout-info { border-left-color: var(--blue); background: rgba(var(--rgb-blue), 0.12); }
.reports-callout-info .reports-callout-title { color: var(--blue); }
.reports-section-card { display: flex; flex-direction: column; gap: 0.75rem; }
.reports-section-title { font-size: 0.95rem; }

/* Compound so it beats .stats-grid's `margin: 1rem 0 0.25rem` (same
   specificity, later in the file) - the tiles belong to the heading above
   them, not floating a line away from it. */
.stats-grid.reports-tiles { margin: 0; }
.stat-tile {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.75rem 0.875rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.08);
}
.stat-tile-value { font-size: 1.375rem; font-weight: 700; line-height: 1.1; }
.stat-tile-label { font-size: 0.75rem; color: var(--muted); }

.reports-chart-wrap { position: relative; height: 16rem; width: 100%; }
.reports-loading, .reports-empty { color: var(--muted); text-align: center; }
.reports-empty-cell { color: var(--muted); text-align: center; }

/* Report map section. The wrap is a flex column because the map gained a
   legend and a caption below it - a fixed-height panel with a second child is
   exactly the shape that silently clips one of them. The map itself needs an
   explicit height: MapLibre sizes to its container, and a container that is
   only as tall as its (empty) content collapses to nothing. */
.reports-map-wrap { display: flex; flex-direction: column; gap: 0.5rem; }
.reports-map {
  height: 20rem;
  width: 100%;
  border-radius: 0.5rem;
  overflow: hidden;
  border: 0.0625rem solid var(--line);
  background: rgba(var(--rgb-slate), 0.08);
}
.reports-map-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--muted);
  font-size: 0.8rem;
}
.reports-map-legend li { display: flex; align-items: center; gap: 0.375rem; }
.reports-map-swatch {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 0.1875rem;
  flex: none;
}
.reports-map-caption { margin: 0; color: var(--muted); font-size: 0.75rem; }
.reports-map-empty {
  margin: 0;
  padding: 1.5rem 1rem;
  color: var(--muted);
  text-align: center;
  border: 0.0625rem dashed var(--line);
  border-radius: 0.5rem;
}

html.mobile .reports-controls-row { gap: 0.625rem; }
html.mobile .reports-controls-row .form-field,
html.mobile .reports-control-report { min-width: 0; flex: 1 1 45%; }
html.mobile .reports-export-button { margin-left: 0; flex: 1 1 100%; }
html.mobile .reports-chart-wrap { height: 13rem; }
/* Shorter on a phone so a map section does not fill the screen and hide the
   figures either side of it. Panning still works - the map is built with
   cooperative gestures, so one finger scrolls the page past it and two move
   the map. */
html.mobile .reports-map { height: 14rem; }

/* Platform ops overview */
.platform-overview-actions { display: flex; align-items: center; gap: 0.75rem; }
.platform-overview-updated { color: var(--muted); font-size: 0.8rem; }
.platform-status-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.platform-status-tile {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.625rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.08);
}
.platform-status-dot { width: 0.625rem; height: 0.625rem; border-radius: 50%; flex: none; background: var(--muted); }
.platform-status-tile.status-ok .platform-status-dot { background: var(--risk-go); }
.platform-status-tile.status-warn .platform-status-dot { background: var(--amber-solid); }
.platform-status-tile.status-down .platform-status-dot { background: var(--danger); }
.platform-status-tile.status-idle .platform-status-dot { background: var(--dot-idle); }
.platform-status-body { display: flex; flex-direction: column; min-width: 0; }
.platform-status-label { font-weight: 600; font-size: 0.85rem; }
.platform-status-detail { color: var(--muted); font-size: 0.75rem; }
.platform-stats-grid { margin-top: 0.5rem; }
.platform-overview-recent { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
.platform-overview-recent > div { min-width: 0; }
.platform-recent-row {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.375rem 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  font-size: 0.85rem;
}
.platform-recent-row:last-child { border-bottom: 0; }
.platform-recent-main { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.platform-recent-when { color: var(--muted); flex: none; font-size: 0.75rem; }
html.mobile .platform-overview-recent { grid-template-columns: 1fr; gap: 0.75rem; }

/* Accessibility utilities */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* Fixed to the window, so it is at the same corner whatever the page has scrolled
   to, and instant: it used to slide in over 0.15 s and was caught half-drawn (its top
   edge cut off, audit S-10). A skip link that is there or not there is easier to read. */
.skip-link {
  position: fixed;
  left: 0.5rem;
  top: -4rem;
  z-index: 2000;
  background: var(--green);
  color: var(--on-green-2);
  padding: 0.5rem 0.875rem;
  border-radius: 0.375rem;
  font-weight: 600;
  box-shadow: var(--shadow-alerts);
}
.skip-link:focus { top: 0.5rem; }

.table-wrap {
  overflow-x: auto;
  margin-top: 0.75rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.data-table th,
.data-table td {
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  white-space: nowrap;
}

/* Row text a touch larger than the base so it reads at the same size as the
   form/overview text; headers keep their own smaller uppercase size below. */
.data-table td {
  font-size: 0.9375rem;
}

.data-table th {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.reports-layout canvas {
  margin-top: 0.75rem;
  max-height: 13.75rem;
}

.jobs-dashboard-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
  overflow-y: auto;
  height: 100%;
}

/* The list and the job side by side, both the full height of the page. The
   job gets two thirds: it is where the work is done, and the list only has to
   identify a row. The Overview tiles and Job Activity chart that used to sit
   above both are the Job activity report now. */
.jobs-page-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 1rem;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

/* Replaces a chart that could not be built - a blocked Chart.js CDN, or a
   canvas the markup no longer has (see createChart). Absolutely positioned
   because every chart holder in the app (.dashboard-chart-wrap,
   .reports-chart-wrap) is position:relative with a
   height of its own, and this one's canvas is absolute inside it: a note in
   normal flow would sit under a canvas-shaped hole or grow the card. */
.chart-unavailable {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 1rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.8125rem;
}

.jobs-table-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.jobs-table-card .table-wrap {
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-gutter: stable;
  min-height: 0;
  flex: 1 1 auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

/* The heading, with the filter cue at its right end once a filter is on; then
   the filters as one set - the search box with Sort at its end (an icon: THE
   SORT ICON, ux:U3), the status and the client - one height, one style,
   sharing the width. Sort was a row of its own under the filters, the cue
   beside it, until 2026-11-02 (Josh: the same icon as on a phone); the row's
   36px and its gap went to the list. "Upcoming and under way" sat beside the
   heading as a checkbox until 2026-11-01; it is the choice "Upcoming" in the
   Status picker now. The filters used to
   sit in a single wrapping row with a label each, which at a third of a 1920
   window broke onto two lines at random (Client alone on the second, the
   checkbox far right), at three different heights (27, 28 and 32 px), with a
   client picker 82 px wide. The controls say what they are ("All statuses",
   "All clients", "Search name or number"); the labels stay for screen readers. */
.jobs-table-toolbar {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  /* Clear of the list below it. The list's framed box joins straight onto the
     filters otherwise, and the row of pickers reads as part of the table. The
     app's own space above a table is .table-wrap's 0.75rem top margin, which
     this card has to zero (its header and body are two tables that must
     join), so the toolbar carries it instead. */
  margin-bottom: 0.75rem;
}
.jobs-toolbar-head {
  display: flex;
  /* The cue on the heading's baseline. The heading's margin box (1.5rem of line
     and its 0.625rem margin) is the row's whole height, so the heading sits
     exactly where it did when the row centred it, and the cue adds no height. */
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  /* The same fixed height as the Flights page's column headers, so the two
     pages' headings sit on one line. */
  min-height: 2.125rem;
}
/* The toolbar's control height: the shared dropdown box's (DROPDOWN CONTROLS:
   its padding, its pinned line box and its border), which the search box takes
   too. The Sort icon is a square of it; a phone sets its own (THE PLAN PAGE ON
   A PHONE, 3). */
.all-jobs-card { --plan-control-h: calc(2 * var(--dropdown-pad-y) + var(--dropdown-line) + 0.125rem); }
/* One row where the list is wide: the search box (two shares) with the icon at
   its end, then the two pickers (a share each). The four cells are named once,
   for a phone and a computer alike; a phone lays them out in a grid of its own
   (THE PLAN PAGE ON A PHONE, 3), and a narrow list in two rows (the jobs-list
   container query further down). */
.jobs-toolbar-filters {
  display: grid;
  grid-template-columns: minmax(0, 2fr) auto minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas: "search sort status client";
  gap: 0.5rem;
}
.jobs-filter-search { grid-area: search; }
.jobs-sort-field { grid-area: sort; }
.jobs-filter-status { grid-area: status; }
.jobs-filter-client { grid-area: client; }
.jobs-filter-field {
  display: flex;
  min-width: 0;
}
.jobs-filter-field > .searchable-select { flex: 1 1 auto; min-width: 0; }

/* The two pickers take the shared dropdown metrics (see DROPDOWN CONTROLS);
   the search box matches them so the three filters are one row of one size. */
.jobs-search-input {
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  margin: 0;
  outline: none;
}
.jobs-search-input:focus { border-color: var(--focus-ring); }
/* "All clients" and "All statuses" are choices here, not placeholders to grey
   out - and the pair have to agree, whichever kind of dropdown each one is. */
.jobs-filter-field .searchable-select-trigger.is-placeholder,
.jobs-status-filter:has(option[value=""]:checked) { color: var(--fg-body); }

/* Row-count footer under the table (the list loads incrementally on scroll, so
   there are no page controls). */
.jobs-pagination {
  margin-top: 0.5rem;
  text-align: right;
  font-size: 0.75rem;
  color: var(--muted);
}

.jobs-pagination[hidden] { display: none; }

.all-jobs-card {
  grid-column: 1;
  grid-row: 1;
  min-height: 0;
}

.job-preview-card {
  grid-column: 2;
  grid-row: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#jobPreviewContent {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.job-preview-tabs {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.25rem;
  flex: 0 0 auto;
}

/* Same visual language as .settings-tab (transparent by default, subtle
   hover highlight, glowing accent bar on the active tab) - just horizontal,
   so the accent bar runs along the bottom edge instead of the left edge. */
.job-preview-tab,
.side-tab,
.risk-assessment-tab {
  position: relative;
  width: auto;
  min-height: 2.25rem;
  padding: 0.5rem 0.875rem;
  border: 0;
  border-radius: 0.5rem;
  color: var(--fg-nav);
  background: transparent;
  font-size: 0.8125rem;
  font-weight: 700;
  transition: background 120ms ease, color 120ms ease;
}

/* One assigned action: what to do, what it is for, when by, and the one
   control the assignee actually has. */
.my-action-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0.875rem;
  margin-bottom: 0.5rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
.my-action-row.is-done { opacity: 0.62; }
.my-action-row.is-done .my-action-main strong { text-decoration: line-through; }

.my-action-main {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}
.my-action-context,
.my-action-due {
  font-size: 0.8125rem;
  color: var(--muted);
}
.my-action-due.is-overdue { color: var(--danger); font-weight: 650; }

html.mobile .my-action-row {
  align-items: stretch;
  flex-direction: column;
  gap: 0.625rem;
}

.job-preview-tab:hover:not(:disabled),
.side-tab:hover:not(:disabled),
.risk-assessment-tab:hover:not(:disabled) {
  filter: none;
  background: rgba(var(--rgb-overlay), 0.055);
  color: var(--fg-strong);
}

.job-preview-tab.active,
.side-tab.active,
.risk-assessment-tab.active {
  color: var(--fg-strong);
  background: var(--active-item-bg);
}

.job-preview-tab.active::before,
.side-tab.active::before,
.risk-assessment-tab.active::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0.25rem;
  border-radius: 0 0 0.5rem 0.5rem;
  background: var(--green);
  box-shadow: var(--glow-active);
}

.job-preview-tab-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin-top: 0.875rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.job-preview-tab-panel {
  display: none;
}

.job-preview-tab-panel.active {
  display: block;
}

/* Preview header right-hand controls: the stage controls (Submit, Approve,
   Send back, Withdraw), Close and a compact Export. They wrap, right-aligned,
   rather than run off the card: "Submit for approval" and Edit job together
   are wider than a phone's header. */
.job-preview-header-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  flex: 0 1 auto;
}
/* Why Close job (or Mark complete) is not offered: "Close job needs a flight log". */
.job-close-wait-note {
  font-size: 0.8125rem;
  color: var(--muted);
}
/* What a header action answered, under the pills. */
.job-preview-action-message {
  margin: 0.5rem 0 0;
}
/* A job with no name yet carries its client's name; muted, so it is not read
   as a name somebody chose (the list, the card's title). */
.is-placeholder-name {
  color: var(--muted);
}
.job-preview-stage-select {
  width: auto;
  padding: 0.3125rem 0.625rem;
  font-size: 0.75rem;
}

/* The job card shows one panel at a time, like the Add Job wizard: the base
   rules above show only the .active one (app.js showJobPreviewTab), and a
   tab that is not offered (Airspace) is [hidden] with its panel. The side
   padding keeps a focus ring inside the scrolling panel, which clips it. */
#jobPreviewContent .job-preview-tab-panel {
  padding: 0.25rem 0.125rem 1.125rem;
}

/* The job preview is display-only (its fields are never re-enabled), so strip
   the affordances that only make sense for editable inputs: the dropdown
   arrow, the date field's calendar-picker icon, and the "dd/mm/yyyy" format
   hint an empty date input shows. Empty selects already render blank because
   their placeholder <option> text is empty. All three kinds of dropdown, or
   the card would show an arrow on the client and none on the pilot beside it
   (which it did, before they were drawn from one place).
   The `.form-field` in each selector is carrying specificity, not narrowing
   the target: the shared arrow rule is `select:not([size]):not([multiple])`,
   which counts as two classes and would otherwise win on source order. */
.job-preview-card .form-field select:disabled,
.job-preview-card .form-field .searchable-select-trigger:disabled,
.job-preview-card .form-field .multiselect-dropdown-trigger:disabled {
  background-image: none;
  padding-right: var(--dropdown-pad-x);
}
/* These fields are display-only, so hovering them shouldn't imply interactivity
   (no text/not-allowed cursor) - keep the plain arrow. The extra `.form-field`
   is needed to outrank `.form-field input:disabled { cursor: not-allowed }`,
   which has equal specificity but is defined later. */
.job-preview-card .form-field input:disabled,
.job-preview-card .form-field select:disabled,
.job-preview-card .form-field textarea:disabled,
.job-preview-card .form-field button:disabled {
  cursor: default;
}
.job-preview-card input[type="date"]:disabled::-webkit-calendar-picker-indicator {
  display: none;
}

.job-preview-agricultural {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}

.job-preview-agricultural strong {
  display: block;
  margin-bottom: 0.625rem;
}

.job-billing-fields,
.job-billing-summary {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}

.job-billing-fields > strong,
.job-billing-summary > strong {
  display: block;
  margin-bottom: 0.625rem;
}

/* Charge-out rules list (org settings). */
.charge-rule-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  text-align: left;
  background: rgba(var(--rgb-slate), 0.06);
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  margin-bottom: 0.5rem;
  cursor: pointer;
  color: var(--text);
}
.charge-rule-row:hover { background: rgba(var(--rgb-slate), 0.12); }
.charge-rule-main { display: flex; flex-direction: column; gap: 0.125rem; flex: 1; min-width: 0; }
.charge-rule-name { font-weight: 600; }
.charge-rule-cond { font-size: 0.8125rem; color: var(--muted); }
.charge-rule-price { font-weight: 600; white-space: nowrap; }
.charge-rule-inactive { opacity: 0.6; }

/* Billing quote/invoice: "raise for a job vs manual" segmented toggle. */
.billing-link-toggle {
  display: inline-flex;
  width: fit-content;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  overflow: hidden;
}
.billing-link-btn {
  padding: 0.4375rem 1rem;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}
.billing-link-btn + .billing-link-btn { border-left: 0.0625rem solid var(--line); }
.billing-link-btn.active { background: var(--green-soft); color: var(--green); }

.spray-diary-products {
  margin-top: 1rem;
}

.spray-diary-products > strong {
  display: block;
  margin-bottom: 0.5rem;
}

/* ux:SR1 - three lines, not one row of seven boxes a few characters wide (and on a phone
   about 40 px each): the product and its remove button; active ingredient and ACVM
   number; batch, rate and unit. The last column is the remove button's, and the two lower
   lines run into it so every line ends at the same edge. */
.spray-product-row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr)) auto;
  gap: 0.375rem;
  margin-bottom: 0.375rem;
  align-items: center;
}
.spray-product-row .sp-name-field { grid-column: 1 / 7; }
.spray-product-row .line-remove { grid-column: 7 / 8; grid-row: 1; align-self: end; }
.spray-product-row .sp-ai-field { grid-column: 1 / 4; }
.spray-product-row .sp-acvm-field { grid-column: 4 / 8; }
.spray-product-row .sp-batch-field { grid-column: 1 / 3; }
.spray-product-row .sp-rate-field { grid-column: 3 / 5; }
.spray-product-row .sp-unit-field { grid-column: 5 / 8; }
/* Job stages step 8: the product's own re-entry and withholding, a fourth line. */
.spray-product-row .sp-reentry-field { grid-column: 1 / 4; }
.spray-product-row .sp-withhold-field { grid-column: 4 / 8; }
.spray-product-row .sp-restriction,
.spray-product-row .sp-field { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.spray-product-row .sp-restriction > span,
.spray-product-row .sp-field > span { font-size: 0.75rem; color: var(--fg-copy); }

/* These inputs sit outside a .form-field, so they take the app's field theming directly,
   as the quote and invoice line items do (they were bare browser boxes). */
.spray-product-row input {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  line-height: var(--dropdown-line);
  font: inherit;
  font-size: 0.875rem;
  margin: 0;
}
.spray-product-row input:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
  outline: none;
}
.spray-product-row input::placeholder { color: var(--muted); }
.spray-product-row .line-remove {
  padding: 0.25rem 0.625rem;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
}

/* ux:SR1 - the spray record, complete. A product in the Spray diary is its row of inputs,
   then its label check (Aerial use, and a note once answered) and two lines: the product
   used, from the rate and the litres logged, and who recorded the check. */
.spray-product {
  padding-bottom: 0.625rem;
  margin-bottom: 0.625rem;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}
.spray-product .spray-product-row { margin-bottom: 0.375rem; }
.spray-product-check {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 0.375rem;
  align-items: end;
}
.spray-product-check .form-field { margin: 0; min-width: 0; }
.spray-product-meta { margin: 0.25rem 0 0; }
/* What the flight logs say, at the top of the dialog. */
.spray-diary-facts {
  margin-bottom: 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
.spray-diary-facts .form-hint { margin: 0; font-size: 0.8125rem; color: var(--fg-copy); }
.spray-diary-facts .form-hint + .form-hint { margin-top: 0.125rem; }
/* The card's lines under the diary summary; an incomplete record reads amber. */
.spray-record-times,
.spray-record-check { margin-top: 0.25rem; }
.spray-record-check.is-missing { color: var(--fg-warn); }
/* The Close job dialog's warning: a caution, like the diary's own lock notice. */
.job-close-spray {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin: 0 0 0.875rem;
  padding: 0.625rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-amber), 0.1);
  border: 0.0625rem solid rgba(var(--rgb-amber), 0.4);
  color: var(--fg-copy);
  font-size: 0.875rem;
}
.job-close-spray strong { font-weight: 600; color: var(--fg-body); }
/* Job stages step 8: a close the organisation's close gates refuse. */
.job-close-spray.is-blocked { background: var(--danger-bg); border-color: var(--danger); }
.job-close-spray.is-blocked strong { color: var(--danger); }

/* Job stages step 3 - planned products on a spray job's plan (card Flight tab and the job
   form's Flight step). Each product is a row that opens it, like a person on the notification
   record; its label check is a pill whose words differ by answer (amber not checked, red no,
   green yes), and the block's line above the list says what needs attention, amber. The old
   Payload text sits in a quiet panel with its one action. */
.planned-products {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.375rem;
  margin: 0 0 0.875rem;
}
.planned-products .spray-diary-actions { margin-top: 0.125rem; }
.planned-products-title { font-weight: 600; color: var(--fg-body); margin-top: 0.25rem; }
/* Compound, so it beats .card-copy (later in the file), which the line also carries. */
.card-copy.planned-products-check { margin: 0; color: var(--fg-warn); }
.planned-target {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.5rem;
}
.planned-target[hidden] { display: none; }
.planned-target-label,
.planned-old-note-label {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.planned-target-text { min-width: 0; color: var(--fg-body); overflow-wrap: anywhere; }
.planned-target-text.is-empty { color: var(--muted); }
.planned-target-edit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.planned-target-edit input { flex: 1 1 12rem; min-width: 0; }
.planned-product-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.planned-product-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  width: 100%;
  margin: 0;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: transparent;
  color: var(--fg-body);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.planned-product-row:hover { background: rgba(var(--rgb-slate), 0.06); }
.planned-product-row .pp-name { font-weight: 600; overflow-wrap: anywhere; }
.planned-product-row .pp-meta { font-size: 0.8125rem; color: var(--fg-copy); overflow-wrap: anywhere; }
.planned-product-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.125rem;
}
.pp-label,
.pp-source {
  display: inline-block;
  padding: 0.1875rem 0.4375rem;
  border-radius: 62.4375rem;
  font-size: 0.6875rem;
  font-weight: 750;
  white-space: nowrap;
}
.pp-label.is-yes { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.pp-label.is-no { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
.pp-label.is-unchecked { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
.pp-source { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.planned-old-note {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
.planned-old-note-text { color: var(--fg-body); overflow-wrap: anywhere; }
/* The dialog: where a client's product came from, and the label check as three answers. */
.planned-product-from {
  margin: 0 0 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-amber), 0.14);
  color: var(--pill-amber-fg);
}
.planned-label-check {
  margin: 0.75rem 0 0;
  padding: 0;
  border: 0;
  min-inline-size: 0;
}
.planned-label-check legend {
  padding: 0;
  margin-bottom: 0.25rem;
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.planned-label-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
}
.planned-label-options > label {
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.875rem;
}

/* ux:SR2 - the notification record (market gap G2). Each person told is a row that opens
   them (a button, so the keyboard reaches it); the notice check reads amber when somebody
   counted was not told, or told late. What the site questionnaire said to tell sits in a
   quiet panel with an Add on each line. */
.job-notification-list {
  list-style: none;
  margin: 0.375rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.job-notification-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  width: 100%;
  margin: 0;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: transparent;
  color: var(--fg-body);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.job-notification-row:hover { background: rgba(var(--rgb-slate), 0.06); }
.job-notification-row .jn-name { font-weight: 600; overflow-wrap: anywhere; }
.job-notification-row .jn-meta { font-size: 0.8125rem; color: var(--fg-copy); overflow-wrap: anywhere; }
.job-notification-row .jn-meta.is-short { color: var(--fg-warn); }
.job-notification-row.is-erased .jn-name { font-weight: 400; font-style: italic; color: var(--muted); }
.job-notification-check { margin-top: 0.25rem; }
.job-notification-check.is-short { color: var(--fg-warn); }
.job-notification-suggestions {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin: 0.5rem 0 0;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
.job-notification-suggestions .jn-suggestions-head { margin: 0; font-size: 0.8125rem; color: var(--fg-copy); }
.job-notification-suggestions .jn-suggestions-hint { margin: 0; font-size: 0.75rem; color: var(--fg-copy); }
.job-notification-suggestion {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.job-notification-suggestion .jn-suggestion-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.job-notification-suggestion button { width: auto; flex: 0 0 auto; }
/* The dialog: where a pre-filled row came from, and who recorded it. */
.job-notification-from {
  margin: 0 0 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  color: var(--fg-copy);
  overflow-wrap: anywhere;
}
#jobNotificationForm .filter-checkbox-label { margin: 0.25rem 0 0.5rem; }
/* Which person the site questionnaire named this is: above the grid, its own row. */
#jobNotificationSourceField { margin-bottom: 0.75rem; }

/* ux:SR3 - the on-site check before spraying (market gap G5). The block's first line says
   whether the check was made, by whom and when, amber when it failed or none is recorded once
   the job has flown; the hazards found on site follow as quiet rows (they are changed in the
   Risk assessment, not here). The dialog's Chance and Effect are the risk dialog's own picker. */
.job-onsite-summary { margin: 0.25rem 0 0.5rem; overflow-wrap: anywhere; }
.job-onsite-summary.is-short { color: var(--fg-warn); }
.job-onsite-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.job-onsite-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  color: var(--fg-body);
}
.job-onsite-row .jo-name { font-weight: 600; overflow-wrap: anywhere; }
.job-onsite-row .jo-meta { font-size: 0.8125rem; color: var(--fg-copy); overflow-wrap: anywhere; }

/* Job stages step 4: the mix as made up on the day. On the card, one quiet row per day (the
   hazards' row style); in its dialog, one row per planned product (As planned / Changed and the
   batch number; a changed one says what was mixed instead and why), then any product added. */
.job-onsite-mix { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; }
.job-onsite-mix-title { font-weight: 600; color: var(--fg-body); }
/* Job stages step 8: the signs on a job's notification record, and the diary's tank mix. */
.job-occurrence-list { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.job-occurrence-list li { display: flex; flex-direction: column; font-size: 0.875rem; }
.job-occurrence-list .jo-title { color: var(--fg-body); font-weight: 600; }
.job-occurrence-list .jo-meta { color: var(--fg-copy); }
.job-occurrence-list .jo-meta.is-high { color: var(--danger); }
.job-onsite-late { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.125rem; font-size: 0.875rem; color: var(--fg-copy); }
.job-onsite-late .is-owed { color: var(--fg-warn); }
.job-signs { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem; }
.job-signs-title { font-weight: 600; color: var(--fg-body); font-size: 0.875rem; }
.job-signs-check { margin: 0; }
.spray-diary-tank-mix { margin-top: 0.75rem; }
.spray-diary-tank-mix > strong { display: block; margin-bottom: 0.375rem; }
.job-onsite-mix-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.job-onsite-mix-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  color: var(--fg-body);
}
.job-onsite-mix-row .jo-name { font-weight: 600; overflow-wrap: anywhere; }
.job-onsite-mix-row .jo-meta { font-size: 0.8125rem; color: var(--fg-copy); overflow-wrap: anywhere; }
.job-mix-rows { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.75rem 0 0.5rem; }
.job-mix-rows:empty { display: none; }
.job-mix-row {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  padding: 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
}
.job-mix-head { display: flex; align-items: baseline; gap: 0.5rem; justify-content: space-between; }
.job-mix-head strong { overflow-wrap: anywhere; }
.job-mix-planned { color: var(--fg-copy); font-size: 0.8125rem; margin-right: auto; }
.job-mix-head .line-remove {
  padding: 0.25rem 0.625rem;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
}
.job-mix-line {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.375rem;
  align-items: center;
}
.job-mix-line > * { min-width: 0; }
.job-mix-line input {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  line-height: var(--dropdown-line);
  font: inherit;
  font-size: 0.875rem;
  margin: 0;
}
.job-mix-line input:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
  outline: none;
}
.job-mix-line input::placeholder { color: var(--muted); }
.job-mix-line input[aria-invalid="true"] { border-color: var(--fg-warn); }

/* Job stages step 4: the logs against the plan, under the spray diary. Quiet lines; a kind with
   a log outside the plan reads amber (warn, never block). */
.job-plan-checks { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; }
.job-plan-checks-title { font-weight: 600; color: var(--fg-body); }
.job-plan-checks-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.125rem; }
.job-plan-check { color: var(--fg-copy); overflow-wrap: anywhere; }
.job-plan-check.is-short { color: var(--fg-warn); }

/* Job stages step 4: a diary row's planned side - what the plan said, and Against the plan
   with its reason - between the row's inputs and its label check. */
.spray-diary-prefill { margin: 0.25rem 0 0.5rem; }
.spray-product-plan { margin: 0 0 0.375rem; }
.spray-product-plan-line { margin: 0 0 0.25rem; }
/* A planned product added to a saved diary from the plan, and a plain row's offer to link. */
.spray-product-from-plan { margin: 0 0 0.25rem; color: var(--fg-warn); }
.spray-product-link { margin: 0 0 0.375rem; }
/* A planned product the diary says was not applied: its inputs greyed, its reason kept. */
.spray-product.is-not-applied .spray-product-row input:disabled { opacity: 0.55; }
.spray-product.is-not-applied .spray-product-row .sp-unit-field .searchable-select { opacity: 0.55; }
/* What each day's mix recorded, read only, above the diary's rows: compact quiet lines. */
.spray-diary-mix {
  margin: 0.25rem 0 0.75rem;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
.spray-diary-mix-title { font-weight: 600; color: var(--fg-body); font-size: 0.875rem; }
.spray-diary-mix-list { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.375rem; }
.spray-diary-mix-day { display: flex; flex-direction: column; gap: 0.0625rem; }
.spray-diary-mix-when { font-weight: 600; font-size: 0.8125rem; color: var(--fg-body); }
.spray-diary-mix-line { font-size: 0.8125rem; color: var(--fg-copy); overflow-wrap: anywhere; }
.spray-product-plan-fields {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 0.375rem;
  align-items: end;
}
.spray-product-plan-fields .form-field { margin: 0; min-width: 0; }
.spray-product-plan-fields .sp-plan-reason[aria-invalid="true"] { border-color: var(--fg-warn); }
.job-onsite-grade { margin: 0 0 0.75rem; font-weight: 600; }
.job-onsite-score { margin-top: 0.25rem; }
.job-onsite-score .risk-score-group { align-items: flex-start; }
.job-onsite-score .risk-seg-row { grid-template-columns: repeat(5, 2.5rem); }
.job-onsite-score .risk-score-readout { font-size: 0.75rem; }
/* The risk dialog's tag on a found row carries a date, a time and a name: it wraps inside its
   hazard cell instead of running under the Consequence box beside it. */
.risk-from-client.risk-found-on-site { white-space: normal; overflow-wrap: anywhere; }

/* Job stages step 6: the site hazards, scored per day in the on-site check. */
.job-site-hazards-progress { margin: 0.75rem 0 0.5rem; font-weight: 600; }
.job-site-hazard-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.job-site-hazard-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "head risk" "na na" "scores scores";
  gap: 0.375rem 0.75rem;
  padding: 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
}
.job-site-hazard-row.risk-row-unassessed { background: rgba(var(--rgb-amber), 0.07); }
.job-site-hazard-head { grid-area: head; display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.job-site-hazard-head .jo-name { font-weight: 600; overflow-wrap: anywhere; }
.job-site-hazard-head .jo-meta { color: var(--fg-copy); font-size: 0.8125rem; overflow-wrap: anywhere; }
.job-site-hazard-row .risk-na-toggle { grid-area: na; justify-self: start; }
.job-site-hazard-row .risk-na-label { display: inline; font-size: 0.875rem; }
.job-site-hazard-scores { grid-area: scores; display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
.job-site-hazard-scores .risk-score-group { align-items: flex-start; flex-direction: column; }
.job-site-hazard-scores .risk-score-name { display: inline; font-weight: 600; margin-right: 0.375rem; }
.job-site-hazard-scores .risk-seg-row { grid-template-columns: repeat(5, 2.5rem); }
.job-site-hazard-scores .risk-score-readout { font-size: 0.75rem; }
.job-site-hazard-risk {
  grid-area: risk;
  align-self: start;
  padding: 0.125rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 62.4375rem;
  font-size: 0.75rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--muted);
}
.job-site-hazard-risk.grade-go,
.job-site-hazard-risk.grade-manage,
.job-site-hazard-risk.grade-nogo { border-color: transparent; color: var(--on-risk); }
.job-site-hazard-risk.grade-go { background: var(--risk-go); }
.job-site-hazard-risk.grade-manage { background: var(--risk-manage); }
.job-site-hazard-risk.grade-nogo { background: var(--risk-nogo); }
/* The risk dialog's "On the day" group: listed, nothing to score there. */
.risk-checklist-day-note {
  display: block;
  margin-top: 0.125rem;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--fg-copy);
}
.modal-body .job-site-hazards-progress { margin: 0.75rem 0 0.5rem; }
.risk-checklist-row.risk-row-for-the-day > td { padding: 0.375rem 0.75rem; }
.risk-checklist-row.risk-row-for-the-day .risk-day-tag { margin-left: 0.5rem; }

.job-area-draw-search {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.job-area-draw-search input {
  flex: 1;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.75rem 0.875rem;
  font: inherit;
  font-size: 0.875rem;
  outline: none;
}
.job-area-draw-search input::placeholder { color: var(--muted); }
.job-area-draw-search input:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.125rem rgba(var(--rgb-green), 0.14);
}

.job-area-draw-search button {
  width: auto;
  flex: 0 0 auto;
}

.job-area-draw-search-results {
  margin-top: 0.5rem;
  max-height: 10rem;
  overflow-y: auto;
  border: 0.0625rem solid var(--frame-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
}

.job-area-draw-search-result {
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  font-size: 0.8125rem;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}

.job-area-draw-search-result:last-child {
  border-bottom: 0;
}

.job-area-draw-search-result:hover {
  background: rgba(var(--rgb-overlay), 0.06);
}

/* Flexes into whatever height is left after the header, search box and action
   row rather than claiming a fixed 70vh - at 70vh the "Save area" button sat
   below the card's clipped bottom edge on any laptop-height window, leaving no
   visible way to save a polygon you had just drawn. */
.job-area-draw-map {
  width: 100%;
  flex: 1 1 auto;
  min-height: 12rem;
  border-radius: 0.5rem;
  overflow: hidden;
  margin: 0.75rem 0;
}

/* Under the draw map: what the editor is doing and what a tap will do. It
   holds its height as the text changes - the map takes whatever height is
   left, so a line coming and going would move the map under the pointer, or
   the finger, in the middle of drawing. One line on a desktop; on a phone the
   longer messages run to two, so two are kept. */
.job-area-draw-status {
  margin: 0 0 0.625rem;
  line-height: 1.45;
  min-height: 1.45em;
  font-size: 0.9375rem;
  color: var(--fg-copy);
}
html.mobile .job-area-draw-status {
  min-height: 2.9em;
}
/* The editor on a phone (review 2026-11-07 item 10): the map had 168 px, a
   quarter of an iPhone 13, under three lines of help and over three rows of
   buttons, and at 320x568 Save area was below the fold. The help goes (the
   status line under the map says what a tap does), the buttons wrap into two
   rows pinned to the foot of the sheet, every one in sight, Save area last at
   the right, and the map takes the height that frees. */
html.mobile #jobAreaDrawOverlay .modal-card > .card-copy { display: none; }
html.mobile #jobAreaDrawOverlay .job-area-draw-search { margin-top: 0.5rem; }
html.mobile #jobAreaDrawOverlay .job-area-draw-map { margin: 0.5rem 0; }
html.mobile #jobAreaDrawOverlay .job-area-draw-status { margin-bottom: 0.25rem; }
html.mobile #jobAreaDrawOverlay .job-area-draw-meta { margin-bottom: 0.25rem; }
html.mobile #jobAreaDrawOverlay .modal-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  flex: none;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0.5rem 0 0.25rem;
  background: var(--sheet-bg);
}
html.mobile #jobAreaDrawOverlay .modal-actions > button { flex: none; padding-inline: 0.75rem; }
html.mobile #jobAreaDrawSaveButton { margin-left: auto; }
/* "Import" on a phone (the name keeps the kinds), so the six buttons take two rows. */
html.mobile .job-area-draw-import-kinds { display: none; }

/* One message line's height, held whether or not a message is showing (see the
   markup). A longer message still grows it; a one-line one never moves the map. */
.job-area-draw-message-slot {
  min-height: 2.25rem;
}
.job-area-draw-message-slot .form-message {
  margin: 0.625rem 0 0;
}

/* The draw-map right-click menu reuses the shared .map-context-menu component;
   it only needs a higher z-index so it clears the modal overlay (z-index 50). */
#jobAreaDrawContextMenu { z-index: 60; }

/* The Location panel becomes a flex column so its preview map can stretch
   to fill whatever height is left after the location text and the button,
   rather than sitting at a fixed 260px. */
.job-preview-tab-panel.active[data-preview-panel="location"] {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* The map and its empty state stack in one frame, so the message sits INSIDE
   the box rather than under it. An empty bordered rectangle with the
   instruction below it reads as something that failed to load. */
.job-location-preview {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  min-height: 10rem;
}

.job-location-preview .job-location-preview-map {
  flex: 1 1 auto;
}

.job-location-empty {
  position: absolute;
  inset: 0.5rem 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 0.375rem;
  padding: 1.125rem;
  border-radius: 0.5rem;
  /* Opaque: it covers a map canvas that may still hold the previous job's
     tiles, and a translucent empty state over somebody else's paddock is
     worse than no empty state at all. */
  background: var(--map-empty-bg);
  color: var(--fg-bright);
  text-align: center;
  font-size: 0.875rem;
  /* The map underneath is clickable and opens the draw dialog; this is a
     label, not a lid. */
  pointer-events: none;
}

.job-location-empty[hidden] { display: none; }

.job-location-empty-sub {
  color: var(--muted);
  font-size: 0.8125rem;
}

.job-location-preview-map {
  width: 100%;
  flex: 1 1 auto;
  min-height: 10rem;
  border-radius: 0.5rem;
  overflow: hidden;
  margin: 0.5rem 0;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  cursor: pointer;
}

/* The Location tab is the whole panel, so its map takes what the panel has
   left under the text, and no less than 30rem: Josh made it half as tall
   again on 2026-09-23 (35rem to 52.5rem) once a closed job drew its flights
   on it, and it now gets that room from the tab instead of from a scroll. */
#jobPreviewContent .job-location-preview-map {
  min-height: 30rem;
}
/* Not on a phone: the whole card scrolls there (THE OPEN JOB ON A PHONE), so
   the panel has no height to fill, and 52.5rem would be 714px of an 844px
   screen. */
html.mobile #jobPreviewContent .job-location-preview-map {
  flex: none;
  height: 35rem;
  min-height: 0;
}

/* Expanded, interactive operational-area map (opened by clicking the preview).
   Landscape: same height, but widened well past the default modal 640px cap
   (the compound selector outranks `.modal-card`'s width/max-width). */
.modal-card.job-location-expand-card {
  width: 94vw;
  max-width: 100rem;
  height: 85vh;
  display: flex;
  flex-direction: column;
}
.job-location-expand-map {
  flex: 1 1 auto;
  min-height: 0;
  border-radius: 0.5rem;
  overflow: hidden;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}

/* The Add Job Location tab's preview box is always present, filling the tab,
   with a centred placeholder until a location is drawn. */
#jobCreateLocationMap {
  position: relative;
  height: auto;
  background: var(--control-bg);
}
/* Here the empty box is the way in, not a label over a clickable map: a
   button, so it takes the pointer, the focus ring and the keyboard. */
#jobCreateLocationEmpty {
  pointer-events: auto;
  cursor: pointer;
  width: auto;
  min-height: 0;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 400;
  border: 0.0625rem dashed rgba(var(--rgb-slate), 0.35);
}
#jobCreateLocationEmpty:hover { border-color: rgba(var(--rgb-green), 0.55); }
#jobCreateLocationEmpty .job-location-empty-cta {
  margin-top: 0.5rem;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background: var(--button-bg);
  color: var(--button-fg);
  font-weight: 700;
}
/* With an area drawn: on the map's corner, over the thing it edits. */
.job-location-edit {
  position: absolute;
  top: 1rem;
  right: 0.5rem;
  z-index: 1;
}


/* Compound with .flight-filter-label, which the element also carries: that
   rule is later in the file at equal specificity, so a bare .org-notam-heading
   selector loses. Block, or the margin-top is ignored entirely - vertical
   margins have no effect on a non-replaced inline box - and resetting
   margin-bottom stops .flight-filter-label's -0.25rem, inert while the box was
   inline, from pulling the control up over the heading. */
.flight-filter-label.org-notam-heading {
  display: block;
  margin-top: 1.375rem;
  margin-bottom: 0;
}

.org-notam-approval-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-top: 0.375rem;
}

/* Keep the location readout compact so the preview map gets as much height as
   possible: shrink the label, tighten line height, and collapse the surrounding
   grid/button spacing. */
.job-preview-tab-panel[data-preview-panel="location"] .form-grid {
  margin-bottom: 0;
}

.job-preview-tab-panel[data-preview-panel="location"] .form-field span {
  font-size: 0.6875rem;
  margin-bottom: 0.125rem;
}

.job-preview-tab-panel[data-preview-panel="location"] #jobPreviewLocationText {
  font-size: 0.875rem;
  line-height: 1.25;
  margin: 0;
}

.job-preview-tab-panel[data-preview-panel="location"] .modal-actions {
  margin: 0;
}

/* The preview card's shared action row (Mark complete / etc.) keeps its 16px
   top+bottom margins even when every button in it is hidden, leaving a ~32px
   gap under the active tab's content. Collapse it when it has no visible
   button so the Location map can stretch all the way to the card's edge. */
#jobPreviewContent > .modal-actions:not(:has(button:not([hidden]))) {
  margin: 0;
}

/* The Add Job modal is a full tabbed editor (Overview/Flight/Location/Weather/
   Risk), so it needs more room than a plain form and a scrolling tab body. */
#jobCreateCard {
  width: 45rem;
  max-width: 94vw;
}

/* ONE SIZE FOR EVERY TAB. The overlay centres the card, so a card as tall as
   its content moved its top on every tab change - 180 px down on Overview, 314
   on Risk at 1440x900 - and the tabs jumped under the pointer. Fixed at the
   height the tallest realistic tab needs (Flight with the NOTAM and spraying
   fields), capped by the card's own 82vh; only the tab body scrolls.
   The FORM is the flex item between the tabs and the action row: as a plain
   block, nothing inside it could ever be height-limited, so these rules used
   to describe a scroll that never happened. On a phone the sheet is already
   full-height (html.mobile .modal-card), and this pins its buttons to the
   bottom instead of riding on the content. */
#jobCreateCard {
  display: flex;
  flex-direction: column;
  height: min(49rem, 82vh);
}
#jobCreateForm {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
#jobCreateCard .job-preview-tab-content {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
}
#jobCreateCard .job-create-wizard-actions {
  flex: 0 0 auto;
}
/* The Location tab's map takes the height the fixed card gives it. */
#jobCreateCard .job-preview-tab-panel.active[data-create-panel="location"] {
  display: flex;
  flex-direction: column;
  height: 100%;
}
#jobCreateCard .job-location-preview {
  position: relative;
  flex: 1 1 auto;
  min-height: 15rem;
}

/* In the Add Job wizard the tabs are step indicators, not clickable controls,
   so they're cyan (vs the panel's green) and don't respond to clicks/hover. */
#jobCreateTabs .job-preview-tab {
  cursor: default;
  pointer-events: none;
}

#jobCreateTabs .job-preview-tab.active::before {
  background: var(--tab-indicator-alt);
  box-shadow: var(--glow-active-alt);
}

/* Edit Job: the wizard becomes free-form - tabs are directly selectable and
   use the standard green rather than the create wizard's cyan step indicator. */
#jobCreateTabs.tabs-navigable .job-preview-tab {
  cursor: pointer;
  pointer-events: auto;
}
#jobCreateTabs.tabs-navigable .job-preview-tab.active::before {
  background: var(--green);
  box-shadow: var(--glow-active);
}

/* Job stages step 7: the steps in two parts named for the job's stages, Enquiry
   then Plan. Each part's name sits over its steps, quiet until a step in it is the
   one shown; a rule between the parts. */
#jobCreateTabs {
  align-items: flex-end;
  gap: 0.75rem;
}
.job-create-part {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}
.job-create-part + .job-create-part {
  padding-left: 0.75rem;
  border-left: 0.0625rem solid var(--line);
}
.job-create-part-label {
  padding-inline: 0.875rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 650;
}
.job-create-part.is-current .job-create-part-label {
  color: var(--fg-strong);
}
.job-create-part-steps {
  display: flex;
  gap: 0.5rem;
}

/* The questionnaire, under the Enquiry fields: the button and what it does. */
.job-create-questionnaire {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-top: 1rem;
  padding-top: 0.875rem;
  border-top: 0.0625rem solid var(--line);
}
.job-create-questionnaire[hidden] { display: none; }
.job-create-questionnaire .form-hint { margin: 0; }

.job-create-wizard-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.job-create-wizard-actions .wizard-actions-spacer {
  flex: 1;
}

/* The draw-area, risk, and add-client overlays can be opened on top of the Add
   Job modal, so they must stack above it regardless of DOM order (all modals
   share z-index: 1000, where later-in-DOM would otherwise win). */
#jobAreaDrawOverlay,
#plannedProductOverlay,
#jobRiskAssessmentOverlay,
#clientCreateOverlay {
  z-index: 1100;
}

/* Render the native date control (calendar icon + dd/mm/yyyy spin segments)
   in dark mode - otherwise the picker icon is near-black and invisible on the
   dark form background. color-scheme also gives the up/down segment steppers a
   legible dark styling. */
input[type="date"] {
  color-scheme: var(--control-scheme);
}

/* Native checkboxes and radios render at roughly 13px, which at the 85% root
   scale is noticeably smaller than everything else on the row. Sized
   explicitly - `flex: none` so a flex row can't stretch or squash them, and
   `min-height: 0` so the mobile touch-target floor can't turn the square into
   a tall rectangle. The label row carries the hit area. */
input[type="checkbox"],
input[type="radio"] {
  width: 1.25rem;
  height: 1.25rem;
  min-height: 0;
  flex: 0 0 auto;
  /* Excluding these from `.form-field input` also dropped that rule's
     `margin: 0`, which had been suppressing the UA's default checkbox margin. */
  margin: 0;
}

/* The row is the target, not the 17px box: every one of these labels toggles
   its checkbox when clicked, so the comfortable hit area is the whole line. */
.filter-checkbox-label,
.airspace-toggle-row,
label:where(:has(> input[type="checkbox"])),
label:where(:has(> input[type="radio"])) {
  display: flex;
  align-items: center;
  min-height: 1.75rem;
}

#jobAreaDrawOverlay .modal-card {
  max-width: 100rem;
  width: 96vw;
  height: 94vh;
  max-height: 94vh;
  display: flex;
  flex-direction: column;
  /* `auto`, not `hidden`: at ID specificity `hidden` also beat the mobile
     full-screen sheet's scroller, leaving the map and the Save area row
     unreachable on a short viewport. */
  overflow-y: auto;
}

/* The header lives in its own table outside .table-wrap entirely, rather
   than relying on `position: sticky` on <th> inside the scrolling table -
   sticky-in-a-scroll-container has proven unreliable here across the
   surrounding grid/flex layers, whereas a header that's structurally never
   part of the scrolling content can't ever scroll with it. Column widths
   are kept in sync between the two tables via the shared .job-col-* classes. */
.jobs-table-head {
  margin-bottom: 0;
}

.jobs-table-head th {
  background: var(--table-head-bg);
}

/* The list sits in the same dark inset box as the Jobs list on the Flights
   page (.job-filter-list): its background, its border, its corners. The
   header had the dark band and the scrolling rows below it sat straight on
   the card, so the list read as a stripe over nothing. Header and rows are
   two tables (see above), so the box is built from both: the header takes the
   top border and corners, the scroll area the sides, bottom and background.
   Desktop only - on a phone the list runs edge to edge by design. */
html:not(.mobile) .all-jobs-card > .jobs-table-head {
  border-collapse: separate;
  border-spacing: 0;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22); /* = the Flights list's frame; the same colour as --control-border in dark */
  border-bottom: 0;
  border-radius: 0.5rem 0.5rem 0 0;
  overflow: hidden;
}
html:not(.mobile) .all-jobs-card > .table-wrap {
  /* Joined to the header: .table-wrap's general 0.75rem top margin would cut
     the box in two. */
  margin-top: 0;
  background: var(--control-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22); /* = the Flights list's frame; the same colour as --control-border in dark */
  border-top: 0;
  border-radius: 0 0 0.5rem 0.5rem;
}

/* syncJobsTableScrollSpacer sets this to the measured scrollbar gutter so the
   header table's columns land over the body table's. Its own cell padding was
   wider than the gutter, and border-box makes padding a floor on width, so the
   header sat 10.4px left of the columns it labels however exact that
   measurement was. Two classes, not one: `.data-table th` carries a type
   selector and would otherwise out-specify the padding reset. */
.jobs-table .job-col-scroll-spacer {
  width: 0;
  padding: 0;
}

.jobs-table-card #addJobButton {
  margin-top: 1rem;
  width: auto;
  /* Its own width, like buttons everywhere else - a flex column stretches
     its children, which made this a bar the width of the list. Centred under
     the list, and a floor of 9.5rem (about 1.5x the bare label) so it reads as
     the list's main action; rem, so it scales with the UI and stays well
     inside the narrowest card. */
  align-self: center;
  min-width: 9.5rem;
}

/* Only the list flexes. Everything else in the card keeps its own height:
   they are all flex-shrink 1 by default, so once the rows overflowed, the
   Create job button was squeezed along with them - 24.9px of its 27.7 at
   1600 wide, its label pushed against the bottom edge. */
.jobs-table-card > .jobs-table-toolbar,
.jobs-table-card > .jobs-table-head,
.jobs-table-card > .jobs-pagination,
.jobs-table-card > #addJobButton {
  flex: none;
}

.job-row.selected {
  background: rgba(var(--rgb-blue), 0.12);
}

#jobPreviewWeatherAsOf {
  margin: 0.375rem 0 0;
}
/* PHONES: a forecast line is two lines, "Forecast for Wed 25 Nov, midday" over
   "Fetched 29 Oct 2026 · 10:14" (app.js drawJobCardWeatherAsOf writes the pieces).
   At the 16 px root the one line broke inside the date ("Fetched 29" / "Oct 2026")
   or left the middle dot at the end of a line. The dot between the pieces is the
   break now, so it is not drawn; the words in the node are unchanged. The readings
   under it get a gap: the card-copy margin below is zero here. */
html.mobile #jobPreviewWeatherAsOf { margin-bottom: 0.625rem; }
html.mobile #jobPreviewWeatherAsOf .as-of-forecast + .as-of-dot { display: none; }
html.mobile #jobPreviewWeatherAsOf .as-of-forecast ~ .as-of-fetched { display: block; }

/* Job stages step 5: the forecast for the window on the Flight tab (Plan), and the weather a job
   recorded on the job form's old Weather step, read-only beneath it. Headings in the card's
   section style (Planned vs flown, NOTAMs); one quiet row a day; a day outside the flying limits
   reads amber, a warning and never a block. The credit is the source's own, small and muted. */
.job-forecast,
.job-old-weather { margin: 0.875rem 0 0.25rem; }
.job-forecast-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  margin-bottom: 0.375rem;
}
.job-forecast-title,
.job-old-weather-title {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-nav);
}
.job-forecast-meta { font-size: 0.75rem; color: var(--muted); }
.job-forecast-state { margin: 0 0 0.375rem; }
.job-forecast-days { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.job-forecast-day {
  display: grid;
  grid-template-columns: minmax(6.5rem, auto) minmax(0, 1fr);
  gap: 0.125rem 0.75rem;
  align-items: baseline;
  padding: 0.4375rem 0.625rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  color: var(--fg-body);
}
.job-forecast-day .jf-day { font-weight: 600; }
.job-forecast-day .jf-figures { color: var(--fg-copy); font-size: 0.8125rem; overflow-wrap: anywhere; }
.job-forecast-day .jf-over { grid-column: 1 / -1; color: var(--fg-warn); font-size: 0.8125rem; }
.job-forecast-day.is-over { border-color: rgba(var(--rgb-amber), 0.55); }
.job-forecast-credit { margin: 0.375rem 0 0; font-size: 0.75rem; color: var(--muted); }
.job-forecast-credit a { color: inherit; }
.job-old-weather-title { margin-bottom: 0.125rem; }
html.mobile .job-forecast-day { grid-template-columns: minmax(0, 1fr); }

/* Job stages step 5: the conditions measured on site, one row a day under the On-site check's
   Mix; a day with a check and no wind recorded reads amber. */
.job-onsite-conditions-row.is-short,
.job-onsite-site-hazards-row.is-short { border-color: rgba(var(--rgb-amber), 0.55); }
.job-onsite-conditions-row .jo-gap,
.job-onsite-site-hazards-row .jo-gap { color: var(--fg-warn); }

.risk-template-axes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.risk-template-axis-label {
  display: block;
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 0.375rem;
}

.risk-template-axis input {
  margin-bottom: 0.375rem;
}

.risk-template-grid {
  margin-top: 1rem;
}

.risk-template-grid th,
.risk-template-grid td {
  text-align: center;
  vertical-align: middle;
}

.risk-template-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  min-width: 5.625rem;
}

.risk-template-cell input[type="number"],
.risk-template-cell input[type="text"] {
  width: 100%;
  padding: 0.25rem 0.375rem;
  font-size: 0.75rem;
  text-align: center;
}

.risk-template-cell input[type="color"] {
  width: 100%;
  height: 1.375rem;
  padding: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.375rem;
  background: transparent;
}

.risk-cell-clickable {
  width: 2.75rem;
  height: 2rem;
  border-radius: 0.375rem;
  cursor: pointer;
  border: 0.125rem solid transparent;
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--on-risk-2);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto;
}

.risk-cell-clickable:hover {
  filter: brightness(1.12);
}

.risk-cell-clickable.selected {
  border-color: var(--ring-selected);
  box-shadow: var(--ring-selected-cell);
}

/* Risk assessment checklist (per-hazard chance x effect scoring). */
/* Compound selector so these widths beat the later, equally-specific
   `.modal-card { max-width: 640px }` rule in the cascade. */
.modal-card.risk-assessment-card {
  width: 96vw;
  /* Wide enough that the longest effect definition sits on a single line
     rather than wrapping. */
  max-width: 93.75rem;
  /* Fixed height (not max-height) so the modal keeps the same overall size
     when switching between the Hazards and Notes tabs. */
  height: 88vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* The active tab panel fills the space; everything else (header, tabs, message,
   actions) stays a fixed height. */
.modal-card.risk-assessment-card > .risk-assessment-panel {
  flex: 1 1 auto;
  min-height: 0;
}
.modal-card.risk-assessment-card > *:not(.risk-assessment-panel) {
  flex: none;
}

/* Header: the coloured grade square sits inline on the title line (anchored to
   the title, so it doesn't shift as the status message length changes), with
   the status line below. */
.risk-assessment-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}
.risk-assessment-title-line {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* Neutralise the global `.card strong` block display + margin so the square
   sits right beside the title text and stays vertically centred on it.
   Two classes keep this ahead of `.card strong` on specificity. */
.risk-assessment-heading .risk-assessment-title-line strong {
  margin: 0;
  display: inline-block;
}
.risk-grade-dot {
  width: 1.875rem;
  height: 1.875rem;
  border-radius: 0.5rem;
  flex: none;
  background: var(--dot-neutral);
}
.risk-grade-dot[hidden] { display: none; }
.risk-grade-dot.grade-go { background: var(--risk-go); }
.risk-grade-dot.grade-manage { background: var(--risk-manage); }
.risk-grade-dot.grade-nogo { background: var(--risk-nogo); }
.risk-assessment-status {
  font-size: 0.8125rem;
  margin-top: 0.125rem;
}

/* Hazards / Notes tabs inside the modal. */
/* .risk-assessment-tab shares the tab styling grouped with .job-preview-tab. */
.risk-assessment-tabs {
  display: flex;
  gap: 0.5rem;
  margin: 0.25rem 0 0.75rem;
}

.risk-assessment-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.risk-assessment-panel[hidden] { display: none; }
.risk-assessment-panel > .risk-checklist-wrap { flex: 1 1 auto; min-height: 0; }

/* Notes tab: let the textarea fill the panel. */
.risk-assessment-notes-panel .form-field {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* Compound with .form-field so it beats the generic `.form-field textarea`
   rule (same specificity, later in the file), which was halving the authored
   minimum height and putting a drag-resize grip on a flex item. */
.risk-assessment-notes-panel textarea,
.risk-assessment-notes-panel .form-field textarea {
  flex: 1 1 auto;
  min-height: 13.75rem;
  resize: none;
}

/* Grade-coloured outcome line at the top of the risk assessment. */
.risk-grade-go { color: var(--fg-go); font-weight: 600; }
.risk-grade-manage { color: var(--fg-warn-3); font-weight: 600; }
.risk-grade-nogo { color: var(--fg-error-2); font-weight: 600; }

/* Risk matrix editor modal: clickable Go/Manage/No-go grid. */
.modal-card.risk-matrix-modal-card {
  width: 96vw;
  max-width: 72.5rem;
}

/* Risk assessment template editor modal: name + the tall situations editor. */
.modal-card.risk-template-modal-card {
  width: 96vw;
  max-width: 65rem;
  max-height: 90vh;
}
/* Breathing room above the name field and above the hazards editor. */
.risk-template-modal-card .modal-body > .form-field { margin-top: 0.5rem; }
.risk-template-modal-card .modal-body > .risk-entries { margin-top: 0.875rem; }
/* Smaller category labels in the template editor (not the equipment list, which
   reuses the same class elsewhere). */
.risk-template-modal-card .risk-entry-category-heading { font-size: 0.6875rem; }
.risk-template-hazard-warning { color: var(--fg-warn-3); }
.risk-matrix-legend {
  display: flex;
  gap: 0.625rem;
  margin: 0.25rem 0 0.75rem;
}
.risk-grade-chip {
  padding: 0.1875rem 0.75rem;
  border-radius: 62.4375rem;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--on-risk);
}
.risk-grade-chip.grade-go { background: var(--risk-go); }
.risk-grade-chip.grade-manage { background: var(--risk-manage); }
.risk-grade-chip.grade-nogo { background: var(--risk-nogo); color: var(--on-solid); }
/* Reuse the original matrix grid look: dark data-table cells, each a
   .risk-template-cell (score, colour swatch bar, label). Only the swatch is
   coloured, so the grid blends with the app's dark theme like it did before. */
/* Fixed layout with a set label column so the five severity columns are equal
   width - keeps every coloured box the same size. */
.risk-matrix-grid { table-layout: fixed; }
.risk-matrix-grid thead th:first-child,
.risk-matrix-grid tbody th { width: 8.75rem; white-space: nowrap; }
.risk-matrix-corner { font-size: 0.625rem !important; white-space: nowrap; color: var(--muted); }
.risk-matrix-cell { cursor: pointer; user-select: none; padding: 0.25rem !important; }
.risk-matrix-cell:hover .risk-matrix-swatch { filter: brightness(1.08); }
/* Blue, as on the score segments and the dialog's panels: the matrix grades in green (Go), amber and red,
   and a ring in the green would read as a grade. */
.risk-matrix-cell:focus { outline: var(--keyboard-ring-width) solid var(--blue); outline-offset: var(--keyboard-ring-inset); }
/* Filled square holding the score + grade, so the two read as one block. */
.risk-matrix-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.0625rem;
  height: 3.25rem;
  padding: 0.3125rem 0.25rem;
  border-radius: 0.5rem;
  border: 0.0625rem solid var(--swatch-border);
  transition: filter 120ms ease;
}
.risk-matrix-swatch.grade-go { background: var(--risk-go); }
.risk-matrix-swatch.grade-manage { background: var(--risk-manage); }
.risk-matrix-swatch.grade-nogo { background: var(--risk-nogo); }
.risk-matrix-cell-score { font-size: 1rem; font-weight: 800; color: var(--on-risk); line-height: 1.1; }
.risk-matrix-cell-grade { font-size: 0.5625rem; font-weight: 500; color: var(--on-risk-3); }
.risk-matrix-swatch.grade-nogo .risk-matrix-cell-score,
.risk-matrix-swatch.grade-nogo .risk-matrix-cell-grade,
.risk-matrix-swatch.grade-nogo .risk-matrix-cell-letter { color: var(--on-solid); }
/* The one-letter grade is the phone's; a desktop shows the word. */
.risk-matrix-cell-letter { display: none; font-size: 0.75rem; font-weight: 700; color: var(--on-risk-3); }

.risk-checklist-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.risk-checklist {
  width: 100%;
  table-layout: fixed;
}

/* Reference tables explaining the 1-5 chance and effect scales: side by side on
   a computer, one on top of the other on a phone (where the button above them
   opens and closes both). */
.risk-score-guide {
  margin-bottom: 0.25rem;
}
.risk-score-guide-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
/* The button is a phone's; a computer has the tables on screen all the time. */
.risk-guide-toggle { display: none; }

.risk-score-guide .table-wrap {
  margin-top: 0;
  /* Fallback: if the window is too narrow to show every definition on one
     line, scroll horizontally rather than wrap. */
  overflow-x: auto;
}

.risk-score-guide-table {
  width: 100%;
  /* Fixed layout so the Chance and Effect halves split the modal width evenly
     (the two definition columns share the remaining space equally). */
  table-layout: fixed;
}

.risk-score-guide-table th,
.risk-score-guide-table td {
  font-size: 0.75rem;
  vertical-align: middle;
  white-space: nowrap;
}

.risk-score-guide-table .risk-col-num {
  width: 1.875rem;
  text-align: center;
}

/* The word column (Chance or Effect). */
.risk-score-guide-table th:nth-child(2),
.risk-score-guide-table td:nth-child(2) {
  width: 5.5rem;
}

/* Visual gap between the chance half and the effect half. */
.risk-guide-scale + .risk-guide-scale {
  border-left: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
}

.risk-checklist th,
.risk-checklist td {
  vertical-align: top;
  font-size: 0.75rem;
}

.risk-checklist .risk-col-applies {
  width: 6rem;
  text-align: center;
}
/* "Doesn't apply" is longer than the column and the header may wrap: two short
   lines in the frozen header's height rather than a clipped word. */
.risk-checklist thead th.risk-col-applies {
  white-space: normal;
  line-height: 1.15;
}

.risk-checklist td.risk-col-applies {
  vertical-align: middle;
}

.risk-checklist .risk-col-num {
  width: 4rem;
  text-align: center;
}

.risk-checklist td.risk-col-num {
  vertical-align: middle;
}
/* Chance and Effect: five segments (and the picked word under them) where a
   narrow number box was. */
.risk-checklist .risk-col-score {
  width: 7.75rem;
  text-align: center;
}
.risk-checklist td.risk-col-score {
  vertical-align: middle;
}

/* Fixed column widths (table-layout: fixed) so Consequence/Action get real room
   regardless of content length, and the Hazard column stops hogging space. */
.risk-checklist .risk-col-index {
  width: 2.5rem;
  text-align: center;
  color: var(--muted);
}
.risk-checklist td.risk-col-index { vertical-align: middle; }
.risk-checklist .risk-col-hazard {
  width: 12.5rem;
  font-size: 0.8125rem;
}
.risk-checklist td.risk-col-hazard { vertical-align: middle; }
.risk-checklist .risk-col-detail {
  width: 17rem;
  color: var(--muted);
}

/* Let long text wrap rather than force the table wider than the modal
   (data-table defaults to nowrap); keep the column headers on one line so the
   sticky header stays a predictable height. */
.risk-checklist td { white-space: normal; word-break: break-word; }
.risk-checklist thead th { white-space: nowrap; }

/* Freeze the column headers at the top of the scrolling list. */
.risk-checklist thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  height: 2.125rem;
  background: var(--surface-raised);
  box-shadow: inset 0 -0.0625rem 0 rgba(var(--rgb-slate), 0.22);
}

/* Consequence/action are user-filled per assessment, so they're editable. */
/* Checklist inputs live inside table cells, not .form-field, so give them
   the standard input surface (guide §2/§6) explicitly. */
.risk-checklist .risk-detail-input {
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  outline: none;
}
.risk-checklist .risk-detail-input:focus-visible {
  border-color: var(--focus-ring);
}
.risk-checklist .risk-detail-input {
  width: 100%;
  min-width: 15rem;
  padding: 0.375rem 0.5rem;
  font-family: inherit;
  font-size: 0.8125rem;
  line-height: 1.45;
  resize: none;
}

/* Category labels stick just below the frozen header, each staying pinned until
   the next category scrolls up and takes its place. Opaque background so the
   scrolling rows never show through. */
.risk-checklist-category th {
  position: sticky;
  /* 2px under the 34px header so it tucks beneath it (header has the higher
     z-index) — avoids the hairline gap that let rows ghost through. */
  top: 2rem;
  z-index: 2;
  background: var(--surface-raised-2);
  box-shadow: inset 0 -0.0625rem 0 rgba(var(--rgb-slate), 0.22);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--muted);
}

.risk-checklist-row.risk-row-excluded {
  opacity: 0.4;
}
/* A dismissed hazard that was asked for a reason keeps that reason at full strength: the
   row's own dimming moves to its cells, and the hazard cell dims its name alone. */
.risk-checklist-row.risk-row-excluded:has(.risk-reason-input:not([hidden])) { opacity: 1; }
.risk-checklist-row.risk-row-excluded:has(.risk-reason-input:not([hidden])) > td:not(.risk-col-hazard) { opacity: 0.4; }
.risk-checklist-row.risk-row-excluded:has(.risk-reason-input:not([hidden])) .risk-hazard-name { opacity: 0.6; }

/* A hazard nobody has decided on. Marked by an accent down its left edge AND a
   tag in words (.risk-unassessed-tag), never by colour alone, and by an empty
   dashed risk cell in place of a graded one: every row starts like this, and the
   assessment is not finished until none does. */
.risk-checklist-row.risk-row-unassessed {
  background: rgba(var(--rgb-amber), 0.07);
}
.risk-checklist-row.risk-row-unassessed > td:first-child {
  box-shadow: inset 0.25rem 0 0 var(--fg-warn);
}
.risk-unassessed-tag {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.0625rem 0.375rem;
  border-radius: 0.25rem;
  border: 0.0625rem solid var(--fg-warn);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--fg-warn);
  white-space: nowrap;
}
.risk-unassessed-tag[hidden] { display: none; }
.risk-value-cell.risk-value-empty {
  border: 0.0625rem dashed var(--line);
  background: transparent;
  color: var(--muted);
}

/* The way to the next hazard still to do, on the tab row: how far the
   assessment has got is the header's status line, so nothing here costs the
   list a row of height. Not offered on the Notes tab, nor once nothing is left. */
.risk-assessment-tabs { align-items: center; flex-wrap: wrap; }
.risk-assessment-tabs .risk-assessment-next { margin-left: auto; }
.risk-assessment-tabs .risk-assessment-next[hidden],
.risk-assessment-tabs[data-tab="notes"] .risk-assessment-next { display: none; }
/* "Mark the rest doesn't apply" sits beside Next unassessed (which keeps the far
   edge), and goes the same way on the Notes tab and once nothing is left to clear. */
.risk-assessment-tabs .risk-assessment-bulk[hidden],
.risk-assessment-tabs[data-tab="notes"] .risk-assessment-bulk { display: none; }
.risk-assessment-tabs .risk-assessment-next:not([hidden]) ~ .risk-assessment-bulk { margin-left: 0; }
.risk-assessment-tabs .risk-assessment-next[hidden] ~ .risk-assessment-bulk { margin-left: auto; }

/* The tabs sit in their own row so the tab roles hold only tabs; Next unassessed
   stays beside them. */
.risk-assessment-tablist { display: flex; gap: 0.5rem; }
/* The panels are tab stops of their own (the tabpanel pattern the app's other tab rows
   keep), so they show where focus is. */
.risk-assessment-panel:focus { outline: var(--keyboard-ring-width) solid var(--blue); outline-offset: var(--keyboard-ring-inset); }

/* Add-job modal: saved risk assessment result (coloured square + grade line),
   shown in place of the template picker once an assessment is completed. */
.jobcreate-risk-result {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  /* Breathing room above and below the completed-assessment status. */
  padding: 0.75rem 0;
}
.jobcreate-risk-headline {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-weight: 600;
}
#jobCreateRiskSummary {
  color: var(--muted);
  font-size: 0.8125rem;
}
/* .form-field is display:grid and .jobcreate-risk-result is display:flex, so
   native [hidden] needs an explicit override to actually hide them. */
#jobCreateRiskTemplateField[hidden],
.jobcreate-risk-result[hidden] { display: none; }
.jobcreate-risk-dot {
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 0.375rem;
  flex: none;
  background: var(--dot-neutral);
}
/* Discrete "start again" control below the primary button. */
.jobcreate-risk-reset {
  width: auto;
  margin-top: 0.5rem;
  background: transparent;
  border: 0;
  padding: 0.25rem 0.125rem;
  color: var(--muted);
  font-size: 0.8125rem;
  text-decoration: underline;
  cursor: pointer;
}
.jobcreate-risk-reset:hover { color: var(--fg-bright-2); }

/* Airspace conflict popout (shown when Next is clicked on the Flight step). */
.airspace-conflict-list {
  margin: 0.25rem 0 0.75rem;
  padding-left: 1.25rem;
  color: var(--fg-error);
  font-size: 0.8125rem;
}
.airspace-conflict-list li { margin: 0.1875rem 0; }
#jobCreateAirspaceApprovalNote { font-weight: 600; }
#jobCreateAirspaceApprovalNote.blocking { color: var(--fg-error-2); }
#jobCreateAirspaceApprovalNote.ready { color: var(--fg-success-2); }

.risk-value-cell {
  width: 2.625rem;
  height: 1.75rem;
  border-radius: 0.375rem;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--on-risk-2);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto;
}
/* Dark type on the No-go red: white on it is 3.76:1, this is 4.8:1. */
.risk-value-cell.grade-nogo { color: var(--on-risk); }

/* Hazard catalogue editor (Settings > Risk Templates). */
.hazard-catalogue-table th,
.hazard-catalogue-table td {
  vertical-align: top;
  font-size: 0.75rem;
}

.hazard-catalogue-table .hazard-col-cat {
  width: 11.25rem;
}

.hazard-catalogue-table .hazard-col-actions {
  width: 5.75rem;
}

.hazard-catalogue-table textarea,
.hazard-catalogue-table input[type="text"] {
  width: 100%;
  min-width: 7.5rem;
  font-size: 0.75rem;
  resize: vertical;
}

.hazard-remove-button {
  width: auto;
  padding: 0.375rem 0.75rem;
}

.hazard-catalogue-controls {
  margin: 0.75rem 0;
}

.date-estimated {
  color: var(--muted);
  font-style: italic;
}

#jobsTableBody tr {
  cursor: pointer;
}

#jobsTableBody tr:hover {
  background: rgba(var(--rgb-overlay), 0.045);
}

/* An inactive job reads as inactive by its colours (and the " (inactive)" after its
   name), not by fading the whole row: opacity .5 took every cell, the "!" mark and
   the status pill down with it, to about half strength and under 4.5:1. The name
   steps down to --muted and the number, date and client to --fg-faint, both 5:1 or
   better on the row in either theme and still dimmer than an open job's; the mark and
   the pill keep full strength. `td.` and the id put these above the phone and narrow
   -list colour rules further down, which would otherwise win on source order. */
#jobsTableBody tr.inactive .job-name-text { color: var(--muted); }
#jobsTableBody tr.inactive > td.job-col-number,
#jobsTableBody tr.inactive > td.job-col-date,
#jobsTableBody tr.inactive > td.job-col-client { color: var(--fg-faint); }
/* The row the card is on carries a blue tint, and on it the dark --fg-faint is 4.4:1: its cells
   take the name's colour there. */
#jobsTableBody tr.inactive.selected > td.job-col-number,
#jobsTableBody tr.inactive.selected > td.job-col-date,
#jobsTableBody tr.inactive.selected > td.job-col-client { color: var(--muted); }

.form-message {
  margin: 0.875rem 0 0.25rem;
  font-size: 0.875rem;
}

.form-message.success { color: var(--green); }
.form-message.error { color: var(--danger); }

.form-message[hidden] { display: none; }

.modal-overlay {
  position: fixed;
  inset: 0;
  /* Modals live in the 1000s of the z-ladder (guide §14): above the mobile
     tab bar (60), More sheet (70) and Logs drawers (82/86) - a full-screen
     modal must cover the tab bar, not be painted over by it. The date
     calendar (4000s) still sits above modals. */
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--scrim);
}

.modal-overlay[hidden] { display: none; }

.modal-overlay.popover {
  background: transparent;
  align-items: flex-start;
  justify-content: flex-start;
}

.modal-card {
  width: 100%;
  max-width: 40rem;
  max-height: 82vh;
  overflow-y: auto;
  margin: 0;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}

.modal-close-button {
  width: auto;
  flex: 0 0 auto;
}

/* Every action row is a flex row with a real gap: a bare `.modal-actions` was a
   plain block, so its buttons sat side by side separated only by the collapsed
   whitespace between the two tags. Bottom margin is 0 - the card's own padding
   is the space under the buttons, and 1rem on top of it left every modal in the
   app visibly bottom-heavy. */
.modal-actions {
  display: flex;
  flex-wrap: wrap;
  /* Stretch (the default), so every button in a row shares the row's height
     rather than each sizing to its own label. Note that a stretched flex item's
     height is the line's cross size MINUS its own vertical margins, so a button
     with a margin-top will come out shorter than its neighbours - that, not the
     alignment, is what once made "Save template" sit below "Delete". */
  gap: 0.625rem;
  margin: 1rem 0 0;
}
/* Only the last row leans on the card's padding; an action row with content
   after it (the flight-detail Timeline) still needs its own gap underneath. */
.modal-actions:not(:last-child) {
  margin-bottom: 1rem;
}

/* Confirm dialogs answer a question, and their actions sit bottom-right like
   the generic confirmDialog's (.confirm-dialog-actions) rather than hugging the
   left edge of the card. */
.modal-confirm .modal-actions,
.modal-confirm .modal-actions-row {
  justify-content: flex-end;
}

.modal-actions-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}

/* Content-width, like every other button in the app. `flex: 1` gave the
   five-button quote/invoice row equal narrow columns with labels broken across
   two lines; `flex: 1 1 auto` fixed the labels but still grew a row's only
   VISIBLE button to full width whenever its siblings were `hidden` rather than
   absent, so "Create quote" and "Close job" - the same shape of dialog -
   rendered differently. No grow, no guessing. */
.modal-actions-row button {
  width: auto;
  flex: 0 1 auto;
  white-space: nowrap;
}

.modal-section-title {
  display: block;
  margin-bottom: 0.625rem;
  font-size: 1rem;
}

/* The sheet is a .pinned-actions card now (see that block): its header and its
   Save row stay where they are and the fields, the plan check and the timeline
   scroll between them. It used to scroll as a whole, which carried Close and
   Save out of sight on a laptop-height window. */

#flightDetailOverlay .modal-header strong {
  font-size: 1.0625rem;
}

#flightDetailOverlay .card-copy {
  font-size: 0.8125rem;
}

#flightDetailOverlay .modal-section-title {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

#flightDetailOverlay .stats-grid + .modal-section-title {
  margin-top: 1rem;
}

#passwordSaveButton {
  margin-top: 1rem;
}

#addUserButton {
  margin-top: 1rem;
}

#addClientButton {
  margin-top: 1rem;
}

#aircraftSaveButton {
  margin-top: 1rem;
}

#platformCreateOrgButton {
  margin-top: 1rem;
}

/* Same standalone-action spacing as the create-org button above: separate the
   Send test email button from the recipient field so it isn't cramped. */
#emailTestSendButton {
  margin-top: 1rem;
}


#userDetailDeleteButton {
  margin-top: 1rem;
}

#flightDetailOverlay .stat-tile-value {
  font-size: 0.9375rem;
}

/* How the log measured up against its job's plan (admins only). Natural
   height: the timeline below is the section that takes the spare space. */
.flight-detail-plan-section {
  margin-top: 1rem;
}

.flight-detail-plan-section .queue-list {
  max-height: none;
}

/* The timeline is a fold ("Timeline (38 events)"), closed until asked for. */
.flight-detail-timeline-section {
  margin-top: 1rem;
}

.flight-detail-timeline-section > summary {
  cursor: pointer;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.flight-detail-timeline-section > summary:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: 0.0625rem;
  border-radius: 0.25rem;
}

/* The body scrolls, so the list takes its natural height: a scroll box inside
   a scroll box is a trap on a phone. */
.flight-detail-timeline-section #flightDetailTimeline {
  max-height: none;
  margin-top: 0.625rem;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.625rem;
  margin: 1rem 0 0.25rem;
}

/* Dashboard (landing page) */
/* The rules that collapsed the page title block on this page are gone with the
   title block itself; the Dashboard's own org-branded header below is now the
   only heading any page draws. */
.dashboard-org {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  margin-bottom: 1.125rem;
}
.dashboard-org-logo-wrap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 0.75rem;
  background: rgba(var(--rgb-slate), 0.1);
  overflow: hidden;
  flex: none;
}
.dashboard-org-logo { width: 100%; height: 100%; object-fit: contain; }
.dashboard-org-fallback { font-size: 1.625rem; line-height: 1; }
.dashboard-org-name {
  font-size: 1.5rem;
  font-weight: 750;
  letter-spacing: 0.0125rem;
  color: var(--fg-title);
}
.dashboard-headline {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11.25rem, 1fr));
  gap: 0.75rem;
}
.dashboard-headline .stat-tile { text-align: center; }
.dashboard-headline .stat-tile-value { font-size: 1.625rem; }
/* Every figure in a row of the headline sits on one line, whatever its label does. Each tile,
   a plain one or a link (dashboardTile), takes two of the headline's rows and shares them as a
   subgrid: the labels of a row are one track, as tall as the tallest, and the figures the next,
   so a label that wraps ("Upcoming and under way" on a phone up to 375px wide, two lines beside
   one) lowers every figure in its row together instead of only its own (9.5px apart at 320 to
   375 wide). The tile's own 0.25rem between the label and the figure is kept (the subgrid's
   row-gap overrides the headline's 0.75rem inside the tile; the rows of tiles keep that). A
   browser without subgrid drops that one line: each tile is then a grid of its own, the label
   over the figure, and only a wrapped label puts its row's figures out of line. */
.dashboard-headline > .stat-tile {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  row-gap: 0.25rem;
}
/* Only present while the figures above are unknown or stale, so it must not
   reserve space the rest of the time - it is toggled with [hidden].
   Compound selector so it beats .card-copy, which the element also carries and
   which is declared later in this file: on a specificity tie the later rule
   wins, and it was silently handing this line .card-copy's type size and its
   1rem bottom margin. */
.card-copy.dashboard-load-notice {
  margin: 0.625rem 0 0;
  font-size: 0.875rem;
  text-align: center;
}
/* Two graphs side by side on top; below them Recent occurrences (left) and
   Quick actions (bottom-right). */
/* Make the Dashboard a flex column that fills the (overflow-hidden) page, so
   the occurrences list can flex into the leftover space and scroll internally
   rather than pushing the page off the bottom of the screen. */
/* The page scrolls when the screen is shorter than the least the two lists below the
   charts may be given (three rows each), instead of squeezing them to a sliver; the
   charts give way first (below). The phone's page is already the scroller. */
#dashboardPage { display: flex; flex-direction: column; overflow-y: auto; }
.dashboard-main {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 1rem;
  /* Fills the page and never gives up its content's least height: that is what makes the
     page scroll below it. */
  flex: 1 0 auto;
}
/* Top row: the two doughnuts side by side, full width. */
.dashboard-charts-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: stretch;
  gap: 1rem;
  /* The charts and the lists share what the page has left, down to a ring that still
     reads and to three rows a list, and the charts stop at a size past which a ring is
     only bigger (the card's title lines are the extra on top of the ring). */
  flex: 1 1 0;
  max-height: calc(var(--dash-ring-max) + 6.75rem);
}
/* Bottom row: this-week jobs (left) and recent occurrences (right), filling
   the remaining height. */
.dashboard-bottom-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  align-items: stretch;
  gap: 1rem;
  /* Never less than its cards need: the lists hold three whole rows at the least (their
     own min-height), and nothing here may shrink below that. */
  flex: 1 1 0;
}
/* This-week jobs: header + a scrolling list of the week's scheduled jobs. */
.dashboard-thisweek { display: flex; flex-direction: column; }
.dashboard-thisweek-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.dashboard-thisweek-head .card-copy { margin: 0; }
.dashboard-thisweek-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-top: 0.625rem;
  /* Three whole rows at the least, then it scrolls. */
  flex: 1 1 0;
  min-height: calc(3 * var(--dash-week-row-h) + 2 * 0.375rem);
  overflow-y: auto;
}
.dashboard-thisweek-row {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.625rem;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  cursor: pointer;
  transition: background 0.12s ease;
}
.dashboard-thisweek-row:hover { background: rgba(var(--rgb-slate), 0.1); }
/* One line each, so every row is the same height and three of them are a known height. */
.dashboard-thisweek-body { flex: 1 1 auto; min-width: 0; }
.dashboard-thisweek-name {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dashboard-thisweek-sub {
  margin-top: 0.125rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dashboard-thisweek-when { font-size: 0.75rem; font-weight: 700; color: var(--fg-when); white-space: nowrap; }
/* Quick actions: a full-width band under the graphs with its heading inline to
   the left of the button row. */
.dashboard-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 1.125rem;
}
/* Compound .card.dashboard-actions beats the global `.card strong` margin so
   the heading is truly centred against the taller button row. */
.card.dashboard-actions > .card-title { flex: 0 0 auto; white-space: nowrap; margin: 0; }
.dashboard-occurrences {
  display: flex;
  flex-direction: column;
}
.dashboard-occurrences-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.dashboard-occurrences-head .card-copy { margin: 0; }
@media (max-width: 1000px) {
  .dashboard-charts-row,
  .dashboard-bottom-row { grid-template-columns: minmax(0, 1fr); }
}
.dashboard-occurrence-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.625rem;
  /* Flex into whatever height the card has left and scroll there, but never to less
     than three whole rows. */
  flex: 1 1 0;
  min-height: calc(3 * var(--dash-occ-row-h) + 2 * 0.5rem);
  overflow-y: auto;
}
.dashboard-occurrence {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.625rem;
  padding: 0.625rem 0.75rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  cursor: pointer;
  transition: background 0.12s ease;
}
.dashboard-occurrence:hover { background: rgba(var(--rgb-slate), 0.1); }
.dashboard-occurrence-titlerow {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
}
.dashboard-occurrence-title { font-size: 0.875rem; font-weight: 700; color: var(--fg-body); flex: 0 0 auto; }
.dashboard-occurrence-desc {
  font-size: 0.8125rem;
  color: var(--fg-desc);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.dashboard-occurrence-when { margin-top: 0.1875rem; font-size: 0.75rem; color: var(--fg-faint); }
/* A chart card: its title and subtitle, then one row for the ring and its legend (or, in the
   same row, the words for a chart that has none). The row takes what the card has left and
   never less than its content needs, so a long legend is paid for by the page scrolling, as
   the two lists are, and it is a grid, not a flex column, because a ring that is as wide as
   its row is tall needs that height to be settled before the ring is laid out: in a flex
   column the card with the longest legend, whose height the legend itself makes, got a
   narrow ring beside a wider one. */
.dashboard-chart-aircraft,
.dashboard-chart-pilot {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto minmax(min-content, 1fr);
}
.dashboard-chart-body,
.dashboard-chart-state { grid-row: 3; }
/* The ring and its legend side by side: no less than a ring that still reads and no less than
   the legend needs to show every slice (a legend that is given too little room is a legend
   that loses entries, and a name with its time cut off reads as the wrong figure), and no
   more than a ring that is only bigger. */
.dashboard-chart-body {
  display: flex;
  align-items: stretch;
  gap: 1rem;
  max-height: var(--dash-ring-max);
  margin-top: 0.625rem;
}
.dashboard-chart-body[hidden] { display: none; }
.dashboard-chart-wrap {
  position: relative;
  /* The ring is a square as tall as the body, so the legend gets the rest of the width.
     Chart.js, responsive with maintainAspectRatio:false, follows this box; the canvas is
     laid over it (below), so its own size never decides the box's. */
  flex: 0 1 auto;
  height: 100%;
  aspect-ratio: 1;
  min-height: var(--dash-ring-min);
  max-width: 50%;
}
.dashboard-chart-wrap > canvas { position: absolute; inset: 0; }
/* The legend: one line a slice (colour, name, time), in as many columns as the room has.
   It is a list in the page, so it cannot clip a line the way the canvas's own legend did;
   past the most a body may be it scrolls. */
.dashboard-donut-legend {
  flex: 1 1 0;
  min-width: 0;
  max-height: var(--dash-ring-max);
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  grid-auto-rows: min-content;
  align-content: start;
  align-content: safe center;
  gap: 0.125rem 1rem;
  font-size: 0.875rem;
  color: var(--chart-legend);
}
.dashboard-donut-legend-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  min-height: 0;
  width: auto;
  margin: 0;
  padding: 0.125rem 0.25rem;
  border: 0;
  border-radius: 0.25rem;
  background: none;
  font: inherit;
  color: inherit;
  text-align: start;
}
button.dashboard-donut-legend-item { cursor: pointer; }
button.dashboard-donut-legend-item:hover { background: rgba(var(--rgb-slate), 0.1); filter: none; }
button.dashboard-donut-legend-item:focus { outline: var(--keyboard-ring-width) solid var(--keyboard-ring); outline-offset: var(--keyboard-ring-inset); }
/* A slice taken off the ring (a click on its line, as on Chart.js's legend). */
.dashboard-donut-legend-item.is-off { opacity: 0.55; }
.dashboard-donut-legend-item.is-off .dashboard-donut-legend-name { text-decoration: line-through; }
.dashboard-donut-legend-swatch {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 0.125rem;
  flex: none;
}
.dashboard-donut-legend-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dashboard-donut-legend-time {
  flex: none;
  margin-inline-start: auto;
  font-variant-numeric: tabular-nums;
  color: var(--fg-soft);
}
/* The grid's columns follow how many of the seven buttons this person is offered
   (data-count, from app.js) and how wide the card is, so the rows come out even: all in
   one row where they fit, otherwise two rows with the longer first, never 6 + 1. The card
   is the container (below); this is the narrow answer, which the phone keeps. */
.dashboard-actions-grid {
  display: grid;
  grid-template-columns: repeat(var(--dash-action-cols, 4), minmax(0, 1fr));
  gap: 0.625rem;
  flex: 1;
  min-width: 0;
}
.dashboard-action {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.625rem;
  padding: 0.75rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  border-radius: 0.625rem;
  background: rgba(var(--rgb-slate), 0.06);
  color: var(--fg-body);
  font-size: 0.875rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.dashboard-action:hover {
  background: rgba(var(--rgb-green), 0.12);
  border-color: rgba(var(--rgb-green), 0.4);
}
.dashboard-action svg {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  color: var(--green);
}

.stat-tile {
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  background: var(--tile-bg);
}

.stat-tile-label {
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.stat-tile-value {
  color: var(--text);
  font-size: 1.0625rem;
  font-weight: 700;
  margin-top: 0.25rem;
}

.stat-tile-warning {
  border-color: rgba(var(--rgb-red), 0.4);
  background: rgba(var(--rgb-red), 0.12);
}

.stat-tile-warning .stat-tile-value {
  color: var(--pill-red-fg);
}

.timeline-list {
  max-height: 16.25rem;
  overflow-y: auto;
}

#flightLayerList .queue-item {
  cursor: pointer;
}
/* One line per line, always: a long job name or aircraft ellipsises rather
   than wrapping and making its row taller than the rest. */
#flightLayerList .queue-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The row's identity line - the day, clock time and duration of the flight.
   Brighter and heavier than the aircraft/job line beneath it because it is
   what someone scanning the list is actually looking for. The two meta lines
   sit closer together than a lone meta line does under the file name. */
#flightLayerList .queue-meta-when {
  color: var(--fg-calendar);
}
#flightLayerList .queue-meta + .queue-meta {
  margin-top: 0.125rem;
}

/* The name's line: the name, then its "needs review" pill beside it, then a
   loading / failed status at the far end. Tall enough for the pill whether or
   not a row has one, so a row with the pill is the same height as one
   without; everything in it is one line. */
.flight-row-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 1.25rem;
  min-width: 0;
}
.flight-row-title .queue-name {
  flex: 0 1 auto;
  min-width: 0;
}
#flightLayerList .flight-row-title .status-needs_review {
  flex: none;
  padding: 0.125rem 0.4375rem;
  line-height: 1.2;
}
.flight-row-status {
  flex: 0 1 auto;
  min-width: 0;
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flight-row-status.is-failed {
  color: var(--fg-error);
}

.flight-filters {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.25rem 0 0.75rem;
  flex: 1;
  min-height: 0;
}
/* On a computer Clear filters ends the Jobs card at the card's own padding,
   as its title starts it: the extra 0.75rem below was height the Jobs list
   can use (Josh, 2026-11-02). */
html:not(.mobile) .filters-card .flight-filters { margin-bottom: 0; }

/* Selects are absent on purpose: they take the shared dropdown metrics (see
   DROPDOWN CONTROLS), and the inputs here match them so the whole filter
   column is one size. */
.flight-filters input:where(:not([type="checkbox"]):not([type="radio"])) {
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  outline: none;
  margin: 0;
}

/* ux:DEC2 - the Flights filter boxes had no focus mark at all (outline: none above, nothing in its place). */
.flight-filters input:where(:not([type="checkbox"]):not([type="radio"])):focus {
  border-color: var(--focus-ring);
}

.flight-filters button {
  width: auto;
}
/* ...except the client picker's trigger, which is a dropdown that happens to
   be a button: it fills the column like the aircraft and pilot pickers above
   and below it. Without this it shrank to its label - a stub between two
   full-width controls. */
.flight-filters .searchable-select-trigger {
  width: 100%;
}

/* The "flown between" pair. Two equal columns of the app's standard date
   field, which is a hidden native input plus a trigger button (see
   setupDateSpinners); the button stretches to the column on its own, so this
   only has to own the columns and their small From/To captions. */
.flight-filter-dates {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
}
.flight-filter-date {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
  color: var(--muted);
  font-size: 0.75rem;
}

.flight-filter-label {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: -0.25rem;
}

/* On a computer the Jobs list fills the column (Josh, 2026-11-02: "expand
   the jobs list down to fill the space"): Needs review, Clear filters, the
   Summary figures and Upload logs below it keep their natural heights, and
   this box takes every pixel the column has left - whatever the job count, so
   one job, fifty or a "No jobs" line all get the same box.
   Its floor, 12.5rem, is just under four job rows (a row is two lines, about
   3.1rem): three whole and most of a fourth, so even a short window shows a
   list, and that it scrolls. It is as tall as the floor can be while Upload
   logs stays whole on screen down to a 700px window and the Summary keeps its
   strip down to 750 (UX round U7a): below the floor the Summary gives way as
   updateSummaryCardVisibility decides, card to strip to nothing, and past that
   the column scrolls as a whole (.filters-column), so nothing is ever cut off.
   A taller floor pushed Upload logs below the fold on laptop-height windows.
   It used to be 8rem, about two and a half rows. Not on a phone, where the
   list is never shown (the jobs sheet picks the job). */
html:not(.mobile) .filters-card .job-filter-list {
  flex: 1 1 0;
  min-height: 12.5rem;
  max-height: none;
}

.job-filter-list {
  box-sizing: border-box;
  /* Its height is the Jobs card's to give (above): the box scrolls whatever
     it is not given room to show. */
  overflow-y: auto;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  border-radius: 0.5rem;
  background: var(--control-bg);
  padding: 0.25rem;
  /* Matches the data-table row size. Options inherit this. */
  font-size: 0.9375rem;
}

.job-filter-option {
  padding: 0.375rem 0.5rem;
  border-radius: 0.375rem;
  /* No explicit font-size - inherits the same size as the other filter
     selects (which also just inherit rather than setting their own). */
  color: var(--fg-label);
  cursor: pointer;
  user-select: none;
}

.job-filter-option:hover {
  background: rgba(var(--rgb-overlay), 0.055);
}

/* Click-to-toggle multi-select, highlighted to match the app's own accent
   (green) instead of a native <select>'s default OS-blue selection color. */
.job-filter-option.selected {
  background: rgba(var(--rgb-green), 0.16);
  color: var(--fg-on-green-tint);
}

.job-filter-option.selected:hover {
  background: rgba(var(--rgb-green), 0.24);
}

/* The same inactive job in the Flights page's Jobs list: coloured, not faded (see
   #jobsTableBody tr.inactive). Not when it is ticked: the green tint carries its own
   text colours, which have to stay. */
.job-filter-option.inactive:not(.selected) .job-filter-option-name { color: var(--muted); }
.job-filter-option.inactive:not(.selected) .job-filter-option-meta { color: var(--fg-faint); }

.filter-checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  /* Body/control size (§3) - otherwise these inherit the 1rem page context. */
  font-size: 0.875rem;
}
/* A sub-option that reads as belonging to the checkbox above it. */
/* Checkbox width (1.25rem) + .filter-checkbox-label's 0.375rem gap, so a
   sub-option lines up with the text of the option above it. */
.filter-checkbox-nested { margin-left: 1.625rem; }
.filter-checkbox-nested input:disabled + * { opacity: 0.5; }
.filter-checkbox-label:has(input:disabled) { opacity: 0.55; }

/* Spacing only - sizing comes from the shared `input[type="checkbox"]` rule.
   This used to carry `width: auto` to beat `.form-field input { width: 100% }`;
   that rule now excludes checkboxes, and the leftover declaration was winning
   on specificity and dropping the box back to its native ~13px. */
.filter-checkbox-label input {
  margin: 0;
}

aside {
  min-width: 0;
  min-height: 0;
  padding: 0;
  background: transparent;
  border: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.map-shell {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-radius: 0.5rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  background: var(--map-bg);
  box-shadow: var(--shadow-map);
}

#map { height: 100%; width: 100%; }

/* Never wider than the map less 3.5rem either side - the right-hand control
   column (zoom, compass, locate) takes about 2.9rem with its margin, so this
   leaves half a rem of air beside it, and centring makes the gap symmetrical. Past that it wraps onto a second row rather than spilling out
   of the map shell, which clipped it and covered the zoom buttons at 1030 -
   1100 px with the jobs list open (the map ~445 px, the bar ~537 px).
   width: max-content is load-bearing: an absolutely positioned box at
   left: 50% only has half the map to shrink-to-fit into, so without it the bar
   would wrap at half width even when it fits. */
.map-context-bar {
  position: absolute;
  z-index: 5;
  top: 1.125rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  width: max-content;
  max-width: calc(100% - 7rem);
  box-sizing: border-box;
  gap: 0 0.5rem;
  padding: 0;
  border-radius: 0.5rem;
  background: var(--map-float-bg-2);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  color: var(--text);
  box-shadow: var(--shadow-map-float);
  backdrop-filter: var(--backdrop-float);
  font-size: 0.875rem;
  /* NO overflow: hidden. It was here to clip the map-type segment's fill to the
     bar's rounded corner, and it also clipped that segment's open option list to
     the height of the bar - which is exactly nothing, because the bar is one
     control tall. (A `position: fixed` panel does not escape it either: the
     bar's own translateX makes it the containing block for one.) The segment
     rounds its own corners below instead. */
}

.map-context-bar label {
  white-space: nowrap;
  display: flex;
  align-items: center;
  /* Was 0.25rem plus the UA's own checkbox margin; that margin is zeroed
     app-wide now, so the gap carries the whole separation. */
  gap: 0.5rem;
  color: var(--fg-control-2);
  padding: 0.625rem 0.625rem;
}

/* Map chrome, not a form field: this picker is one segment of the bar that
   floats on the map, so it keeps the bar's fill and its divider instead of a
   box of its own. The arrow, the type size and the hover are the shared
   dropdown ones, so it still reads as the same control. It is the map type
   picker, whose panel is drawn by enhanceSelect - hence the trigger here, and
   the wrapper below it, which is what stands in the bar's flex row now. */
.map-context-bar > .searchable-select {
  width: auto;
  flex: 0 0 auto;
}
.map-context-bar select,
.map-context-bar .searchable-select-trigger {
  width: auto;
  min-width: 7.75rem;
  padding: 0.6875rem 0.75rem;
  padding-right: 1.75rem;
  border: 0;
  border-right: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  /* Its own left corners, because the bar cannot clip them any more (see
     .map-context-bar): this is the bar's first segment, its fill is not the
     bar's, and a square corner would show outside the bar's rounded left edge.
     0.4375rem is the bar's INNER radius - its 0.5rem less its own hairline
     border - so the two curves sit on each other. */
  border-radius: 0.4375rem 0 0 0.4375rem;
  background-color: var(--map-control-bg);
  background-position: right 0.75rem center;
  color: var(--fg-strong);
}
/* The shared hover lifts the border colour; here the only border is the
   divider between segments, which must not flicker as the pointer crosses. */
.map-context-bar select:hover:not(:disabled),
.map-context-bar .searchable-select-trigger:hover:not(:disabled) {
  border-color: rgba(var(--rgb-slate), 0.16);
}
/* The open list is the app's popover, not map chrome: it takes the panel style
   from DROPDOWN CONTROLS, and only has to clear the bar it hangs from. */
.map-context-bar .searchable-select-panel {
  --dropdown-panel-gap: 0.375rem;
}
/* ...and while it is open, the bar is the topmost thing on the map. Both maps
   draw a centred loading notice over themselves (.map-loading-badge z-index 9,
   .map-loading-overlay 10) and either would paint over an option list on a short
   phone map. Only while open, so nothing else about the map's layering moves. */
.map-context-bar:has(.searchable-select.open),
.map-legend-card:has(.searchable-select.open) {
  z-index: 11;
}

.map-context-menu {
  position: fixed;
  /* Context menus sit above all page chrome (mobile header 20 / tab bar 60 /
     drawers 86) but below modal overlays (1000) - guide §14. */
  z-index: 900;
  display: none;
  min-width: 11.875rem;
  padding: 0.375rem;
  border-radius: 0.75rem;
  background: var(--menu-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  box-shadow: var(--shadow-menu);
}

.map-context-menu.open { display: grid; gap: 0.25rem; }

.map-context-menu button {
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--text);
  border-radius: 0.5rem;
  padding: 0.5rem 0.625rem;
  font-weight: 650;
}

.map-context-menu .danger-button {
  color: var(--fg-error);
}

.map-context-menu .danger-button:hover {
  background: rgba(var(--rgb-red-deep), 0.18);
}

.map-context-menu button:hover { background: rgba(var(--rgb-green), 0.14); }

.map-page-layout {
  height: 100%;
  display: flex;
}

#airspaceMapShell {
  flex: 1;
  min-width: 0;
  min-height: 0;
}

#airspaceMap { height: 100%; width: 100%; }

.map-loading-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.875rem;
  background: var(--map-bg);
  color: var(--fg-label);
  font-size: 0.875rem;
  transition: opacity 0.25s ease;
}

/* Kept in the DOM (not display:none) during the fade so the reveal is a
   smooth cross-fade to the fully-rendered map rather than a hard cut. */
.map-loading-overlay.hidden {
  opacity: 0;
  pointer-events: none;
}

/* ux:k1 - the cover carrying "Airspace not loaded" and Retry. The spinner goes
   (nothing is loading), the message reads as a message, and the cover stays
   until the load works: never the bare map. */
.map-loading-overlay.is-failed .map-loading-spinner { display: none; }
.map-loading-overlay.is-failed .map-loading-text {
  color: var(--fg-error);
  font-weight: 650;
}
.map-loading-overlay .airspace-load-retry[hidden] { display: none; }

/* Non-blocking progress for streaming flight tracks. The airspace map's
   .map-loading-overlay is opaque and inset:0 because there is nothing to see
   until it finishes; this one sits over a map that is already drawing, and the
   view re-fits after every batch, so it must not cover or capture anything. */
/* Centred in the map and big enough to read across the room.
   It used to be a small pill tucked under the top edge, where it competed with
   the layers control and, on a day's flying, sat there counting to 342 in text
   smaller than the legend beside it. Loading a few hundred tracks is the
   longest wait this page has; the thing that says how far through it is should
   be the thing you can see.
   Still pointer-events: none - the map re-fits as each batch lands and panning
   and zooming stay live underneath, which is the whole reason this is a badge
   and not the full-cover overlay the airspace map uses. */
.map-loading-badge {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.875rem 1.375rem;
  border-radius: 0.75rem;
  background: var(--map-badge-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  box-shadow: var(--shadow-badge);
  color: var(--fg-bright);
  font-size: 1.0625rem;
  font-weight: 650;
  white-space: nowrap;
  pointer-events: none;
}
.map-loading-badge .map-loading-spinner {
  width: 1.375rem;
  height: 1.375rem;
  border-width: 0.1875rem;
}
/* The badge lets the pointer through to the map; its Stop button takes it.
   Shown only while the list header's own Stop is out of sight - the list
   collapsed on a desktop, or any phone (the list is a sheet there) - so the
   page never offers two of them side by side. */
.map-loading-badge .map-loading-stop {
  pointer-events: auto;
  /* Centred in whatever height it gets: the phone's touch floor makes it
     taller than its text, which otherwise sits at the top. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  padding: 0.25rem 0.75rem;
  font-size: 0.75rem;
}
.layout:not(.flights-collapsed) .map-loading-stop {
  display: none;
}
html.mobile .layout .map-loading-stop {
  display: inline-flex;
}
/* The phone override that pushed this clear of the header and the layers
   button is gone with the move to the centre: there is nothing at the middle
   of the map to collide with, on either layout. */
@media (prefers-reduced-motion: reduce) {
  .map-loading-badge .map-loading-spinner { animation: none; }
}

.map-loading-spinner {
  width: 2.125rem;
  height: 2.125rem;
  border-radius: 50%;
  border: 0.1875rem solid rgba(var(--rgb-slate), 0.25);
  border-top-color: var(--green);
  animation: map-loading-spin 0.8s linear infinite;
}

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

/* Small inline loading donut (e.g. while auto-fetching weather). */
.loading-donut {
  display: inline-block;
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 50%;
  border: 0.125rem solid rgba(var(--rgb-slate), 0.25);
  border-top-color: var(--green);
  animation: map-loading-spin 0.8s linear infinite;
  flex: none;
}

.weather-loading {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 0.875rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.weather-loading[hidden] { display: none; }

#jobCreateRiskButton {
  margin-top: 0.875rem;
}

.map-legend-card {
  position: absolute;
  z-index: 5;
  top: 1.125rem;
  left: 1.125rem;
  max-width: 17.5rem;
  max-height: calc(100% - 2.25rem);
  overflow-y: auto;
  padding: 0.875rem 1rem;
  border-radius: 0.5rem;
  background: var(--map-float-bg-2);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  color: var(--text);
  box-shadow: var(--shadow-map-float);
  backdrop-filter: var(--backdrop-float);
}

.map-legend-card .card-copy { margin: 0.375rem 0 0; }

/* Pin the airspace legend to a stable width so collapsing a toggle group can't
   reflow/shrink it (the widest expanded state is ~15.8rem). Minimised state is
   excluded so it still shrinks to just the (+) button. */
#airspaceMapLegend:not(.minimized) { width: 16.5rem; box-sizing: border-box; }

.map-legend-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
}

.map-legend-minimize-button {
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  min-height: 0;
  padding: 0;
  border-radius: 0.3125rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.24);
  background: rgba(var(--rgb-overlay), 0.04);
  color: var(--text);
  font-size: 0.9375rem;
  line-height: 1;
  cursor: pointer;
}

.map-legend-minimize-button:hover {
  background: rgba(var(--rgb-overlay), 0.1);
}

.map-legend-card.minimized .map-legend-body {
  display: none;
}

/* When collapsed, strip the card chrome entirely so only the toggle
   (+) button is left floating on the map, not an empty container. */
.map-legend-card.minimized {
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
  backdrop-filter: none;
  overflow: visible;
}

.airspace-chart-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--fg-muted-2);
}

/* Compact map-legend variant: the shared dropdown style at the size the
   legend's other rows are set in, on the legend's own fill. (The field is a
   <label> whose caption is uppercase micro-type, which the control must not
   inherit - hence the three resets at the top.) */
.airspace-chart-field select,
.airspace-chart-field .searchable-select-trigger {
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--fg-neutral);
  background-color: var(--map-control-bg-3);
  border-color: var(--control-border-3);
  border-radius: 0.375rem;
  font-size: 0.8125rem;
  padding: 0.3125rem 1.25rem 0.3125rem 0.4375rem;
  background-position: right 0.4375rem center;
}
/* The legend is 16.5rem wide and scrolls (.map-legend-card), so a panel wider
   than the card would put a sideways scrollbar on the legend. It is capped to
   the field instead, and the option rows ellipsise as the trigger does. */
.airspace-chart-field .searchable-select-panel { max-width: 100%; }

/* The Map type field under the Chart field: the same control, a row's gap
   below it, and the same before the NOTAMs group that follows it. */
.airspace-chart-field + .airspace-chart-field { margin-top: 0.5rem; }
.airspace-chart-field + .airspace-toggle-group { margin-top: 0.5rem; }

/* Which dataset the chart selection actually draws (openAIP / NZ AIP / eVNC).
   It used to mirror the page-header subtitle; that went with the title bar, so
   this - beside the control that changes it - is the only place the fact
   appears. The parent field is an uppercase label; this line is prose. */
.airspace-chart-source {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  line-height: 1.35;
  color: var(--fg-subtle);
}
.airspace-chart-source:empty { display: none; }

/* Why the Chart selector has nothing but "Vector map" in it. Shown only when
   no chart pyramid has been ingested, so an empty control explains itself
   instead of reading as a fault. */
.airspace-chart-note {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  line-height: 1.35;
  color: var(--fg-subtle);
}
.airspace-chart-note[hidden] { display: none; }

/* Separates the vector kind toggles from the Chart selector above them; the
   whole block is hidden when a raster VNC chart is selected. */
#airspaceVectorControls {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgba(var(--rgb-slate), 0.18);
}

.airspace-toggle-list {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  margin-top: 0.125rem;
}

.airspace-toggle-row {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: 0.8125rem;
  color: var(--fg-control-2);
  padding: 0.1875rem 0;
  cursor: pointer;
}

.airspace-toggle-row input[type="checkbox"] {
  margin: 0;
}

/* Rows with no colour swatch reserve its width plus the row gap it would sit
   in, so their labels line up in the same column as every other row's instead
   of starting hard against the checkbox. */
.airspace-toggle-plain > span {
  margin-left: calc(0.75rem + 0.4375rem);
}

/* ux:k1 - the line under a plain toggle ("Outline NOTAM-activated zones"): what
   the switch is for, in the legend's small print, under its label. */
.airspace-toggle-hint {
  margin: 0 0 0.25rem calc(1.25rem + 0.4375rem + 0.75rem + 0.4375rem);
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--fg-subtle);
}

.airspace-swatch {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 0.1875rem;
  flex-shrink: 0;
}

/* Functional toggle groups: a collapsible sub-list with a master (select-all)
   checkbox in its header. */
.airspace-toggle-group {
  margin-top: 0.25rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  padding-top: 0.25rem;
}
.airspace-toggle-group:first-child { margin-top: 0; border-top: none; padding-top: 0; }
.airspace-toggle-group-summary {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--fg-label);
  cursor: pointer;
  padding: 0.25rem 0;
  user-select: none;
  list-style: none;
}
.airspace-toggle-group-summary::-webkit-details-marker { display: none; }
.airspace-toggle-group-summary:hover { color: var(--text); }
.airspace-group-master { margin: 0; cursor: pointer; flex-shrink: 0; }
.airspace-toggle-group-title { flex: 1 1 auto; }
.airspace-toggle-group-caret {
  color: var(--fg-muted-2);
  font-size: 0.6875rem;
  transition: transform 0.15s ease;
}
.airspace-toggle-group:not([open]) .airspace-toggle-group-caret { transform: rotate(-90deg); }
.airspace-toggle-group-items {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  /* Checkbox width (1.25rem) + .airspace-toggle-row's 0.4375rem gap, so group
     members line up with their group title. */
  padding-left: 1.6875rem;
}

.organization-logo-field {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}

/* The preview shows the whole logo, not a cropped corner of it: a wide wordmark
   used to be cut to a meaningless fragment (object-fit: cover into a square).
   It sits on a neutral plate in a wide slot, so a dark logo does not vanish into
   a dark card and a light one into a light card. */
.organization-logo-plate {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 12rem;
  height: 4.75rem;
  padding: 0.375rem;
  border-radius: 0.5rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.3);
  background: rgba(var(--rgb-slate), 0.3);
  overflow: hidden;
}

.organization-logo-preview {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  flex: none;
}

.organization-logo-preview[hidden] { display: none; }

.organization-logo-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.75rem;
  flex: none;
}

.organization-logo-fallback[hidden] { display: none; }

.organization-logo-controls {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.organization-logo-buttons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.organization-logo-controls .card-copy {
  margin: 0;
}

.card {
  background: var(--card-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  border-radius: 0.5rem;
  padding: 1.125rem;
  margin-bottom: 0.75rem;
  box-shadow: var(--shadow-card);
}

.card strong {
  display: block;
  margin-bottom: 0.625rem;
  font-size: 1.25rem;
  line-height: 1.2;
}

/* `.card strong` is the CARD TITLE rule, and it caught every bold word in every
   card and dialog: the Support guide, the privacy notice, the risk matrix
   legend and the email help each rendered their inline bold as a 17px block
   heading on a line of its own. Bold inside running text is inline. Scoped to
   the text containers, so a title that is a direct child of its card is not
   touched, and so are the strong rules that name their own classes (a
   `.parent > strong` heading keeps its look). The extra element or class in each
   selector is what outranks `.card strong`. */
.card p strong,
.card li strong,
.card .card-copy strong,
.card .form-hint strong,
.card .support-body strong {
  display: inline;
  margin: 0;
  font-size: inherit;
  line-height: inherit;
}

/* Every dialog is `.card.modal-card`. The card's own surface is the translucent
   card gradient (93% opaque in the dark theme), which is right on a page and
   wrong for a dialog: the page behind showed through the text. The opaque sheet
   surface already exists for the phone's sheets and the More panel, so the
   dialogs use it. Compound selector, because `.card` is declared after
   `.modal-card` and at equal specificity would win. */
.card.modal-card {
  background: var(--sheet-bg);
}

.card-copy {
  margin: -0.125rem 0 1rem;
  color: var(--fg-copy);
  font-size: 0.9375rem;
}

button {
  /* Buttons size to their content by default (normal HTML behaviour). Full-width
     is opt-in: add `.btn-block`. `.modal-actions` and `.modal-actions-row` lay
     their buttons out in a gapped flex row instead. This is deliberately the
     inverse of the old default (`width: 100%` on every button), which silently
     made any new multi-button row stack full-width and could collapse the
     surrounding layout. */
  /* A floor so a compact chip is still a usable pointer target - several
     row-level buttons landed at 21px once the 85% root scale was applied. */
  min-height: 1.75rem;
  /* Horizontal is 1.8x the vertical. At the old 0.75rem it was 1.2x, which put
     the label almost against the edge - reported on the billing settings page,
     but it was never a billing problem: this is the Standard size and the same
     box was measured on 30 buttons across six pages. The tabs beside it already
     ran 1.6x, so the button was the odd one out. Compact chips (0.25rem
     0.75rem) are unchanged - they are a different size, not a smaller version
     of this one. STYLE_GUIDE.md section 5 carries the same numbers; change both. */
  padding: 0.625rem 1.125rem;
  border: 0;
  border-radius: 0.5rem;
  background: var(--button-bg);
  color: var(--button-fg);
  /* Buttons don't inherit font by default — without this they fall back to the
     UA default (Arial 13.333px), off the Inter stack and off the type scale.
     Emphasis style per §3 (0.9375rem, weight 750). Components that need a
     different size (chips, tabs) set their own font-size and win over this. */
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 750;
  cursor: pointer;
}

/* Opt-in full-width for a standalone button. */
.btn-block { width: 100%; }

/* Opt-in DOUBLE width, for a lone confirming action that content-width leaves
   looking incidental - the two-factor Verify button, which is the only thing on
   that step and was 50.7px in a 273.4px card. 7.5rem is that measured width
   doubled; full-width would have been five times it, which is a different
   design decision and not the one asked for. Same opt-in discipline as
   .btn-block: never widen a bare button. */
/* The sign-in screen's Verify button, and only that: `.btn-wide` is added
   from one place (app.js, the two-factor step) and nowhere else. Since the
   sign-in button became full width (.btn-block, below) the class changes
   nothing there: a full-width button is already wider than 7.5rem. */
.btn-wide { min-width: 7.5rem; }

/* The sign-in card's only action, on both of its steps - "Sign in" and then
   "Verify". The full width of the card (Josh, 2026-11-02: "full width login
   button"), opted into in the markup with .btn-block as button-width-default
   asks; the list below leaves a .btn-block alone. Block so it takes a line of
   its own whatever sits beside it in the form. It was centred at content width
   before (width: fit-content and auto margins); an id-level width here would
   beat .btn-block, so there is none. */
#loginSubmitButton {
  display: block;
}

/* Buttons that predate the content-width default and are pinned to it
   explicitly, so a later container rule can't quietly stretch them again.
   Full-width is opt-in: use `.btn-block`, not this list (on the signed-out
   cards a .btn-block is left out of it, so the opt-in wins there). */
.login-card button:not(.btn-block),
.modal-actions:not(.modal-actions-row) button,
#addJobButton,
#quoteAddButton,
#uploadLogButton,
#flightFiltersClearButton,
.multiselect-dropdown-trigger,
.hazard-actions-head button {
  width: auto;
}

#uploadLogButton {
  margin: 1.25rem 0;
  padding: 1.3125rem 0.75rem;
  background: rgba(var(--rgb-green), 0.16);
  border: 0.0625rem solid rgba(var(--rgb-green), 0.4);
  color: var(--fg-on-green-tint);
  /* Never shrinks and never loses out to the summary card for space - it
     should always be fully visible, not pushed off the bottom of the screen. */
  flex: 0 0 auto;
}

#uploadLogButton:hover {
  background: rgba(var(--rgb-green), 0.26);
  color: var(--fg-on-green-tint-hover);
}

button:hover:not(:disabled) { filter: brightness(1.08); }
button:disabled { background: var(--button-disabled-bg); cursor: not-allowed; }
.secondary-button { background: var(--secondary-bg); }
.danger-button { background: var(--danger-bg); color: var(--pill-red-fg); }

/* Form controls don't inherit font by default (same UA-default trap as
   buttons): pin the Inter stack and the body/control size (§3) app-wide.
   Component rules that need another size (compact filters, chips) override.
   Dropdowns take theirs from the shared block below, with the rest of their
   box. */
input,
textarea {
  font-family: inherit;
  font-size: 0.875rem;
}

/* ---- DROPDOWN CONTROLS ----------------------------------------------------
   Every dropdown in the app draws the same box, closed AND open. There are two
   kinds - the single-choice picker (a button + panel, see enhanceSelect) and
   the multi-select picker (a button + tick list) - and which one a control is
   depends on whether it takes one answer or several, never on how it should
   look. Whether a single-choice picker has a search box in its panel depends on
   what it lists (WHICH DROPDOWNS SEARCH in app.js); a person should not be able
   to tell by looking.

   NO DROPDOWN IN THE APP IS A NATIVE <select> ANY MORE, and that is the whole
   point of this block. A native select can be styled down to the arrow, and for
   a while most of them were - but clicking one still opens the operating
   system's own option list, which no stylesheet reaches, so half of each
   control went on looking like the browser's while the other half looked like
   ours. `select` stays in these selectors all the same: it costs nothing, it
   keeps a list box (`size`/`multiple`, which is not a dropdown) in the family,
   and if app.js ever fails before it enhances a page the fields still read as
   fields instead of as unstyled browser chrome.

   The metrics are the shared field metrics - the same border, radius, fill and
   0.625rem/0.6875rem padding as a text input and the date trigger - so a
   dropdown lines up with whatever sits beside it in a form. Change them here
   and every dropdown follows; nothing below re-declares them. */
select,
.searchable-select-trigger,
.multiselect-dropdown-trigger {
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background-color: var(--control-bg);
  color: var(--fg-body);
  font-family: inherit;
  font-size: 0.875rem;
  /* Pinned for the same reason as the line box below: both pickers are
     buttons, and `button` sets 750, so without this every searchable dropdown
     showed its value in extra-bold beside a regular-weight native select. */
  font-weight: 400;
  /* Pinned so all three kinds come out the same height: a button, a select and
     a select's inner value box each work out a different "normal" line box. */
  line-height: var(--dropdown-line);
  text-align: left;
  outline: none;
}

/* The arrow, and the room it needs. Not on a list box (`size`/`multiple`):
   that is an open list, not a dropdown, and it has nothing to open. */
select:not([size]):not([multiple]),
.searchable-select-trigger,
.multiselect-dropdown-trigger {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--dropdown-pad-arrow);
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right var(--dropdown-pad-x) center;
  background-size: 0.5rem 0.3125rem;
  cursor: pointer;
}

select:hover:not(:disabled),
.searchable-select-trigger:hover:not(:disabled),
.multiselect-dropdown-trigger:hover:not(:disabled) {
  border-color: var(--control-border-hover);
}

/* Nothing chosen yet reads as placeholder grey on all three. A native select
   shows this state as its empty option, which has no styling hook of its own -
   hence :has(). Where an empty value is a real choice ("All clients" on the
   Plan toolbar) the page says so and takes the grey off both of its pickers. */
select:not([size]):not([multiple]):has(option[value=""]:checked),
.searchable-select-trigger.is-placeholder,
.multiselect-dropdown-summary.multiselect-dropdown-placeholder {
  color: var(--muted);
}

/* The fill is restated because both pickers are buttons, and `button:disabled`
   paints the disabled-button grey: a read-only picker came out a different
   colour from the read-only select beside it on the job card. */
select:disabled,
.searchable-select-trigger:disabled,
.multiselect-dropdown-trigger:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  background-color: var(--control-bg);
}
/* ...and the arrow with it: `button:disabled` sets `background`, a shorthand,
   which wipes the arrow image too, so a disabled picker came out a flat box
   beside a disabled native select that kept its arrow. (The job card, which
   wants no arrow on its read-only fields, outranks this on purpose.) */
.searchable-select-trigger:disabled,
.multiselect-dropdown-trigger:disabled {
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right var(--dropdown-pad-x) center;
  background-size: 0.5rem 0.3125rem;
}

/* Focused, and open, are the same mark on all three - the same one a focused
   text field takes. (Reaching one by keyboard adds the app-wide focus ring on
   top, from `button:focus, select:focus` further down; that
   rule already covers a trigger, because a trigger is a button.) */
select:focus,
.searchable-select-trigger:focus,
.multiselect-dropdown-trigger:focus,
.searchable-select.open .searchable-select-trigger,
.multiselect-dropdown-trigger[aria-expanded="true"] {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
}

/* The one part of a dropdown the app cannot draw is the open option list: the
   browser draws that, and there is no styling for it that does not mean
   replacing the control with a <div> and losing its keyboard behaviour with
   it. What can be said is which theme it is in, so a list opened in the dark
   theme is dark and one opened in the light theme is white. */
select {
  color-scheme: var(--control-scheme);
}

/* A list box (the upload modal's job list) keeps the shared box and drops the
   single-line height with it. */
select[size],
select[multiple] {
  line-height: 1.4;
}

.upload-metadata-card {
  align-self: start;
}

.upload-metadata-form {
  margin-top: 0.875rem;
}

/* Pilot / Client act as filters for the job list below. */
.upload-job-filters {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
  margin-bottom: 0.875rem;
}
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

.form-field {
  display: grid;
  gap: 0.375rem;
  min-width: 0;
  /* Rows hug their content. Without this, a field stretched to match a
     taller neighbour in the same form-grid row (e.g. one with a multi-line
     hint) distributes the surplus across its rows - inflating the label row
     and pushing the control out of line with the neighbour's (§4). */
  align-content: start;
}

.form-field-full {
  grid-column: 1 / -1;
}

/* Keeps a pair of date fields (issue + valid-until / issue + due) side by side
   in their own row, so a hidden neighbour (e.g. the status field on a new
   quote/invoice) can't reflow them onto different rows. */
.billing-date-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.375rem, 1fr));
  gap: 0.75rem;
  align-items: start;
}

.form-field span {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* A unit inside an uppercase label keeps its case: "(m)" is metres and "(M)"
   reads as mega, "(hPa)" as "(HPA)" is no unit at all. The span is nested in
   the label span, so this (0,2,0) beats `.form-field span` (0,1,1); the label
   words stay uppercase. */
.form-field .unit {
  text-transform: none;
  letter-spacing: 0;
}

/* Marks a required-but-empty field (PIC/aircraft/risk assessment - the same
   set jobReadinessIssues() already flags for the "not ready" row badge) so
   it's obvious at a glance inside the panel, not just from that badge. */
.form-field.field-missing span {
  color: var(--fg-warn);
}

/* The pickers too: a searchable dropdown's own <select> is hidden, so marking
   only `select` painted the amber on nothing (Pilot in Command lost it). */
.form-field.field-missing select,
.form-field.field-missing input,
.form-field.field-missing .searchable-select-trigger,
.form-field.field-missing .multiselect-dropdown-trigger {
  border-color: rgba(var(--rgb-amber), 0.55);
}

p.field-missing {
  color: var(--fg-warn);
}

button.field-missing {
  border-color: var(--fg-warn);
  color: var(--fg-warn);
}

/* Checkboxes and radios are excluded - `width: 100%` stretched the four
   service-interval tick boxes in Settings > Aircraft into full-width bars, and
   the guard rule written to stop exactly that (`.filter-checkbox-label input`)
   has the same specificity, so it lost on source order. */
/* The same pinned line box the dropdowns use, so a text field and the picker
   beside it are the same height to the pixel (they were 35 and 36). Textareas
   keep the body's line spacing - they hold prose, not one value. */
.form-field input:where(:not([type="checkbox"]):not([type="radio"])) {
  line-height: var(--dropdown-line);
}

/* Selects are deliberately absent below: they take the same metrics from the
   shared DROPDOWN CONTROLS block, which every dropdown in the app reads, so a
   picker and the text input beside it stay the same size without being
   declared twice. Keep it that way - a `.form-field select` rule here would
   outrank the shared one and only this one form would drift. */
.form-field input:where(:not([type="checkbox"]):not([type="radio"])),
.form-field textarea {
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  /* Body/control size (§3) - bare `font: inherit` picks up the 1rem page
     context and drifts off the scale. */
  font-size: 0.875rem;
  outline: none;
  margin: 0;
}

.form-field input[type="range"] {
  padding: 0;
  border: none;
  background: transparent;
  height: 1.5rem;
}

/* Theme the native file picker so its button matches the app instead of the
   browser's default grey "Choose File" chrome. */
.form-field input[type="file"] {
  padding: 0.375rem 0.5rem;
  cursor: pointer;
  color: var(--muted);
}
.form-field input[type="file"]::file-selector-button {
  margin-right: 0.6875rem;
  border: 0.0625rem solid var(--control-border-4);
  border-radius: 0.375rem;
  background: var(--secondary-bg);
  color: var(--fg-body);
  padding: 0.4375rem 0.875rem;
  font: inherit;
  font-weight: 650;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.form-field input[type="file"]::file-selector-button:hover {
  background: var(--secondary-bg-hover);
  border-color: var(--control-border-hover-2);
}

/* Custom date picker - replaces the native date input. A trigger showing the
   value opens a calendar popover (month grid, month/year picker, "Today"); a
   field carrying a time gets two typed segments under the grid. Optional dates
   hide behind an "Add date" toggle. */
.date-field { display: flex; flex-direction: column; gap: 0.5rem; }
.date-optional-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  align-self: flex-start;
  font-size: 0.8125rem;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
}
.date-optional-toggle input {
  margin: 0;
  accent-color: var(--green);
  cursor: pointer;
}
/* Date field: a trigger button showing the value, opening a calendar popover. */
.date-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.625rem 0.75rem;
  font: inherit;
  /* Matches the form controls it sits among (§3), down to the pinned line box
     that fixes every field control at one height. */
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  font-weight: 650;
  cursor: pointer;
  text-align: left;
}
.date-trigger:hover:not(.is-disabled) { border-color: var(--control-border-hover); }
.date-trigger:focus-visible {
  border-color: var(--focus-ring);
}
/* The trigger's value span sits inside a .form-field label, whose generic
   `span` rule uppercases field LABELS - undo all of that here so the value
   reads "23 Jul 2026", not "23 JUL 2026" (guide §15 display format). */
.date-trigger .date-trigger-text {
  text-transform: none;
  letter-spacing: normal;
  font-size: inherit;
  font-weight: inherit;
  color: inherit;
}
.date-trigger.is-empty .date-trigger-text { color: var(--muted); font-weight: 500; }
.date-trigger.is-disabled { opacity: 0.65; cursor: default; }
.date-trigger-icon { display: inline-flex; width: 1.125rem; height: 1.125rem; color: var(--muted); flex: none; }
.date-trigger-icon svg { width: 100%; height: 100%; }

.date-cal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: var(--scrim-4);
}
.date-cal {
  position: fixed;
  z-index: 4001;
  width: 17.5rem;
  box-sizing: border-box;
  padding: 0.75rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  border-radius: 0.75rem;
  background: var(--popover-bg);
  box-shadow: var(--shadow-calendar);
}
/* Mobile: a full-width bottom sheet (like the More/Flights drawers) instead
   of a floating centred popover - always fits the screen, keyboard or not,
   and caps its height with internal scroll on short viewports. */
.date-cal-mobile {
  left: var(--safe-left) !important;
  right: var(--safe-right) !important;
  top: auto !important;
  bottom: 0 !important;
  transform: none;
  width: auto;
  /* 34rem (462px) was enough for a date-only sheet - measured at 436.6px - but
     a field carrying a time adds a 62px row, and the whole 498.6px sheet then
     scrolled inside the cap with Now / Clear / Done pushed 23px past the bottom
     of a 390x844 phone: unreachable, so the time could not be confirmed at all.
     38rem clears it with room to spare, and the sticky footer below covers the
     short viewports (landscape, a small Android) where it still has to scroll. */
  max-height: min(85dvh, 38rem);
  overflow-y: auto;
  border-radius: 1rem 1rem 0 0;
  border-bottom: 0;
  padding: 0.875rem 1rem calc(1rem + var(--safe-bottom));
  /* Subtle rise only - a full-height translate parks the sheet off-screen
     if the animation ever stalls on its first frame. */
  animation: dateCalRise 0.18s ease;
}
@keyframes dateCalRise {
  from { transform: translateY(0.75rem); opacity: 0.6; }
  to { transform: none; opacity: 1; }
}
/* Inside the full-width sheet, cap the calendar itself so the square day cells
   (aspect-ratio 1) stay a sane size and the footer stays on screen. The cap is
   what sizes a day: seven cells and six 0.125rem gaps share it, so 23.5rem is
   the narrowest cap that puts a cell on the 3.25rem touch floor (it was 21rem,
   which made every day 39.4px). At the 16px phone base 23.5rem is 376px, wider
   than the sheet on any phone under 408px, so there the cap does nothing and a
   day is its column's square (html.mobile .date-cal-day, Touch targets). The
   class is only ever added on mobile (see the isMobile() branch in the date
   picker), so no desktop popover is affected. */
/* Done is how a date is confirmed, so it rides the bottom of the sheet rather
   than the bottom of the content: when the sheet does scroll, the row scrolling
   out of reach leaves no way to finish. */
.date-cal-mobile .date-cal-foot {
  position: sticky;
  bottom: 0;
  background: var(--popover-bg);
}
.date-cal-mobile > * {
  max-width: 23.5rem;
  margin-left: auto;
  margin-right: auto;
}
.date-cal-head {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  margin-bottom: 0.5rem;
}
.date-cal-nav {
  width: 2rem;
  height: 2rem;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  color: var(--fg-soft);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
}
.date-cal-nav:hover { background: rgba(var(--rgb-slate), 0.14); color: var(--fg-strong); }
.date-cal-title {
  flex: 1;
  text-align: center;
  border: 0;
  background: none;
  color: var(--fg-title-2);
  font-size: 0.9375rem;
  font-weight: 700;
  cursor: pointer;
  padding: 0.25rem;
  border-radius: 0.375rem;
}
.date-cal-title:hover { background: rgba(var(--rgb-slate), 0.1); }
/* minmax(0, 1fr), not 1fr. A plain 1fr track is minmax(auto, 1fr) and cannot be
   squeezed below its content's min-content width, so anything that widens a day
   cell silently widens the whole grid past the popover instead of the cells
   getting smaller. That is what happened when bare buttons gained a default
   padding: a day cell inherited 15.3px of it either side, its min-content went
   to 43.8px, and seven of those came to 307px inside a 215.6px box - the last
   two columns of every month rendered outside the calendar. */
.date-cal-weekdays,
.date-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  /* The same gap on both, or the two grids divide the same width differently
     and MON..SUN stop sitting over the columns they label - by the Sunday
     column the drift is most of a cell. */
  gap: 0.125rem;
}
.date-cal-weekdays { margin-bottom: 0.25rem; }
.date-cal-weekdays span {
  text-align: center;
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--fg-faint);
  padding: 0.25rem 0;
}
.date-cal-day {
  aspect-ratio: 1 / 1;
  /* A cell in a seven-across grid, not a button with a label. It sizes itself
     from the track, so the default button padding has nothing to do here and
     was what pushed the grid over its container. */
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 0.5rem;
  background: none;
  color: var(--fg-calendar);
  font-size: 0.8125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.date-cal-day:hover:not(.is-selected) { background: rgba(var(--rgb-slate), 0.14); }
.date-cal-day.is-other { color: var(--fg-disabled); }
.date-cal-day.is-today { box-shadow: inset 0 0 0 0.0625rem rgba(var(--rgb-green), 0.6); }
.date-cal-day.is-selected { background: var(--green); color: var(--on-green); }
.date-cal-day:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: 0.0625rem;
}
.date-cal-months {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.375rem;
  padding: 0.25rem 0;
}
.date-cal-month {
  padding: 0.5rem 0;
  border: 0;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
  color: var(--fg-calendar);
  font-size: 0.8125rem;
  font-weight: 650;
  cursor: pointer;
}
.date-cal-month:hover { background: rgba(var(--rgb-slate), 0.16); }
.date-cal-month.is-selected { background: var(--green); color: var(--on-green); }
.date-cal-time {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}
.date-cal-time-label {
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03rem;
  color: var(--fg-faint);
  margin-right: auto;
}
.date-cal-time-colon { color: var(--fg-faint); font-weight: 700; }
/* Two typed segments instead of a pair of +1/-1 steppers - the time is entered
   in four keystrokes whatever it is, rather than one per minute travelled. */
.date-cal-timeseg {
  width: 2.75rem;
  min-width: 0;
  box-sizing: border-box;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 0.9375rem;
  color: var(--fg-body);
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  padding: 0.3125rem 0.25rem;
}
.date-cal-timeseg:focus {
  outline: none;
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.16);
}
/* Time mode has no month to page through, so the date being edited takes the
   whole header and doubles as the way back to the grid. */
.date-cal-title-wide { flex: 1; }
.date-cal-keypad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.375rem;
  margin-top: 0.625rem;
}
.date-cal-key {
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  background: rgba(var(--rgb-slate), 0.08);
  color: var(--fg-body);
  border-radius: 0.5rem;
  font-size: 1.125rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.date-cal-key:hover { background: rgba(var(--rgb-slate), 0.18); }
.date-cal-key:active { background: rgba(var(--rgb-green), 0.22); }
.date-cal-key-wide { grid-column: span 2; }
.date-cal-foot {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}
.date-cal-action {
  border: 0;
  background: none;
  color: var(--green);
  font-size: 0.8125rem;
  font-weight: 650;
  cursor: pointer;
  padding: 0.25rem;
}
.date-cal-action.date-cal-clear { color: var(--muted); }
.date-cal-action.date-cal-done { margin-left: auto; color: var(--fg-calendar); font-weight: 700; }
.date-cal-action:hover { filter: brightness(1.2); }

.form-field textarea {
  resize: vertical;
  min-height: 6.875rem;
}

/* Job picker with a jump-to-job button: the searchable select flexes to fill
   and the ">" button sits flush beside it. */
.job-select-row {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
}
.job-select-row .searchable-select { flex: 1 1 auto; min-width: 0; }
.job-goto-btn {
  flex: 0 0 auto;
  width: 2.625rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.job-goto-btn:hover:not(:disabled) { border-color: rgba(var(--rgb-green), 0.55); color: var(--green); }
.job-goto-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Open job beside Flight details' Job picker (review 2026-11-07, item 24): a word, not the ">" of
   the Billing pickers, and as tall as the picker beside it. */
.job-select-row > .secondary-button { flex: 0 0 auto; margin: 0; align-self: stretch; }

/* Upload complete: one row per job the batch landed on, each with Open job (item 24). The
   name gives way when the row is narrow; the button never does. */
.upload-complete-jobs {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.upload-complete-jobs[hidden] { display: none; }
.upload-complete-job {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.upload-complete-job-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  font-size: 0.875rem;
}
.upload-complete-job > .secondary-button { flex: 0 0 auto; margin: 0; }

/* Type-to-search combobox (job / client pickers). Native <select> hidden;
   trigger + filtered panel overlaid. */
/* min-width: 0 because the wrapper is a grid or flex item nearly everywhere it
   is used, and an item's automatic minimum size is its content's min-content
   width - which for a one-line, never-wrapping trigger is the whole value. A
   client called "Waimakariri District Council Rural Water Race Maintenance
   Contract 2026/27" then widened the form column it sits in and drew straight
   over the field beside it. */
.searchable-select { position: relative; min-width: 0; }
.searchable-select-native { display: none; }
/* Box, arrow, hover, focus and disabled all come from the shared DROPDOWN
   CONTROLS block - this trigger must be indistinguishable from a native
   <select>. Only what is specific to a button showing one line of value
   belongs here. */
.searchable-select-trigger {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.searchable-select-trigger.is-placeholder { color: var(--muted); }
/* At least as wide as its trigger and as wide as its longest option, up to a
   limit - it was pinned to the trigger's width, so a narrow trigger cut every
   client name to "Aircr..." and the search box's own placeholder to "Type to
   se...". Opening flips it to the trigger's right edge when it would run off
   the screen (enhanceSelectWithSearch). */
.searchable-select-panel {
  position: absolute;
  z-index: 60;
  /* How far the panel clears the trigger. Named, because the panel hangs from
     the trigger's bottom edge most of the time and from its top edge when it
     opens upwards (.drops-up), and the two must clear it by the same amount. */
  --dropdown-panel-gap: 0.25rem;
  top: calc(100% + var(--dropdown-panel-gap));
  left: 0;
  min-width: 100%;
  width: max-content;
  max-width: min(22rem, calc(100vw - 2rem));
  box-sizing: border-box;
  background: var(--popover-bg-2);
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  box-shadow: var(--shadow-dropdown);
  padding: 0.375rem;
  /* The panel is the app's popover wherever it opens, so it does not inherit
     the type of whatever it opens inside. The two on the Logs and airspace maps
     sit in chrome whose own captions are uppercase micro-type in a muted grey,
     and without these the option lists came out uppercase, semibold and grey. */
  color: var(--fg-body);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  text-align: left;
}
.searchable-select-search {
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.375rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.5rem 0.625rem;
  font: inherit;
  font-size: 0.875rem;
  outline: none;
  margin-bottom: 0.375rem;
}
.searchable-select-search:focus { border-color: var(--focus-ring); }
.searchable-select-list { max-height: 15rem; overflow-y: auto; }
.searchable-select-option {
  padding: 0.5rem 0.625rem;
  border-radius: 0.375rem;
  cursor: pointer;
  font-size: 0.875rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.searchable-select-option:hover,
/* Where the arrow keys are. Focus stays in the search box, so the list needs
   its own mark for the row Enter would take. */
.searchable-select-option.is-active { background: rgba(var(--rgb-slate), 0.14); }
.searchable-select-option.is-selected { background: var(--green-soft); color: var(--select-picked-fg); }
.searchable-select-option.is-selected.is-active { background: rgba(var(--rgb-green), 0.28); }
/* An <optgroup>'s heading. The Logs basemap picker is the one that has them
   ("New Zealand", "Worldwide"), and two of its options both read "Satellite" -
   so the headings are the only thing telling them apart and cannot be dropped
   just because the panel draws the list itself now. Not a row: the arrow keys
   and the type-ahead walk straight past it. */
.searchable-select-group-label {
  padding: 0.4375rem 0.625rem 0.1875rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-muted-2);
}
.searchable-select-group + .searchable-select-group { margin-top: 0.125rem; }
/* A disabled option is shown, because a native select shows it, and cannot be
   reached - it is not the same class as a live row, so nothing can land on it. */
.searchable-select-option-disabled {
  padding: 0.5rem 0.625rem;
  font-size: 0.875rem;
  color: var(--muted);
  opacity: 0.7;
  cursor: default;
}
/* A required picker submitted empty: the browser cannot show its own message
   over a hidden control, so the field carries the same amber a missing
   form-field does. */
.searchable-select.is-invalid .searchable-select-trigger {
  border-color: rgba(var(--rgb-amber), 0.55);
}
.searchable-select-empty { padding: 0.5rem 0.625rem; color: var(--muted); font-size: 0.8125rem; }
/* Opened upwards. A panel that does not fit under its trigger but does fit over
   it opens over it instead - a native select does the same at the bottom of a
   screen - and inside a box that hides its overflow it is the difference between
   a list you can read and one cut off at the page's edge (see panelPlacement).
   Set while the panel is open, cleared when it shuts. */
.searchable-select.drops-up > .searchable-select-panel {
  top: auto;
  bottom: calc(100% + var(--dropdown-panel-gap));
}

.multiselect-dropdown {
  position: relative;
  /* Same reason as .searchable-select: a grid item's automatic minimum is its
     min-content width, and one long name should not widen the form. */
  min-width: 0;
}

/* Box, arrow, hover, focus and disabled come from the shared DROPDOWN CONTROLS
   block; this picker only differs in that its value can run to two lines when
   several things are ticked. */
.multiselect-dropdown-trigger {
  white-space: normal;
}

/* The summary is a <span> inside a .form-field, so `.form-field span` - the
   FIELD LABEL style - was landing on the value: "Sprayer Two" rendered as
   "SPRAYER TWO" at 10px bold, and a missing field's amber label colour turned
   the value amber too. Three classes deep to outrank
   `.form-field.field-missing span`. */
.form-field .multiselect-dropdown-trigger .multiselect-dropdown-summary {
  color: inherit;
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: normal;
  text-transform: none;
}

/* Nothing selected: the span is empty, which gave the button no line to be
   tall around - 25px beside 35px inputs. A no-break space holds the line and
   still shows nothing, because an unassigned field renders blank. */
.multiselect-dropdown-summary:empty::before {
  content: "\00a0";
}

/* Three classes deep to beat `.form-field span` (see above); the colour is the
   shared placeholder grey, so nothing-chosen reads the same on all three
   kinds of dropdown. */
.form-field .multiselect-dropdown-trigger .multiselect-dropdown-summary.multiselect-dropdown-placeholder {
  color: var(--muted);
}

.multiselect-dropdown-panel {
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  z-index: 20;
  max-height: 13.75rem;
  overflow-y: auto;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  border-radius: 0.5rem;
  background: var(--popover-bg-3);
  box-shadow: var(--shadow-dropdown-2);
  padding: 0.375rem;
}

.multiselect-dropdown-panel label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.5rem;
  border-radius: 0.375rem;
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--fg-body);
}

.multiselect-dropdown-panel label:hover {
  background: rgba(var(--rgb-overlay), 0.06);
}

/* Sizing comes from the shared `input[type="checkbox"]` rule; a bare
   `width: auto` here would return the box to its native ~13px against the
   shared 1.25rem height and render it as a rectangle. */
.multiselect-dropdown-panel input {
  margin: 0;
}

.form-field input:disabled,
.form-field select:disabled,
.form-field textarea:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.form-field input::placeholder,
.form-field textarea::placeholder {
  color: rgba(var(--rgb-slate), 0.70);
}

/* Dropdowns are absent: they take the same focus mark from the shared
   DROPDOWN CONTROLS block, everywhere in the app rather than only inside a
   form field. */
.form-field input:focus,
.form-field textarea:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
}

/* Browser autofill paints inputs with an opaque light background, which flashes
   the dark login/form fields white. Cover it with an inset shadow in the field
   colour and keep the text legible. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  -webkit-box-shadow: var(--autofill-fill);
  box-shadow: var(--autofill-fill);
  -webkit-text-fill-color: var(--fg-body);
  caret-color: var(--fg-body);
  transition: background-color 9999s ease-in-out 0s;
}

/* ux:DEC2 - the keyboard focus ring (Josh, 2026-10-14, "Solid accent ring"; 2026-11-01, "more
   subtle"): a solid outline, --keyboard-ring-width wide (2 CSS px as drawn), in --keyboard-ring,
   3:1 or better in both themes; it was the same green at 35% as a shadow, which measured 2.2:1
   on the dark theme and 1.7:1 on the light. An outline, not a shadow: it follows the control's
   corners, shows in forced-colours mode, and leaves the control's own shadow (the light theme's
   button edge) alone. Keyboard only, so a mouse click rings nothing - and the app, not the
   browser, says what keyboard means: every ring rule is on :focus, and while a pointer is driving
   (html.using-pointer, app.js) the ring is 0 wide. It was :focus-visible, the browser's own guess,
   until 2026-11-02: WebKit does not count a script focus after a click-focus as visible, so arrow
   keys along a row of tabs or a list after a click drew nothing, step after step; and it rang every script
   focus after a tap. :focus weighs what :focus-visible did, so no rule outranks another it used to
   lose to. The few rules that change something other than the ring (a hint's colour, a menu
   button's opacity, the zone card's back arrow) say :where(html:not(.using-pointer)) as well, so a
   click changes nothing. A text field's own edge stays on :focus-visible, which a field always
   matches. The ring stands off the
   control by an offset, so that it reads against the surface and not against the control's fill
   (a button's offset is --button-focus-offset, set below). Where an ancestor clips an outside
   ring the rule for that control draws it inset (offset --keyboard-ring-inset). Every ring in this
   file reads these tokens, so the pointer rule below reaches all of them. */
button:focus,
select:focus,
input[type="checkbox"]:focus,
.app-nav-link:focus,
.queue-item:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: 0.0625rem;
}
/* A button is the one control that paints the bright main fill (--button-bg), so its ring stands off
   by --button-focus-offset: in the dark theme 2px of card between the blue and the ring, where the
   0.0625rem above renders as none. Same specificity as the rule above and straight after it, so a
   rule that styles its own ring (an inset one) wins over this exactly as it won over that. */
button:focus { outline-offset: var(--button-focus-offset); }
/* Eight places where the bigger ring would be cut by a clipping edge 0 to 4px from the button (found by
   focusing every button in the harness's states and photographing the ring on each side, with the thin
   offset and the bigger one): the phone tab bar's last tab, the quote chips' scroller, the Flights column's
   Upload logs, the phone logs band's toggle, the job card's Assess the risk, every button in the desktop job
   card's tab panels (the panel's scroller starts 1.7px from their left edge), the phone job card's tab strip
   (a tab parked at either end of the scrolled strip) and the phone Dashboard's quick actions (a row Tab
   scrolls into view lands flush with the page's edge: the bottom one going down, the top one coming back
   up). None paints the bright main fill, so nothing needs
   separating from it: they keep the thin ring they always had, through the same token (it inherits, so the
   container is enough). A button that does paint the main fill is never named here. */
html.mobile .mobile-tab-bar,
html.mobile .billing-chips,
html.mobile .job-preview-tabs,
html.mobile .dashboard-actions-grid,
#jobPreviewTabContent,
#uploadLogButton,
#jobPreviewRiskButton { --button-focus-offset: 0.0625rem; }

/* What nothing above names (a link, a row that is a button, a summary, a tab stop of its own) rang
   in the browser's own colour, which measured 1.4:1 on the dark theme. The same ring. :where() so
   no selector here outranks a rule that styles its own control, and a programmatic stop
   (tabindex -1: the main region, a heading focus lands on) is left alone. */
:where(a, summary, [role="button"], [tabindex]:not([tabindex="-1"]), input[type="radio"], input[type="range"]):focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: 0.0625rem;
}

/* A ring only for the keyboard (Josh, 2026-11-01: he saw it "on each page" from taps and clicks).
   The app moves focus by script all the time (onto the page after a change, into a dialog and back
   to what opened it, onto a row after a reload), and whether the browser rings what script focused
   is its own guess: WebKit, the iPhone app's engine, rang it after a tap. So app.js says who is
   driving: html.using-pointer from a real mouse, finger or pen press until the next key, Option+Tab
   included, or until focus arrives with no key the page saw (Tab from the address bar, a screen
   reader); moveFocus and the listeners beside it. Under it the ring is 0 wide, which every ring
   rule in this file reads, and the browser's own ring (an element no rule here styles, such as a
   heading focus lands on) is off too: :where() keeps that at no specificity, so it never outranks
   a rule of the app's. A key takes the class off before it moves focus, so a keyboard user always
   has the ring. */
html.using-pointer { --keyboard-ring-width: 0rem; }
:where(html.using-pointer :focus-visible) { outline: none; }

/* The panel scrolls inside an overflow:auto container that would clip an
   outside ring, so its ring is drawn inset. */
.job-preview-tab-panel:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: var(--keyboard-ring-inset);
}

@media (max-width: 980px) {
  .form-grid {
    grid-template-columns: 1fr;
  }
}

/* On the job card, the two dates one over the other: End date takes the
   right-hand column under Start date and Pilot in command fills the space
   beside it. Placed here rather than reordered in the markup, so the tab order
   and every one-column layout (phone, narrow window) still run Start date,
   End date, Pilot in command - a markup swap would put the pilot between the
   two dates there. */
@media (min-width: 981px) {
  html:not(.mobile) #jobPreviewOverviewForm .form-grid { grid-auto-flow: row dense; }
  html:not(.mobile) #jobPreviewOverviewForm label[for="jobPreviewEndDateInput"] { grid-column: 2; }
}

.role-checkboxes {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

.role-checkboxes label {
  display: flex;
  align-items: flex-start;
  gap: 0.5625rem;
  font-size: 0.875rem;
  color: var(--fg-body);
  text-transform: none;
  font-weight: 400;
  cursor: pointer;
}

.role-checkboxes input {
  margin-top: 0.125rem;
  flex: none;
}
.role-option-text { display: flex; flex-direction: column; }
/* The group sits inside a .form-field whose `span` rule styles labels as
   uppercase 0.75rem/700 micro-labels; the two-class selectors here outrank it
   so the role name reads as emphasis body text and the description as Meta
   (guide §3) rather than inheriting the micro-label size/weight/tracking. */
.role-checkboxes .role-option-name {
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: normal;
  color: var(--fg-body);
  text-transform: none;
}
.role-checkboxes .role-option-desc {
  margin-top: 0.125rem;
  font-size: 0.8125rem;
  font-weight: 400;
  letter-spacing: normal;
  line-height: 1.35;
  color: var(--fg-desc-2);
  text-transform: none;
}

.hidden-file-input { display: none; }

.log-manager-card {
  position: relative;
  transition: border-color 0.15s ease, background 0.15s ease;
}

/* NO GREEN ON THE CARD ITSELF. It has no border of its own, so border-color
   here painted nothing and the background washed the whole card - including the
   queue list below the dashed box - in green with no outline to contain it. The
   dashed box is what reads as the drop target, so the highlight belongs there
   and nowhere else. */
.log-manager-card.drag-over {
  background: transparent;
}

.drop-hint[hidden] { display: none; }

.drop-hint {
  display: grid;
  place-items: center;
  gap: 0.5rem;
  min-height: 8.625rem;
  margin: 0.5rem 0 1rem;
  padding: 1.125rem;
  border: 0.0625rem dashed rgba(var(--rgb-slate), 0.62);
  border-radius: 0.5rem;
  color: var(--fg-bright);
  text-align: center;
  font-size: 0.875rem;
  background: var(--drop-bg);
  cursor: pointer;
}

.drop-icon {
  font-size: 2.625rem;
  line-height: 1;
  color: var(--muted);
  margin-bottom: -0.25rem;
}

.log-manager-card.drag-over .drop-hint {
  color: var(--fg-on-green-tint);
  /* Solid rather than dashed while a file is over it: the box stops being a
     placeholder and becomes the thing you are about to hit. */
  border-style: solid;
  border-color: var(--green);
  background: rgba(var(--rgb-green), 0.14);
}

.upload-mode, .map-options {
  display: grid;
  gap: 0.375rem;
  margin: 0.625rem 0;
  color: var(--fg-control-2);
  font-size: 0.875rem;
}

.queue-list {
  max-height: 21.875rem;
  overflow-y: auto;
  border: 0;
  border-radius: 0;
  background: transparent;
  margin: 0.25rem 0 0;
  padding: 0;
  font-size: 0.8125rem;
}

.queue-list.empty {
  color: var(--fg-empty);
  font-style: normal;
  padding: 0;
}

#queueList {
  margin: 1.5rem 0;
}

.queue-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.625rem;
  align-items: center;
  padding: 0.75rem 0.625rem;
  margin: 0 0 0.4375rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0.5rem;
  background: var(--tile-bg);
}


.queue-item-no-toggle {
  grid-template-columns: 1fr auto;
}

.queue-item:hover { background: rgba(var(--rgb-overlay), 0.045); }
.queue-item:last-child { margin-bottom: 0; }
/* An inactive record (aircraft, client, charge rule, ended arrangement) is coloured
   down, not faded: its name and details below, at full strength for every pill and
   button in the row. */

.queue-item.selected {
  border-color: rgba(var(--rgb-blue), 0.65);
  background: rgba(var(--rgb-blue), 0.14);
}

.queue-item.log-visible {
  border-color: rgba(var(--rgb-green), 0.55);
  background: rgba(var(--rgb-green), 0.12);
}

/* SELECTION HAS TO WIN OVER "VISIBLE", and it did not: the two rules above have
   equal specificity, so the later one took the row - and every selectable row
   is a visible one, because a track you can click on the map is by definition
   drawn on it. So selection never showed at all.
   Drawn as a ring rather than a fill, which also lets a row say both things at
   once: green that it is on the map, blue that it is the one you picked. */
.queue-item.selected,
.queue-item.log-visible.selected {
  border-color: rgba(var(--rgb-blue), 0.95);
  box-shadow: 0 0 0 0.125rem rgba(var(--rgb-blue), 0.4);
}

#adminSettingsSection[hidden] { display: none; }

.org-switcher-row-content {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-width: 0;
}

.org-switcher-row-text {
  min-width: 0;
  flex: 1;
}

.queue-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 760;
  color: var(--text);
}

.queue-meta {
  color: var(--fg-meta);
  margin-top: 0.3125rem;
  font-size: 0.75rem;
}

/* LIST ROW TEXT (Josh, 2026-11-05: "standardize the text/font in the major list
   boxes"). The rows of the four major lists - Plan's jobs, Flights' jobs and logs,
   Aircraft, Clients, in every view and on the phone - write their text in these
   three styles and no others, so a name is the same size wherever it is listed:
     .list-row-title      the row's name (a job, a log, an aircraft, a client)
     .list-row-secondary  the line under it, and the facts of a list-view row
     .list-row-meta       the small print beside a name (a log's loading status);
                          a pill (.status-pill) is the same size, by the token
   Sizes are the Plan list's, in rem so the phone root scales them (ui-scale-rem).
   They live here once. A row's own rules say where its text goes, never how big
   it is: no font-size on these rows anywhere else (ux-list-text-frontend.mjs).
   A state may recolour a style (inactive, picked, selected) - that is a role, not
   a size - and it does so from a selector that outranks the class. This block sits
   after .queue-name and .queue-meta, which the rows still carry for their layout,
   so it wins over them at equal weight. The td forms match `.data-table td`
   (Plan's cells), which would otherwise set the size of every cell it holds. */
:root {
  --list-title-size: 0.9375rem;
  --list-secondary-size: 0.8125rem;
  --list-meta-size: 0.6875rem;
  --list-line-height: 1.25;
}
.list-row-title,
td.list-row-title {
  font-size: var(--list-title-size);
  font-weight: 400;
  line-height: var(--list-line-height);
  color: var(--text);
}
.list-row-secondary,
td.list-row-secondary {
  font-size: var(--list-secondary-size);
  font-weight: 400;
  line-height: var(--list-line-height);
  color: var(--muted);
}
.list-row-meta,
td.list-row-meta {
  font-size: var(--list-meta-size);
  font-weight: 400;
  line-height: var(--list-line-height);
  color: var(--muted);
}
.queue-item.inactive .list-row-secondary { color: var(--fg-faint); }
/* A name nobody typed (the client's, standing in) stays muted: its own rule is
   earlier in the sheet, so the class above would otherwise take the colour back. */
.list-row-title.is-placeholder-name { color: var(--muted); }

.item-progress {
  margin-top: 0.5rem;
  height: 0.375rem;
  width: 100%;
  background: rgba(var(--rgb-slate), 0.18);
  border-radius: 62.4375rem;
  overflow: hidden;
  border: 0;
}

.item-progress-bar {
  height: 100%;
  width: 0%;
  background: var(--green);
  transition: width 0.25s ease;
}

.log-details {
  margin-top: 0.5rem;
  padding: 0.5rem 0.5625rem;
  border-left: 0.125rem solid rgba(var(--rgb-slate), 0.22);
  color: var(--fg-label);
  font-size: 0.75rem;
  line-height: 1.45;
  background: var(--inset-bg);
  border-radius: 0.5rem;
}

.log-review-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.375rem;
}

.log-review-row button {
  width: auto;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
}

.log-title-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.375rem;
  align-items: center;
  min-width: 0;
}

.log-details-toggle {
  width: 1.75rem;
  height: 1.75rem;
  min-height: 0;
  padding: 0;
  margin: 0;
  display: inline-grid;
  place-items: center;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--fg-muted-2);
  font-size: 0.8125rem;
  line-height: 1;
  font-weight: 800;
}

.log-details-toggle:hover {
  background: rgba(var(--rgb-slate), 0.12);
  color: var(--fg-strong-2);
}

.jobs-table {
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
}

/* Column widths in a `table-layout: fixed` table are a priority order: every
   column given a rem width is rigid, and whichever column is `auto` absorbs the
   entire squeeze as the table narrows. The name was the auto column while #,
   Date, Client and Status held 34.5rem between them, and the jobs list only
   gets half the page, so the job name - the thing that identifies the row - was
   the first thing to vanish: 164px at 1600 wide, 4px at 1280.
   Each rigid column is now sized to what its content actually measures (the
   longest date renders at 79px, the widest status pill, "Pending approval", at
   81.5px) and Client is a percentage, so it gives ground as the card narrows
   instead of holding 12.5rem while the name starves. */
/* The Date column holds the start date only - the job's length used to sit
   under it as a second line, which made every multi-day job's row taller than
   its neighbours.
   The overflow guard stays: a cell in a `table-layout: fixed` table with no
   ellipsis paints its overrun straight over the next column rather than being
   clipped, which is how a too-wide date first showed up written across the job
   name. */
.jobs-table .job-col-date {
  width: 7.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.jobs-table .job-col-name {
  width: auto;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Name and the attention mark on one line; the NAME gives way when they don't
   fit, so the mark - the safety flag - is never the part that is cut off. */
.job-name-line {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}
/* The row carries at most one mark after the name (jobFlagMark), a fixed
   1.125rem, so the name simply takes whatever is left. */
.job-name-text {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Pushed to the far end of the name's space, so the marks stand in one
   column down the list instead of trailing each name at whatever width it
   happens to be - a column the eye can run down. */
.job-row-badges {
  display: inline-flex;
  flex: none;
  gap: 0.25rem;
  margin-left: auto;
}

/* Every row the same height, whatever it holds. Every cell is one line (the
   table is nowrap throughout and the name line above cannot wrap), so this is
   a floor nothing in a row reaches past - the tallest thing a row can hold, a
   status or warning pill, fits inside it with room to spare. */
#jobsTableBody tr.job-row { height: 2.5rem; }
#jobsTableBody tr.job-row > td { vertical-align: middle; }
/* The "..." on a Plan row is the phone's (item 25, drawn only in the phone layout): a computer
   has the right-click and the hint line under the list. Hidden here too, for the moment between
   a layout change and the list being drawn again. */
html:not(.mobile) #jobsTableBody .row-menu-button { display: none; }

/* ---- A list too narrow for its columns ---------------------------------
   The list takes a third of the page beside the job. At that width six
   columns do not fit: measured, the job name - the one thing that identifies
   a row - got 101px at 1920 wide, 17px at 1600 and nothing at all at 1440,
   with the warning pills clipped away beside it. So below the width the
   table needs, each row becomes the same two-line block the phone layout
   uses (see "Jobs list rows on mobile"):
     line 1  #number  name…  [!]                     [status]
     line 2  date · client
   Nothing is dropped, the name gets the full width, and every row is still
   the same height. 46rem is where the table leaves the name about 12rem.
   Desktop only: the phone layout has its own rules and must not get both. */
.all-jobs-card { container: jobs-list / inline-size; }
/* Being a size container makes the card its own stacking context, which traps
   a searchable-select panel's z-index inside it - and the client picker's
   panel is wider than its trigger, so with a long client name it ran over the
   job card beside it and was painted UNDER that card's tabs and inputs, and
   clicks on it landed there. While one of its pickers is open the card stacks
   above its neighbour. (A grid item, so z-index applies without position.) */
.all-jobs-card:has(.searchable-select.open) { z-index: 5; }

/* A narrow list (a third of the window up to about 1590 wide with the nav open):
   the search box with the Sort icon at its end on a line of their own, the two
   pickers sharing the next. The first column is half the row less half the gap
   and the client picker spans the other two, so the pickers stay equal while
   the first line splits into the search box and the icon (auto: the icon's own
   square). The placeholder, "Search job, number or client" (147px, measured in
   Chromium), is whole in every list a computer draws: the narrowest, 218px at
   1025 wide with the nav open, leaves it 155px; one row from 30rem leaves it
   155px as well (ux-plan-phone-frontend.mjs, 8, prices it for SF Pro). */
@container jobs-list (max-width: 30rem) {
  .jobs-toolbar-filters {
    grid-template-columns: calc(50% - 0.25rem) minmax(0, 1fr) auto;
    grid-template-areas: "search search sort" "status client client";
  }
}

@container jobs-list (max-width: 46rem) {
  html:not(.mobile) .jobs-table-head .job-col-date,
  html:not(.mobile) .jobs-table-head .job-col-client { display: none; }
  /* 3.75rem = the row's 0.75rem inset + its 2.5rem number column + the 0.5rem
     gap, so "Name" sits over the names. Change them together. */
  html:not(.mobile) .jobs-table-head .job-col-number { width: 3.75rem; }
  html:not(.mobile) .jobs-table-head .job-col-name { padding-left: 0; }
  /* Each row is its own grid, so an auto status column would be as wide as
     that row's pill and "centred" would mean nothing down the list. A fixed
     column instead, the same 6.5rem as this heading's content box (7.5rem less
     its even 0.5rem padding), both ending the row's 0.75rem inset from the
     right - so every pill and the heading share one centre line. 6.5rem
     clears the widest pill, "Pending approval" (about 6rem). */
  html:not(.mobile) .jobs-table-head .job-col-status { padding-right: 0.75rem; padding-left: 0.25rem; }

  html:not(.mobile) #jobsTable,
  html:not(.mobile) #jobsTable > tbody { display: block; }
  html:not(.mobile) #jobsTableBody tr.job-row {
    display: grid;
    /* Fixed first column: each row is its own grid, so an auto one would step
       a longer job number's name out of line with the rest. */
    grid-template-columns: 2.5rem max-content minmax(0, 1fr) 6.5rem;
    /* The pill spans both lines and sits in the middle of the row, not level
       with the name - so the client line stops short of its column. */
    grid-template-areas:
      "num name name   status"
      "num date client status";
    align-items: center;
    column-gap: 0.5rem;
    row-gap: 0.125rem;
    height: auto;
    padding: 0.5rem 0.75rem;
    border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  }
  html:not(.mobile) #jobsTableBody tr.job-row > td {
    padding: 0;
    border: 0;
    width: auto;
  }
  html:not(.mobile) #jobsTableBody .job-col-number { grid-area: num; align-self: start; }
  html:not(.mobile) #jobsTableBody .job-col-name { grid-area: name; min-width: 0; }
  html:not(.mobile) #jobsTableBody .job-col-status { grid-area: status; text-align: center; align-self: center; }
  html:not(.mobile) #jobsTableBody .job-col-date { grid-area: date; }
  html:not(.mobile) #jobsTableBody .job-col-client { grid-area: client; min-width: 0; }
  /* Line 2 reads a step down from the name: the secondary style, from the tokens of
     LIST ROW TEXT. Only in this two-line layout; the wide table keeps its columns
     at the table's own size and colour. */
  html:not(.mobile) #jobsTableBody .job-col-date,
  html:not(.mobile) #jobsTableBody .job-col-client {
    font-size: var(--list-secondary-size);
    line-height: var(--list-line-height);
    color: var(--muted);
  }
  html:not(.mobile) #jobsTableBody .job-col-client::before {
    content: "\00b7";
    margin-right: 0.375rem;
  }
  /* The name wraps to a second line before it ellipsises (review 2026-11-07, item 41): a job is
     told from the next by its name, and one line of it, cut to "AUDIT-...", told nothing. */
  html:not(.mobile) #jobsTableBody .job-name-line { align-items: flex-start; }
  html:not(.mobile) #jobsTableBody .job-name-text {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}


.jobs-table .job-col-client {
  width: 21%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Centred, heading and pills alike, so pills of different widths ("Open",
   "Pending approval") share one centre line down the column. Even side
   padding, or the centre is off by the difference. */
.jobs-table .job-col-status {
  width: 7.5rem;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
  text-align: center;
}

/* And every pill the same width, so the column reads as one column rather
   than a ragged edge. Sized to the widest label, "Pending approval" (81.5px,
   about 6rem), with a little room; the column's 6.5rem content box holds it
   in both the table and the two-line layout. Desktop only: on a phone the
   pill ends the name's line, where a fixed width would come straight off
   every job name. */
html:not(.mobile) #jobsTableBody .job-col-status .status-pill {
  box-sizing: border-box;
  width: 6.25rem;
  text-align: center;
}

/* A list narrower still (the third of a window up to about 1280 wide; Josh, 2026-11-07): the status
   is a small word pill at the start of line 2, before the date and client, so the name keeps line 1
   (two lines when it needs them) and the status column's 6.5rem goes to the client. The STATUS
   heading over that column goes with it (item 41, measured at 1025, 1100, 1180 and 1280 wide). The
   full pill column is back from 1280 up. */
@container jobs-list (max-width: 24rem) {
  html:not(.mobile) #jobsTableBody tr.job-row {
    grid-template-columns: 2.5rem max-content max-content minmax(0, 1fr);
    grid-template-areas:
      "num name   name name"
      "num status date client";
  }
  html:not(.mobile) #jobsTableBody .job-col-status { text-align: left; align-self: center; }
  html:not(.mobile) #jobsTableBody .job-col-status .status-pill {
    width: auto;
    padding: 0 0.3125rem;
    font-size: 0.6875rem;
  }
  html:not(.mobile) .jobs-table-head .job-col-status { display: none; }
}

.jobs-table .job-col-number {
  width: 2.75rem;
  padding-right: 0.5rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* Sortable column headers: clickable, with an arrow on the active column. */
.jobs-table-head .job-sortable { cursor: pointer; user-select: none; }
.jobs-table-head .job-sortable:hover { color: var(--fg-nav); }
.jobs-table-head .job-sortable.sorted-asc::after { content: " \25B2"; font-size: 0.5625rem; color: var(--green); }
.jobs-table-head .job-sortable.sorted-desc::after { content: " \25BC"; font-size: 0.5625rem; color: var(--green); }

.status-pill {
  display: inline-block;
  padding: 0.1875rem 0.4375rem;
  border-radius: 62.4375rem;
  font-size: var(--list-meta-size);
  font-weight: 750;
  white-space: nowrap;
}

.status-pending { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.status-queued { background: rgba(var(--rgb-blue), 0.20); color: var(--pill-blue-fg); }
.status-uploading { background: rgba(var(--rgb-orange), 0.18); color: var(--pill-sand-fg); }
.status-processing { background: var(--pill-orange-bg); color: var(--pill-orange-fg); }
.status-completed { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.status-failed { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
.status-needs_review { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
/* Held on the device until the connection comes back (the upload outbox).
   Deliberately the calm blue of "queued", not an alarm colour: nothing is
   wrong, the log is safe, it just hasn't been sent yet. */
.status-waiting { background: rgba(var(--rgb-blue), 0.20); color: var(--pill-blue-fg); }
.status-airworthy { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.status-grounded { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
.status-under_maintenance { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
.status-retired { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.status-open { background: rgba(var(--rgb-blue), 0.20); color: var(--pill-blue-fg); }
.status-upcoming { background: var(--pill-violet-bg); color: var(--pill-violet-fg); }
.status-cancelled { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
/* Awaiting an approver's sign-off before it can go live. */
.status-pending_approval { background: var(--pill-apricot-bg); color: var(--pill-orange-fg); }
/* Financials workflow stages. */
.status-draft { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.status-quoted { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
.status-invoiced { background: var(--pill-teal-bg); color: var(--pill-teal-fg); }
.status-maintenance-due { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
.status-maintenance-upcoming { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
/* Defect lifecycle. "Open" already exists above in the calm blue, and a
   deferred defect is amber rather than blue because it is a defect somebody has
   looked at and decided to carry - still live, but no longer unassessed.
   "Grounding" is its own pill, not a severity: severity is the triage signal,
   grounding is the airworthiness decision, and the two are independent. */
.status-deferred { background: rgba(var(--rgb-amber), 0.22); color: var(--pill-amber-fg); }
.status-resolved { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.status-grounding { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }

/* 50h/100h/200h/yearly checkbox groups in the aircraft add/detail modals. */
.maintenance-interval-list {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.375rem 1rem;
}
.maintenance-interval-list label { white-space: nowrap; }

/* Fleet rows can now carry three pills (open defects + maintenance +
   airworthiness), and the defect and maintenance-log rows inside the aircraft
   modal carry their own. Wrapping matters on a phone: three pills do not fit
   beside a serial number, and the alternative is a row that overflows. */
#aircraftFleetList .queue-item:not(.rec-row) > div:last-child,
#aircraftDefectList .queue-item > div:last-child,
#aircraftMaintenanceList .queue-item > div:last-child {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.375rem;
  align-items: center;
}

/* The airworthiness record hangs off the aircraft's own dialog, below the
   aircraft form: two more sections in the same card rather than a screen of
   their own, because a defect and the service that cleared it are facts about
   THIS aircraft and are read next to its hours and its maintenance clock. Each
   section needs its own separation from the block above it - .modal-section-title
   carries a bottom margin only. */
#aircraftDetailOverlay .modal-section-title { margin-top: 1.125rem; }
#aircraftDetailOverlay .card-copy { margin: 0 0 0.5rem; font-size: 0.8125rem; }

/* Users settings tab hosts the personnel master-detail: let it use the full
   panel width/height instead of the settings forms' narrow single column. */
#settingsPage .settings-layout[data-settings-panel="users"] {
  /* Flex column, NOT block. The panel is height:100% with overflow:hidden, and
     it holds two children: the access-review banner and the personnel
     master-detail. Under display:block the master-detail's own height:100%
     resolved against the PANEL rather than the space left beside the banner,
     so the content ran over by exactly the banner's height and the clipped
     edge cut off the bottom of the list card - taking the "Add user" button
     with it. A flex column gives the banner its natural height and hands the
     rest to the master-detail. */
  display: flex;
  flex-direction: column;
  /* The banner carries its own margin-bottom; the inherited 1rem grid gap
     would double the spacing now that it applies. */
  gap: 0;
  max-width: none;
  overflow: hidden;
  padding: 0;
}
#settingsPage .settings-layout[data-settings-panel="users"] > .personnel-layout {
  /* Take the remaining height rather than the panel's full height. min-height:0
     lets the two cards' own scrollers do the overflowing. */
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
}
/* This override's display outranks the generic .settings-layout[hidden] rule,
   so it needs its own [hidden] guard or the panel shows on every tab. */
#settingsPage .settings-layout[data-settings-panel="users"][hidden] {
  display: none;
}

/* "+ Add user" sits in the list card's header beside its title, so it is on
   screen without scrolling the list (it used to sit under 41 rows). */
.personnel-add-user-button {
  flex: none;
}

/* Inactive users stay listed (so they can be reactivated) but read as muted. */
.personnel-row.inactive .queue-name { color: var(--fg-subtle); }

/* Compliance-flag banner (user detail): a rule the user violates. */
.compliance-flags {
  border: 0.0625rem solid rgba(var(--rgb-red), 0.4);
  background: rgba(var(--rgb-red), 0.08);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  margin-top: 0.75rem;
}
.compliance-flags[hidden] { display: none; }
.compliance-flags strong { color: var(--fg-error); }
.compliance-flags ul { margin: 0.375rem 0 0; padding-left: 1.125rem; }
.compliance-flags li { font-size: 0.8125rem; color: var(--fg-body); }

/* Certification-types management: rows with a remove control + an add row. */
.cert-type-row { align-items: center; }
.cert-type-remove {
  flex: none;
  min-height: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.625rem;
  height: 1.625rem;
  padding: 0;
  margin: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.375rem;
  background: var(--control-bg);
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1;
  cursor: pointer;
}
.cert-type-remove:hover { color: var(--fg-error); border-color: rgba(var(--rgb-red), 0.5); }
.cert-type-add {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.625rem;
}
.cert-type-add input {
  flex: 1 1 auto;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.5rem 0.625rem;
  font: inherit;
  font-size: 0.875rem;
}
/* width:auto overrides the global button{width:100%}; without it flex:none keeps
   the button 100%-wide and it overflows the panel past the input. */
.cert-type-add button { flex: none; width: auto; margin: 0; }

/* Optional cert expiry lead-time input under the rule's cert checkboxes. */
.cert-lead-field { display: block; margin-top: 0.75rem; }
.cert-lead-field span { display: block; font-size: 0.75rem; color: var(--muted); margin-bottom: 0.25rem; }
.cert-lead-field input {
  width: 8.125rem;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.4375rem 0.5625rem;
  font: inherit;
  font-size: 0.875rem;
}

/* Compliance report card: sections of flagged personnel and jobs. */

/* NOTAM overlay - map legend toggle status + job-preview strip + map popup. */
.airspace-swatch-notam {
  background: var(--amber-solid);
  border-radius: 0.125rem;
  border: 0.0625rem dashed rgba(var(--rgb-orange), 0.9);
}
/* Non-relevant NOTAMs are drawn faded on the map, so mute the legend swatch to
   match (dimmer fill + no dashed border). */
.airspace-swatch-notam-other {
  background: rgba(var(--rgb-orange), 0.4);
  border-color: transparent;
}
/* The two NOTAM sub-toggles sit under one "NOTAMs" heading. */
/* NOTAMs is a collapsible group like the zone lists, and like them its summary
   row carries the master checkbox and the title but NO colour swatch - the
   swatches belong to the two relevance rows inside it, which are the things
   that actually have a colour on the map. */
.airspace-notam-status {
  margin-left: auto;
  font-size: 0.6875rem;
  color: var(--fg-subtle);
  white-space: nowrap;
}
.airspace-notam-status.stale { color: var(--fg-amber); }

/* NOTAM time scrubber - floating bar along the bottom of the airspace map. Its
   foot is --airspace-timebar-foot (below), which a computer raises while the
   map credit is open in the corner beside it; a phone has a foot of its own. */
.airspace-timebar {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: var(--airspace-timebar-foot);
  /* Outranks the layers legend card (5) deliberately, rather than winning on DOM
     order as it used to - which is how it came to be painted over two of that
     card's toggle rows without anyone choosing it. The chart legend panel is
     also 6 and comes later, so it still covers the bar while it is open: that
     one the pilot has just deliberately expanded, and collapsing it brings the
     bar straight back. */
  z-index: 6;
  display: flex;
  align-items: center;
  /* Wrapping exists for the freshness line below, which takes a full row of
     its own. The four controls never wrap: the slider shrinks instead. */
  flex-wrap: wrap;
  gap: 0.625rem;
  width: min(66%, calc(100% - 3rem));
  padding: 0.5rem 0.75rem;
  background: var(--map-float-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  border-radius: 0.625rem;
  box-shadow: var(--shadow-map-notice);
}
/* The bar's foot on a computer: 1.5rem, and while the map credit is open (the
   whole credit, wrapped, growing left and up out of the bottom-right corner,
   wider than the corner the bar leaves it) the credit's height over its 0.5rem
   margin and 0.5rem of air - over the chart legend's bar as well when a chart
   is up, which lifts the credit 2.75rem (ux:k1). The credit publishes its own
   height on the map shell (--map-credit-h, app.js createMapCreditControl). The
   open layers card reads the same foot, so it never reaches the raised bar. */
#airspaceMapShell { --airspace-timebar-foot: 1.5rem; }
html:not(.mobile) #airspaceMapShell:has(.map-credit.is-expanded) {
  --airspace-timebar-foot: max(1.5rem, calc(0.5rem + var(--map-credit-h, 1.75rem) + 0.5rem));
}
html:not(.mobile) #airspaceMapShell:has(#airspaceLegendPanel:not([hidden])):has(.map-credit.is-expanded) {
  --airspace-timebar-foot: calc(2.75rem + 0.5rem + var(--map-credit-h, 1.75rem) + 0.5rem);
}
/* How old the stored NOTAM picture is. This is a different fact from where the
   scrubber is pointing, and conflating the two is what let the bar read "live"
   over an 18-hour-old feed. Its own full-width row, and nothing at all to say
   while the feed is healthy and the bar is following the clock - so the
   everyday desktop bar stays the single line it has always been. */
.airspace-time-freshness {
  flex: 0 0 100%;
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--fg-label);
}
.airspace-timebar.is-stale {
  border-color: rgba(var(--rgb-orange), 0.8);
  background: var(--notice-warn-bg);
}
.airspace-timebar.is-stale .airspace-time-freshness {
  color: var(--fg-warn-2);
  font-weight: 600;
}
.airspace-time-date {
  flex: 0 0 auto;
  background: var(--map-control-bg-2);
  color: var(--fg-body-2);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.3);
  border-radius: 0.375rem;
  padding: 0.25rem 0.375rem;
  font-size: 0.75rem;
  color-scheme: var(--control-scheme);
}
.airspace-time-slider {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--amber-solid);
  cursor: pointer;
}
.airspace-time-label {
  flex: 0 0 auto;
  min-width: 13rem;
  text-align: right;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-label);
  white-space: nowrap;
}
.airspace-time-now {
  flex: 0 0 auto;
  padding: 0.25rem 0.625rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--on-amber);
  background: var(--amber-solid);
  border: none;
  border-radius: 0.375rem;
  cursor: pointer;
}
.airspace-time-now:hover { background: var(--fg-warn); }

/* Mobile: the time bar spans the width (minus a small margin) and sits above the
   bottom tab bar. The map runs under the tab bar now, so the offset is the
   tab-bar reserve plus room for the scale bar, which sits in the corner
   directly below it (a fixed 33.5px row - see "Shared: both full-screen maps"
   in the mobile section). The two stack rather than overlap.

   The bar is REORDERED for a phone rather than re-marked-up: the DOM keeps the
   desktop order (date, slider, label, Now) and `order` + the wrap above build
   a two-row bar out of it, so the desktop bar cannot be disturbed by this.
   Row 1 is date + what you are looking at + Now; row 2 is the slider, on its
   own, full width. That last point is the whole exercise: squeezed onto one
   line the slider measured 66.4px for a 24-hour range - 22 minutes per pixel,
   so a 5px thumb slip moved the clock two hours - and the date button wrapped
   to three lines ("8 / Sep / 2026"), which is what made the bar 79.2px tall
   and reached it up into the legend. Full width it is ~353px, about 4 minutes
   per pixel, with a thumb a gloved hand can actually catch. */
html.mobile .airspace-timebar {
  width: calc(100% - 1.25rem);
  /* 3rem leaves the credit its line and the scale its row. The credit opened
     to read runs to two or three lines on a phone, so the bar rises clear of
     it while it is open (--map-credit-h, the credit's height, set on the map
     shell by createMapCreditControl). */
  bottom: calc(max(3rem, var(--map-credit-h, 0rem) + 1.125rem) + var(--mobile-tab-bar-inset));
  gap: 0.25rem 0.5rem;
  padding: 0.375rem 0.5rem 0.5rem;
}
html.mobile .airspace-timebar .date-field { order: 1; flex: 0 0 auto; }
html.mobile .airspace-time-label {
  order: 2;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  font-size: 0.6875rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile .airspace-time-now { order: 3; }
html.mobile .airspace-time-slider { order: 4; flex: 0 0 100%; }
html.mobile .airspace-time-freshness { order: 5; }
/* Upright, the label takes the room the date and Now leave it and wraps in
   it, instead of asking for its one-line width first: at the 16px phone base
   that width (231px at 390) pushed Now onto a row of its own on every phone,
   and the bar grew from 144px to 200px. Wrapped, its two or three short lines
   still fit the row's 52px. */
html.mobile.is-portrait .airspace-time-label { flex: 1 1 0; white-space: normal; }
/* A short phone held upright (an iPhone SE in Safari with its bars, 375x548)
   has no height left for the bar under the control column: at the 16px base
   the column ends 228px into the map and the bar, with the stale-NOTAM line,
   began 200px in, so the bar and its Now button took the locate button's
   taps. Under 621px the bar stops short of the column instead, as it does
   sideways, and keeps the map's left edge; with the credit opened (the bar
   rises clear of it) the full width needs about 620px. Taller phones keep the
   full width. The column's own width is --touch-min plus MapLibre's 10px
   margin either side of it, in px as MapLibre writes it: as 1.25rem it was
   20px only at the 16px base, and at 14px the bar ran 2.5px onto the column. */
@media (max-height: 620px) {
  html.mobile.is-portrait .airspace-timebar {
    left: 0.625rem;
    transform: none;
    width: calc(100% - 0.625rem - (var(--touch-min) + 20px));
  }
}
/* Under 360px across, the narrowed bar leaves the label 40px between the date
   and Now (at 320), too narrow for its lines; there it takes the bar's first
   row of its own, over the date and Now. */
@media (max-height: 620px) and (max-width: 359px) {
  html.mobile.is-portrait .airspace-time-label { order: 0; flex: 0 0 100%; }
  html.mobile.is-portrait .airspace-time-now { margin-left: auto; }
}

/* The date button is the app's own picker trigger (the native input is hidden
   by setupDateSpinners), so it inherits a 100%-wide form-field shape. In the
   bar it is one compact control among three: content width, one line. */
html.mobile .airspace-timebar .date-field { gap: 0; }
html.mobile .airspace-timebar .date-trigger {
  width: auto;
  min-height: var(--touch-min);
  gap: 0.375rem;
  padding: 0 0.5rem;
  font-size: 0.8125rem;
  white-space: nowrap;
}
html.mobile .airspace-timebar .date-trigger-icon { width: 0.9375rem; height: 0.9375rem; }
html.mobile .airspace-time-now {
  min-height: var(--touch-min);
  padding: 0 0.875rem;
  font-size: 0.8125rem;
}

/* A scrubber for a thumb in a glove: a tall hit area, a fat thumb and a track
   that reads at arm's length in sun. Both vendor prefixes - styling either
   pseudo-element opts the control out of its native rendering entirely, so the
   track has to be drawn too. */
html.mobile .airspace-time-slider {
  height: var(--touch-min);
  min-height: var(--touch-min);
  margin: 0;
  padding: 0;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
html.mobile .airspace-time-slider::-webkit-slider-runnable-track {
  height: 0.375rem;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.38);
}
html.mobile .airspace-time-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: -0.6875rem;
  border-radius: 999px;
  background: var(--amber-solid);
  border: 0.125rem solid var(--thumb-border);
  box-shadow: var(--shadow-thumb);
}
html.mobile .airspace-time-slider::-moz-range-track {
  height: 0.375rem;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.38);
}
html.mobile .airspace-time-slider::-moz-range-thumb {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  background: var(--amber-solid);
  border: 0.125rem solid var(--thumb-border);
}
html.mobile .airspace-time-freshness { font-size: 0.75rem; }

/* Landscape is the opposite problem: 844px of width and a map only 315px tall,
   where a two-row bar costs 35% of it. There the controls go back on one line
   (the desktop arrangement) and only the freshness keeps its own row - the
   slider is still ~350px, so the scrub precision the rebuild exists for is
   untouched. `is-portrait` is set from matchMedia in applyMobileClass, so this
   needs no media query of its own. */
/* One line up here puts the bar level with the map's right-hand control column
   (a touch-floor button plus MapLibre's 10px gutters), and the bar outranks
   that column, so a full-width bar paints straight over the buttons. It
   already clipped the compass, and the locate button below that was covered
   outright: a tap meant for "show my location" opened the date picker. Both
   ends keep clear of the column, symmetrically, so the bar stays centred where
   it has always been. */
/* The one line is a grid rather than a wrapping row: a wrapping row breaks on
   each control's natural width, the slider's 129px included, so at the 16px
   phone base a bar under about 513px (a 640 or 667px phone) put Now on a row
   of its own, the bar doubled to 124px, the Layers button above it went
   under the header and the bar covered the zoom and locate buttons. Here the
   slider keeps 8rem and takes what is spare, and the label gives way first,
   wrapping its time onto a second line. The freshness line keeps its own row,
   and the toggle that folds the bar (below) takes the last column, after Now. */
html.mobile:not(.is-portrait) .airspace-timebar {
  width: calc(100% - 1.25rem - 2 * (var(--touch-min) + 1.25rem));
  display: grid;
  grid-template-columns: auto minmax(8rem, 1fr) minmax(0, max-content) auto auto;
}
html.mobile:not(.is-portrait) .airspace-time-freshness { grid-column: 1 / -1; }
html.mobile:not(.is-portrait) .airspace-timebar .date-field { order: 1; }
html.mobile:not(.is-portrait) .airspace-time-slider { order: 2; flex: 1 1 auto; }
html.mobile:not(.is-portrait) .airspace-time-label {
  order: 3;
  flex: 0 0 auto;
  text-align: right;
  white-space: normal;
}
html.mobile:not(.is-portrait) .airspace-time-now { order: 4; }
html.mobile:not(.is-portrait) .airspace-time-toggle { order: 5; }

/* ---- The time bar, collapsed (phones; app.js setAirspaceTimeBarCollapsed) ----
   Josh, 2026-11-02: "some way to minimize the notam timeline bar on mobile".
   The toggle is the bar's last button, a chevron after Now (upright, on the
   first row: the label gives up the width; sideways, the last column). Pressed,
   the bar folds to the toggle alone: a pill on the bar's own foot, centred,
   whatever the bar's width was (the short-phone bar is left-anchored to clear
   the control column; the pill, one row at the foot of the map, sits well
   below where the column ends). The pill says
   which time the NOTAMs are for - "Now", or the chosen time in amber - and,
   while the feed is out of date, the freshness line under it, in the bar's own
   stale colours (.is-stale is on the same element).
   Nothing positioned against the bar is told separately: the bar is still the
   element they read, so --airspace-timebar-h (app.js syncAirspaceTimeBarHeight)
   becomes the pill's height and the Layers button drops to sit 0.5rem over the
   pill, and the zone popup's room ends at the pill's top. On a computer the
   toggle is not shown and nothing here applies. */
.airspace-time-toggle { display: none; }
html.mobile .airspace-time-toggle {
  display: inline-flex;
  order: 3;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  width: var(--touch-min);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: 0;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--fg-label);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.2;
}
html.mobile .airspace-time-toggle-chevron {
  display: inline-flex;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
}
html.mobile .airspace-time-toggle svg { width: 100%; height: 100%; }
/* Open, the toggle is the chevron alone: the clock and the words are the pill's. */
html.mobile .airspace-time-toggle-clock,
html.mobile .airspace-time-toggle-text { display: none; }
/* The pill's widest is the bar's own width: upright the full width less its
   margin (the control column ends far above the foot of the map), sideways
   the width that clears the control columns either side. Its words wrap
   rather than cut: the time is the one thing on it that matters. */
html.mobile .airspace-timebar.is-collapsed {
  display: flex;
  flex-wrap: nowrap;
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: calc(100% - 1.25rem);
  gap: 0;
  padding: 0;
  border-radius: 999px;
}
html.mobile:not(.is-portrait) .airspace-timebar.is-collapsed {
  max-width: calc(100% - 1.25rem - 2 * (var(--touch-min) + 1.25rem));
}
html.mobile .airspace-timebar.is-collapsed > :not(.airspace-time-toggle):not(.airspace-time-freshness) { display: none; }
/* The freshness line is the bar's live region (role=status): folded, it is
   hidden the way .visually-hidden is, never display:none, so a feed that goes
   out of date while the bar is folded is still announced. The pill shows the
   same words on screen. */
html.mobile .airspace-timebar.is-collapsed > .airspace-time-freshness {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
html.mobile .airspace-timebar.is-collapsed .airspace-time-toggle {
  width: auto;
  min-width: 0;
  max-width: 100%;
  padding: 0.25rem 0.75rem 0.25rem 0.875rem;
  gap: 0.5rem;
  border-radius: inherit;
  color: var(--fg-strong);
}
html.mobile .airspace-timebar.is-collapsed .airspace-time-toggle-clock {
  display: inline-flex;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--fg-label);
}
html.mobile .airspace-timebar.is-collapsed .airspace-time-toggle-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  text-align: left;
}
html.mobile .airspace-time-toggle-time { font-variant-numeric: tabular-nums; }
html.mobile .airspace-time-toggle-stale {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--fg-warn-2);
}
html.mobile .airspace-timebar.is-collapsed .airspace-time-toggle-chevron { transform: rotate(180deg); }
/* Not now: the time is the amber of the bar's own Now and thumb, so a pill over
   another day's NOTAMs does not read as the live picture. The stale colours
   (.is-stale) keep the edge, being the stronger warning. */
html.mobile .airspace-timebar.is-collapsed.is-not-now:not(.is-stale) { border-color: rgba(var(--rgb-amber), 0.8); }
html.mobile .airspace-timebar.is-collapsed.is-not-now .airspace-time-toggle-clock,
html.mobile .airspace-timebar.is-collapsed.is-not-now .airspace-time-toggle-time { color: var(--fg-amber); }
/* The fold settles into place rather than jumping; never under reduced motion.
   `translate`, not `transform`, which centres the bar. */
@keyframes airspace-timebar-settle {
  from { opacity: 0.4; translate: 0 0.5rem; }
  to { opacity: 1; translate: 0 0; }
}
html.mobile .airspace-timebar.is-settling { animation: airspace-timebar-settle 0.16s ease-out; }
@media (prefers-reduced-motion: reduce) {
  html.mobile .airspace-timebar.is-settling { animation: none; }
}


.notam-popup-container .maplibregl-popup-content { padding: 0; }

/* Overlapping NOTAMs: stack them all in one scroll box kept to about a single
   card's height, so the popup stays compact and you scroll through the ones
   underneath the top polygon. */
.notam-popup-stack { display: grid; }
.notam-popup-stack-head {
  padding: 0.5rem 0.75rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-subtle);
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  background: rgba(var(--rgb-slate), 0.08);
}
.notam-popup-scroll {
  max-height: 15rem;   /* ~ one listing tall; scroll for the rest */
  overflow-y: auto;
}

.compliance-report.empty { color: var(--muted); font-size: 0.875rem; }
.compliance-report-section { margin-top: 0.875rem; }
.compliance-report-section > strong { color: var(--muted); font-size: 0.8125rem; }
.compliance-report-item {
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  margin-top: 0.5rem;
}
.compliance-report-flags { margin: 0.375rem 0 0; padding-left: 0.25rem; list-style: none; }
.compliance-report-flags li { font-size: 0.8125rem; color: var(--fg-body); margin-top: 0.25rem; }
.compliance-report-flags .status-pill { margin-right: 0.375rem; }
.eligibility-rule-active-label { margin-top: 1rem; }

/* Compliance rule editor: group fields into When / Require / Otherwise steps so
   the relationship between them reads as a sentence. Each step is separated and
   led by a coloured chip. */
.rule-section { margin-top: 1.125rem; padding-top: 1rem; border-top: 0.0625rem solid var(--line); }
.rule-section-head { font-size: 0.9375rem; font-weight: 600; color: var(--fg-body); }
.rule-section-step {
  display: inline-block;
  margin-right: 0.5rem;
  padding: 0.0625rem 0.5625rem;
  border-radius: 62.4375rem;
  background: rgba(var(--rgb-green), 0.12);
  border: 0.0625rem solid rgba(var(--rgb-green), 0.32);
  color: var(--green);
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  vertical-align: 0.0625rem;
}
.rule-section-hint { color: var(--muted); font-size: 0.8125rem; margin: 0.25rem 0 0.75rem; }
/* The requirement's parameter fields sit directly under the Requirement select. */
.rule-req-params { margin-top: 0.75rem; }
.queue-item.inactive .queue-name { color: var(--muted); }
.queue-item.inactive .queue-meta { color: var(--fg-faint); }

/* Header row (subtitle + action button) shared by the safety sub-panels. */
.incidents-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}
.incidents-header .card-copy { margin: 0; }

/* Side tabs: a vertical tab rail on the left and the active panel on the right,
   like the Settings page. One set of rules for every page that has them
   (Billing: Quotes / Invoices; Hazards & occurrences). A panel's own actions
   sit in the toolbar inside its card. */
.side-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: row;
  gap: 1.5rem;
}
.side-tabs {
  flex: none;
  width: 11.25rem;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
}
/* Vertical rail: full-width tabs with the active accent bar on the left edge
   (overriding the shared horizontal .side-tab rules above). */
.side-tabs .side-tab {
  width: 100%;
  text-align: left;
  min-height: 2.5rem;
  padding: 0.625rem 1rem;
  font-size: 1.0625rem;
}
.side-tabs .side-tab.active::before {
  left: 0;
  right: auto;
  top: 0;
  bottom: 0;
  width: 0.25rem;
  height: auto;
  border-radius: 0.5rem 0 0 0.5rem;
}
.side-panels {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
}
.side-panel[hidden] { display: none; }

/* Safety page: side tabs (hazard register / occurrence reporting / my actions)
   filling the page height. */
.safety-layout {
  max-width: none;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* The panel's card scrolls: a long register stays inside the page's height and
   the rail stays put. */
.safety-panel > .card { overflow-y: auto; }

/* Safety hazard register: one bordered card per hazard, with a nested list of
   corrective actions. */
.hazard-card {
  border: 0.0625rem solid var(--line);
  border-radius: 0.625rem;
  background: var(--tile-bg);
  padding: 0.875rem;
  margin-bottom: 0.75rem;
}
.hazard-card:last-child { margin-bottom: 0; }
.hazard-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.75rem;
  cursor: pointer;
}
.hazard-head:hover .queue-name { text-decoration: underline; }
/* The hazard's name and its lines take the list standard's styles (.list-row-title,
   .list-row-secondary), as every other list's rows do. */
.hazard-pills { display: flex; gap: 0.375rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.hazard-actions { margin-top: 0.75rem; border-top: 0.0625rem solid var(--line); padding-top: 0.625rem; }
.hazard-actions-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
}
.hazard-actions-head span { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.hazard-actions-head button { padding: 0.25rem 0.75rem; font-size: 0.75rem; }
.hazard-actions-list { display: flex; flex-direction: column; gap: 0.375rem; }
.hazard-actions-empty { color: var(--muted); font-size: 0.8125rem; }
.action-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.625rem;
  padding: 0.375rem 0.625rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0.5rem;
  background: var(--tile-bg-3);
  cursor: pointer;
}
.action-desc { font-size: 0.875rem; }
.hazard-title-row {
  display: flex;
  align-items: center;
  gap: 0.5625rem;
  flex-wrap: wrap;
}
.action-row:hover { background: rgba(var(--rgb-overlay), 0.045); }
/* Completed actions are dimmed slightly rather than struck through. */
.action-row.done { opacity: 0.55; }

/* Occurrence rows are click-to-edit for managers (admins / Health & Safety). */
.incident-row-clickable { cursor: pointer; }
.incident-row-clickable:hover td { background: rgba(var(--rgb-slate), 0.07); }
.incident-desc-cell {
  max-width: 21.25rem;
  color: var(--fg-soft);
  white-space: normal;
}
.incident-outcome-cell {
  max-width: 20rem;
  color: var(--fg-soft);
  white-space: normal;
}
/* Specificity, not preference. `.data-table th, .data-table td` sets
   white-space: nowrap and outranks a bare class, so the two rules above set a
   max-width that nothing enforced: a long description ran straight over the
   Outcome column and into the severity pill rather than wrapping inside its own
   cell. Measured at 1440: the description cell's content was 342px wide in a
   289px box. Leading with .incidents-table takes the selector past it. */
.incidents-table td.incident-desc-cell,
.incidents-table td.incident-outcome-cell {
  white-space: normal;
}
/* The job it happened on (item 42): a link to the job's card, wrapping inside a cell of its own. */
.incidents-table td.incident-cell-job {
  max-width: 11rem;
  white-space: normal;
  overflow-wrap: anywhere;
}
.incidents-table td.incident-cell-job .incident-job-link { text-align: left; }
/* The side rail and its gap take about 190px from the card, and the nine-column Occurrence
   list, which fits a full card, was cut off at its right edge (Investigator, Reported by,
   Status) with nothing to say so (measured at 1100, 2026-11-03). Two steps: under 1280 the
   rail is narrower; under 1360 the list wraps its text columns and tightens its cells
   instead of scrolling, and the figures (When, Severity, CAA, Status) keep their lines.
   Billing's own rail from 1280 up is as it was. */
@media (max-width: 1279px) {
  html:not(.mobile) .side-body { gap: 1rem; }
  html:not(.mobile) .side-tabs { width: 9.5rem; }
  html:not(.mobile) .side-tabs .side-tab { padding-inline: 0.75rem; }
}
@media (max-width: 1359px) {
  html:not(.mobile) .incidents-table th,
  html:not(.mobile) .incidents-table td { padding-inline: 0.5rem; }
  html:not(.mobile) .incidents-table td.incident-cell-title,
  html:not(.mobile) .incidents-table td.incident-cell-detail,
  html:not(.mobile) .incidents-table td.incident-cell-when {
    white-space: normal;
    overflow-wrap: anywhere;
  }
  html:not(.mobile) .incidents-table td.incident-cell-title { max-width: 11rem; }
  html:not(.mobile) .incidents-table td.incident-desc-cell { max-width: 11rem; }
  html:not(.mobile) .incidents-table td.incident-outcome-cell { max-width: 9rem; }
}
/* Closed occurrence pill: a dimmer, greyed-out version of the open grey. */
.status-occurrence-closed {
  background: var(--pill-closed-bg);
  color: var(--fg-faint-2);
}
/* Keep the occurrence modal's Save and Delete buttons the same size and aligned
   (the submit button otherwise carries a 16px top margin the Delete lacks). */
#incidentSubmitButton,
#incidentDeleteButton {
  height: 2.25rem;
  margin: 0;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Read-only overview modal (occurrence / hazard for non-managers). */
.detail-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.625rem 1.125rem;
  margin: 0;
}
.detail-list dt {
  align-self: start;
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding-top: 0.125rem;
}
.detail-list dd {
  margin: 0;
  color: var(--fg-body);
  font-size: 0.875rem;
  line-height: 1.4;
  white-space: pre-wrap;
}
/* The optional block under the detail list - the overview modal's second
   child. The card scrolls (max-height + overflow-y: auto), so this grows the
   scroll length rather than being clipped; the rule that matters is the divider
   above it, which is what stops a file list reading as more detail rows. */
.detail-extra {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1.125rem;
  padding-top: 1.125rem;
  border-top: 1px solid rgba(var(--rgb-slate), 0.25);
}
.detail-extra input[type="file"] { max-width: 100%; }

/* Occurrence evidence: the files attached to one occurrence, in the occurrence
   modal and in the read-only overview. */
.evidence-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.evidence-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
/* The filename is the control that opens the viewer, so it stays a button for
   the keyboard and screen readers, but it should read as the file's name
   rather than as a second action beside Remove. */
.evidence-name {
  flex: 1;
  min-width: 0;
  justify-content: flex-start;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.evidence-meta { font-size: 0.75rem; color: var(--muted); }
.evidence-remove { flex: 0 0 auto; }

.action-meta { font-size: 0.75rem; color: var(--muted); margin-top: 0.125rem; }

/* Financials: summary stat cards + quote/invoice line-item editor. */
/* Fill the page height as a flex column: summary row + tab bar stay their
   natural height, and the active Quotes/Invoices panel grows to fill the rest. */
#financialsPage .financials-layout {
  max-width: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}
.financials-summary {
  flex: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.375rem, 1fr));
  gap: 0.625rem;
  margin-bottom: 0;
}
.financials-summary .stat {
  border: 0.0625rem solid var(--line);
  border-radius: 0.625rem;
  background: var(--tile-bg);
  padding: 0.5rem 0.75rem;
}
.financials-summary .stat .stat-label {
  font-size: 0.75rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.financials-summary .stat .stat-value { font-size: 1.125rem; font-weight: 600; margin-top: 0.125rem; }
.financials-summary .stat.warn .stat-value { color: var(--danger); }
/* Sibling of the summary grid inside the page's flex column: `flex: none` so it
   takes only the height of its sentence and the billing panel below keeps the
   rest. Hidden entirely once the figures are current. */
.financials-load-notice {
  flex: none;
  margin: 0;
  font-size: 0.875rem;
}

/* The active billing panel grows to fill remaining height; its card and table
   stretch to fit, with the table body scrolling internally. */
.side-panel {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
}
.side-panel > .card {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 0;
}
/* min-width: 0 all the way down: a table wider than the card scrolls inside the
   card and never widens the page. */
.billing-panel > .card > .table-wrap {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}
.billing-add-button { flex: none; }
.line-items { margin: 0.875rem 0; }
.line-items-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
}
.line-items-head span { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.line-item-row {
  display: grid;
  grid-template-columns: 1fr 5rem 6.875rem auto;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.375rem;
}
/* Line-item inputs (quote/invoice) live outside a .form-field, so apply the
   app's field theming directly instead of leaving bare browser-default boxes. */
.line-item-row input {
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.625rem 0.6875rem;
  font-size: 0.875rem;
}
.line-item-row input:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
  outline: none;
}
.line-item-row input::placeholder { color: var(--muted); }
.line-item-row .line-remove {
  padding: 0.25rem 0.625rem;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
}
.line-items-total { text-align: right; margin-top: 0.5rem; font-size: 0.875rem; color: var(--muted); }
.line-items-total strong { color: var(--text); }

/* Close-job modal: intro copy, resource rows, per-person time rows. */
.job-close-intro { margin: 0 0 0.875rem; }
.resource-row {
  display: grid;
  grid-template-columns: 1fr 5.625rem auto;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.5rem;
}
/* These inputs sit outside a .form-field, so they need the app's field theming
   applied directly (rather than rendering as bare browser-default boxes), plus
   a touch more height/text so the resources box reads comfortably. */
.resource-row input,
.labor-row input {
  width: 100%;
  box-sizing: border-box;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: 0.75rem 0.75rem;
  font-size: 0.9375rem;
}
.resource-row input:focus,
.labor-row input:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
  outline: none;
}
.resource-row input::placeholder,
.labor-row input::placeholder { color: var(--muted); }
.resource-row .line-remove {
  padding: 0.25rem 0.625rem;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
}
.job-close-labor { margin: 0.875rem 0 0.25rem; }
.labor-row {
  display: grid;
  grid-template-columns: 1fr 7.5rem;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.5rem;
}
.labor-row .labor-name { font-size: 0.875rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Somebody the plan never had, whose log on the job put them here. */
.labor-row .labor-who { display: flex; flex-direction: column; min-width: 0; }
.labor-row .labor-note { font-size: 0.75rem; color: var(--muted); }

/* ux:M1 - Billing money fixes. The Close job dialog's load state (the form shows
   only once its details are in), where its resources came from, and what Close
   job does; a priced line with no description, flagged; the hint under a quote
   or invoice saying why its actions are off; a converted quote's invoice link in
   the action row; and the invoice row a Convert has just created. */
.job-close-load { margin: 0.5rem 0 0.875rem; }
.job-close-seeded { margin: 0 0 0.5rem; }
.job-close-consequences { margin: 0.75rem 0 0; font-size: 0.8125rem; }
.line-item-row .li-desc[aria-invalid="true"] { border-color: var(--danger); }
.billing-record-hint { margin: 0 0 0.5rem; font-size: 0.8125rem; }
.modal-actions-row .quote-converted-link { align-self: center; }
.financial-row.selected { background: rgba(var(--rgb-blue), 0.12); }

/* Alerts: the bell beside the organisation name, and its dropdown feed. */
/* Org logo + name in the top bar - phone only (the dashboard page carries its
   own block on desktop, and the sidebar names the organisation on every other);
   shown by the html.mobile header rules. */
.header-org { display: none; }
/* Both of the bell's homes anchor the panel: the sidebar footer on a desktop,
   the phone's sticky bar on a phone. Nothing on the way up to either hides its
   overflow or carries a transform, so the panel - wider than the sidebar it
   hangs in - is drawn over the page beside it rather than cut off at the
   sidebar's edge. */
.alerts-menu { position: relative; }
/* No bell for this account (applyOrgNavVisibility hides it in the Platform
   organisation only - every member of an operating organisation has it, for
   their own "For you" list): take the wrapper out too, or it still claims its
   share of the row's gap. */
.alerts-menu:has(> .alerts-bell[hidden]) { display: none; }
/* THE GLYPH IS CENTRED BY FLEX, NOT BY THE BOX FITTING IT. The button is wider
   and taller than its padding and the icon on a phone (the touch floor below
   gives it min-width and min-height), and as a plain button the icon - a block,
   so as wide as the content box - kept its SVG at the left edge: all of the
   extra width landed on the right and the bell sat 3.4px left of centre (Josh,
   2026-11-01, from the installed iPhone app). Flex centres it on both axes
   whatever size the floor makes the box, instead of relying on the browser's
   own centring of a button's contents. The badge is absolutely positioned
   (below), so it is not a flex item and does not push the icon over. The
   sidebar's rule (.app-nav-footer .alerts-bell) sets its own justify-content:
   there the chip is drawn on the icon, which centres the SVG by its own equal
   padding. ux-bell-centred-frontend.mjs holds all of this. */
.alerts-bell {
  width: auto;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5625rem 0.75rem;
  font-size: 1.125rem;
  line-height: 1;
  background: var(--secondary-bg);
  /* The foreground that goes with that background. The button's own default is white
     for a solid control, and on the light theme's pale --secondary-bg an icon drawn in
     currentColor vanished (the emoji it replaced was drawn in its own colours). */
  color: var(--secondary-fg);
  border-radius: 0.625rem;
}
.alerts-bell-icon { display: block; }
/* The bell is an icon like every other one (it was an emoji): sized with the button's
   own font size, drawn in the control's text colour. */
.alerts-bell-icon svg { display: block; width: 1.125em; height: 1.125em; }
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.125rem;
  height: 1.125rem;
  padding: 0 0.3125rem;
  border-radius: 0.5625rem;
  background: var(--badge-bg);
  color: var(--on-solid);
  font-size: 0.6875rem;
  font-weight: 600;
}
/* On the bell the badge is a corner overlay; elsewhere it flows inline. */
.alerts-bell .nav-badge {
  position: absolute;
  top: -0.3125rem;
  right: -0.3125rem;
  border: 0.125rem solid var(--bg);
}
/* UPWARDS AND TO THE RIGHT, because the bell is now in the bottom-left corner
   of the screen. Downwards it would have opened off the bottom edge, and
   right-aligned it would have run off the left one. 66vh caps it well inside a
   viewport it starts at the foot of, so a long feed scrolls inside the panel
   rather than growing past the top of the screen. */
.alerts-dropdown {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 0;
  width: 23.75rem;
  max-width: 90vw;
  max-height: 66vh;
  overflow-y: auto;
  z-index: 60;
  padding: 0.75rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.75rem;
  /* Opaque, like every other sheet (--sheet-bg): --panel is translucent, and the
     page behind it showed through the text. */
  background: var(--sheet-bg);
  box-shadow: var(--shadow-alerts);
  scrollbar-width: thin;
}
/* "Alerts", then on the right the count and (For you, something unread) Mark all
   read: one line, so on a phone the 44px Mark all read shares the header's height
   instead of standing on a row of its own with nothing beside it. */
.alerts-dropdown-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-weight: 700;
  font-size: 0.9375rem;
  margin: 0.125rem 0.125rem 0.625rem;
}
.alerts-dropdown-counts { margin-left: auto; }
.alert-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.75rem;
  margin-bottom: 0.4375rem;
  border: 0.0625rem solid var(--line);
  border-left-width: 0.1875rem;
  border-radius: 0.5rem;
  background: var(--tile-bg);
  cursor: pointer;
}
.alert-row:hover { background: rgba(var(--rgb-overlay), 0.045); }
.alert-row.critical { border-left-color: var(--danger); }
.alert-row.warning { border-left-color: var(--amber-rule); }
.alert-row .alert-body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.alert-row .alert-title { font-weight: 500; }
.alert-row .alert-detail { font-size: 0.8125rem; color: var(--muted); margin-top: 0.125rem; }

/* Log-incident modal: roomier description box and breathing room before the
   submit button. */
/* Checklist templates (Settings) + completion modal items. */
.checklist-templates { display: flex; flex-direction: column; gap: 0.875rem; }
.checklist-template-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  border-radius: 0.5rem;
  padding: 0.75rem;
}
.checklist-template-card textarea { min-height: 7.5rem; }
.checklist-template-actions { display: flex; gap: 0.5rem; }
.checklist-complete-button { margin: 0 0 0.75rem; }

/* The "Required" toggle on a checklist template's item rows. Sits between the
   label and the delete button and must never squeeze the label, so it is
   flex: 0 0 auto with its text held on one line. */
.list-editor-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  white-space: nowrap;
  font-size: 0.75rem;
  color: var(--muted);
  cursor: pointer;
}
.list-editor-toggle-box { margin: 0; }

/* Running a checklist. This is the one screen in the product that is used
   standing at an aircraft rather than sitting at a desk, so the answers are
   buttons at the touch-target floor rather than a checkbox, and the row reads
   top-to-bottom (label, then answers, then the note a failure needs) so it
   survives a narrow phone without any of it being cut off. */
.checklist-run-items { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.75rem 0; }
.checklist-run-item {
  display: flex;
  flex-direction: column;
  gap: 0.4375rem;
  padding: 0.625rem 0.6875rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.05);
}
.checklist-run-item-failed { border-color: rgba(var(--rgb-red), 0.45); }
.checklist-run-label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.35;
}
/* .field-choices is the same control used in a form field rather than a
   checklist row: a small set of mutually exclusive answers picked by tapping,
   not by opening a select. Reporting a defect is the other thing in this
   product done standing at the aircraft, so severity and defect status are
   answered the same way a checklist item is. */
.checklist-run-choices,
.field-choices { display: flex; gap: 0.4375rem; }
/* Equal thirds so the buttons stay in one row on the narrowest phone, and the
   touch floor on the short axis. --touch-min is only declared under
   html.mobile, so the fallback is what desktop actually uses. */
.checklist-choice,
.field-choice {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--touch-min, 2.5rem);
  padding: 0.375rem 0.5rem;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
  font-weight: 600;
}
.checklist-choice:hover:not(:disabled) { color: var(--text); }
.checklist-choice.selected { color: var(--text); }
.checklist-choice-pass.selected {
  background: var(--green-soft);
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--pill-green-fg);
}
.checklist-choice-fail.selected {
  background: rgba(var(--rgb-red), 0.2);
  border-color: rgba(var(--rgb-red), 0.55);
  color: var(--pill-red-fg);
}
.checklist-choice-na.selected {
  background: rgba(var(--rgb-slate), 0.2);
  border-color: rgba(var(--rgb-slate), 0.5);
}
/* The note lives inside the item row, not a .form-field, so it does not pick up
   the app's input surface and renders as a stark white browser default on the
   dark panel. Same trap the risk checklist's in-table inputs hit; same fix:
   state the surface (guide 2/6) explicitly. */
.checklist-run-note {
  width: 100%;
  padding: 0.375rem 0.5rem;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  font-family: inherit;
  font-size: 0.8125rem;
  outline: none;
}
.checklist-run-note:focus-visible { border-color: var(--focus-ring); }

.field-choice:hover:not(:disabled) { color: var(--text); }
.field-choice.selected { color: var(--text); }
.field-choice:disabled { opacity: 0.45; cursor: not-allowed; }
/* Severity is triage, so it climbs grey → amber → red. None of these is green:
   there is no reassuring answer to "how bad is it", and green is reserved for
   emphasis rather than a large fill (guide §1/§2). */
.field-choice-minor.selected {
  background: rgba(var(--rgb-slate), 0.2);
  border-color: rgba(var(--rgb-slate), 0.5);
}
.field-choice-major.selected {
  background: rgba(var(--rgb-amber), 0.2);
  border-color: rgba(var(--rgb-amber), 0.55);
  color: var(--pill-amber-fg);
}
.field-choice-critical.selected {
  background: rgba(var(--rgb-red), 0.2);
  border-color: rgba(var(--rgb-red), 0.55);
  color: var(--pill-red-fg);
}
/* Defect status mirrors the pills the list shows for the same three values, so
   picking one here looks like the pill it will become. */
.field-choice-open.selected {
  background: rgba(var(--rgb-blue), 0.2);
  border-color: rgba(var(--rgb-blue), 0.55);
  color: var(--pill-blue-fg);
}
.field-choice-deferred.selected {
  background: rgba(var(--rgb-amber), 0.2);
  border-color: rgba(var(--rgb-amber), 0.55);
  color: var(--pill-amber-fg);
}
.field-choice-resolved.selected {
  background: var(--green-soft);
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--pill-green-fg);
}

/* The consequence of ticking "this defect stops the aircraft flying", stated
   before the save rather than discovered later when a job refuses to start.
   Same panel geometry as the job scheduling warning; the danger tint because
   this one CHANGES the aircraft's state instead of flagging a clash. */
.defect-grounding-notice {
  background: rgba(var(--rgb-red), 0.12);
  border: 0.0625rem solid rgba(var(--rgb-red), 0.45);
  color: var(--text);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.8125rem;
  line-height: 1.4;
}

/* Reading a completed checklist: read-only, so the answer is a pill rather
   than a control, and a note hangs under the item it explains. */
.checklist-detail-items { display: flex; flex-direction: column; gap: 0.375rem; margin: 0.75rem 0; }
.checklist-detail-item {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.25rem 0.625rem;
  padding: 0.5rem 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}
.checklist-detail-label { font-size: 0.875rem; line-height: 1.35; }
.checklist-detail-result {
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  padding: 0.1875rem 0.4375rem;
  border-radius: 62.4375rem;
  font-size: 0.6875rem;
  font-weight: 650;
  white-space: nowrap;
}
.checklist-detail-result-pass { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.checklist-detail-result-fail { background: rgba(var(--rgb-red), 0.24); color: var(--pill-red-fg); }
.checklist-detail-result-na { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-neutral); }
.checklist-detail-note {
  grid-column: 1 / -1;
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.4;
}

/* Battery-only equipment fields (shown when category = battery). */
.equipment-battery-field[hidden] { display: none; }

#incidentLogForm textarea { min-height: 8.75rem; }

/* Personnel master-detail: a list of people (left) and the selected person's
   details (right), mirroring the Logs page's list + panel arrangement. */
.personnel-layout {
  display: grid;
  grid-template-columns: minmax(17.5rem, 21.25rem) minmax(0, 1fr);
  gap: 0.75rem;
  height: 100%;
}

.personnel-list-card,
.personnel-detail-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.personnel-list-wrap {
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
  margin-top: 0.5rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

/* The generic .queue-list caps itself at 350px; here the wrap owns the
   scrolling and height, so let the list fill it right down to the button. */
.personnel-list-wrap .queue-list {
  max-height: none;
}

.personnel-detail-card {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--rgb-slate), 0.35) transparent;
}

.personnel-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5625rem 0.6875rem;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: left;
}
.personnel-row + .personnel-row { margin-top: 0.125rem; }

.personnel-row:hover { background: rgba(var(--rgb-slate), 0.08); }
.personnel-row.selected { background: rgba(var(--rgb-green), 0.14); }
.personnel-row .queue-name { min-width: 0; }
.personnel-row-summary {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  flex: none;
}

.personnel-detail-empty {
  flex: 1;
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 0.875rem;
}
.personnel-detail-empty[hidden],
.personnel-detail-body[hidden] { display: none; }

.personnel-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}
.personnel-detail-lastflown,
.personnel-detail-meta { margin: 0.125rem 0 0; color: var(--muted); }
.personnel-detail-lastflown { margin-top: 0.625rem; }
.personnel-detail-meta { font-size: 0.8125rem; opacity: 0.85; }

/* Data & privacy card (P2 data lifecycle) */
.data-privacy-section { margin-top: 1.125rem; padding-top: 1.125rem; border-top: 0.0625rem solid var(--line); }
.data-privacy-section:first-of-type { border-top: 0; margin-top: 0.5rem; padding-top: 0; }
.data-privacy-subhead {
  display: block; font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.375rem;
}
.data-privacy-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
.data-privacy-preview { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.375rem; }
.data-privacy-preview-row {
  font-size: 0.85rem; padding: 0.375rem 0.625rem; border-radius: 0.375rem;
  background: rgba(var(--rgb-mist), 0.08);
}
.data-privacy-log { margin-top: 0.375rem; display: flex; flex-direction: column; gap: 0.3125rem; font-size: 0.85rem; }
.data-privacy-log-row { color: var(--muted); }
.data-privacy-log-row .dp-log-type { text-transform: capitalize; font-weight: 600; color: var(--text); }

/* Access-review banner on the Users settings panel */
.access-review-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap; padding: 0.875rem 1.125rem; margin-bottom: 1rem;
  border: 0.0625rem solid var(--line); border-radius: 0.5rem;
  background: rgba(var(--rgb-mist), 0.06);
}
.access-review-text { display: flex; flex-direction: column; gap: 0.125rem; min-width: 16rem; flex: 1; }
.access-review-text strong { font-size: 0.95rem; }
.access-review-banner .card-copy { margin: 0; font-size: 0.82rem; }

.personnel-detail-form { margin-top: 1.125rem; }

/* Save changes / Deactivate are peer form actions, not a modal footer: keep
   them content-width in a left-aligned row (§5), not stretched to fill the
   detail column by .modal-actions-row's flex:1. */
.personnel-detail-form .modal-actions-row button { flex: 0 0 auto; }

.personnel-detail-secondary-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}

/* Rows here are a checkbox and one line of text, unlike the role lists this
   class is otherwise used for, whose two- and three-line descriptions need the
   top alignment .role-checkboxes sets. Without this they sit flush at the top of
   the mobile touch target with the slack pooled underneath. */
#eligibilityRuleCerts label {
  align-items: center;
}

/* Access vs Data & privacy. Deactivate and "Erase personal data" are both
   destructive and both end a person's access, so side by side with nothing
   between them they read as alternatives to each other. They are not: erasure
   deactivates the account as part of what it does, and cannot be undone. */
.personnel-action-group {
  /* Matches .modal-actions' 1rem: the Save row above is the last child of its
     form, so it contributes no bottom margin and this is the whole gap. The
     rule above each group is what makes it read as its own section rather than
     a caption over yet another button row. */
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
}

/* A section heading, at the same 1rem as .modal-section-title - these name two
   genuinely different kinds of action and need to be read before the buttons
   under them are, not skimmed past as a label. */
.action-group-label {
  display: block;
  margin: 0 0 0.625rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--fg-body);
}

/* One line under a group heading saying HOW FAR that group's buttons reach -
   your organisation only, or every organisation this person works for. The
   two groups hold buttons that read almost identically ("Take off roster" /
   "Lock account"), so the sentence is the thing that stops an admin taking a
   contractor out of a competitor's operation by accident. Sits between the
   heading and the buttons deliberately: read before the click, not after. */
.personnel-action-note {
  margin: -0.25rem 0 0.625rem;
}

/* The note and the answer that sit UNDER their buttons rather than over them: why
   "Email reset link" is greyed out, and what the click did. The rule above pulls a
   note up to its heading, which here would pull it into the button row; and the
   answer is here, not in the form's message line far above, so it is read where
   the button was pressed. */
#personnelEmailResetNote,
#personnelEmailResetMessage {
  margin: 0.625rem 0 0;
  /* "Reset link sent to <address>": an address has no spaces to break at, so a long one
     ran past the card on a phone and scrolled the page sideways. */
  overflow-wrap: anywhere;
}

/* The person's current standing, directly under their name. Only drawn when
   there is something to say - an ordinary active member gets no line at all,
   so the presence of one is itself the signal. */
.personnel-detail-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem 0.5rem;
  margin: 0.375rem 0 0;
}

.personnel-detail-section {
  margin-top: 1.375rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
  padding-top: 1rem;
}
.personnel-detail-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.625rem;
}

/* Certification editor modal: wider than the default card, with an add form. */
.personnel-cert-card { min-width: min(42.5rem, 92vw); }
.personnel-cert-add { margin-top: 0.875rem; }
.personnel-cert-row-actions { display: flex; align-items: center; gap: 0.5rem; }

/* Account (org switcher) modal: gear-icon settings button top-right, org list,
   SPS logo, and a full-width logout anchored at the bottom. */
.orgswitcher-card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  /* As wide as what it holds, not the 40rem every dialog card is: it carries a name,
     an email, two rows and the sign-out button. */
  width: min(20rem, calc(100vw - 1rem));
}
.orgswitcher-settings-button {
  width: 2.125rem;
  height: 2.125rem;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  flex: 0 0 auto;
}
.orgswitcher-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
}
.orgswitcher-powered-by {
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.orgswitcher-brand-logo {
  align-self: center;
  max-width: 8.75rem;
  opacity: 0.85;
}
.orgswitcher-logout-button { width: 100%; }
/* The card is a flex column with its own gap, so .form-message's stacking
   margin would land this a whole extra line away from the list it is about. */
.orgswitcher-error { margin: 0; }
/* The row whose switch is in flight. Dimmed rather than spinner'd: the answer
   is a page reload a moment later, and a spinner that is always replaced by a
   new document is animation nobody sees finish. */
.org-switcher-row-busy {
  opacity: 0.55;
  pointer-events: none;
}

/* The add-user modal's submit button needs breathing room above it. */
#userCreateForm button[type="submit"] {
  margin-top: 1rem;
}
/* More than five memberships -> the org list becomes a scroll box. */
.orgswitcher-list-scroll {
  max-height: 16.25rem;
  overflow-y: auto;
}
/* Platform account pinned to the top, staying visible while the list scrolls. */
.org-switcher-row-pinned {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-raised);
}

/* Risk matrix entries: grouped by fixed category, with a cleaner, more modern
   type treatment for the entry text than the app's default. */
.risk-entries { display: flex; flex-direction: column; gap: 1.25rem; }
.risk-entry-section { display: flex; flex-direction: column; gap: 0.5rem; }
.risk-entry-section > .secondary-button { align-self: flex-start; }
.risk-entry-category-heading {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}
.risk-entry-hint { margin: -0.125rem 0 0.25rem; font-size: 0.75rem; color: var(--muted); }
.risk-entry-textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  font-size: 0.8125rem;
  line-height: 1.5;
}

/* Reusable list editor (risk hazard templates + checklist templates). */
.list-editor-rows { border-top: 0.03125rem solid var(--line); }
.list-editor-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.14);
}
.list-editor-row.dragging { opacity: 0.45; }
.list-editor-grip {
  flex: 0 0 auto;
  cursor: grab;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  user-select: none;
}
.list-editor-grip:active { cursor: grabbing; }
.list-editor-input { flex: 1 1 auto; width: 100%; min-width: 0; }
/* A hazard row sits outside any .form-field, so its field took the browser's
   own look (white on the dark theme); give it the app's, as a checklist item
   already has inside its .form-field. */
.list-editor-input {
  box-sizing: border-box;
  margin: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  outline: none;
}
.list-editor-del,
.list-editor-move {
  flex: 0 0 auto;
  width: 2rem;
  padding: 0.3125rem 0;
  background: transparent;
  border: 0.0625rem solid var(--line);
  color: var(--muted);
  line-height: 1;
  font-size: 0.9375rem;
}
.list-editor-del:hover:not(:disabled),
.list-editor-move:hover:not([aria-disabled="true"]) { color: var(--text); background: rgba(var(--rgb-overlay), 0.045); }
/* A row at the top or bottom has nowhere to go that way. aria-disabled, not
   disabled, so a button that has just moved its row to an edge keeps focus and
   the next press does nothing instead of dropping the keyboard user's place. */
.list-editor-move[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.list-editor-add { width: auto; margin-top: 0.5rem; }
.risk-entry-table,
.risk-entry-table textarea {
  font-family: inherit;
  font-size: 0.8125rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
}
/* Give the entry text fields room: full-width table with evenly-sized columns
   (a narrow actions column) and textareas that fill their cell. */
.risk-entry-table { width: 100%; table-layout: fixed; }
.risk-entry-table textarea { width: 100%; box-sizing: border-box; }
.risk-entry-table .hazard-col-actions { width: 6rem; }

#needsReviewBadge {
  border: 0;
  cursor: pointer;
  font-family: inherit;
  margin: 0;
}
/* In the Jobs panel's header, to the right beside the collapse toggle. */
.filters-card-header #needsReviewBadge {
  margin-left: auto;
  min-height: 0;
  padding: 0.1875rem 0.4375rem;
  font-size: 0.6875rem;
}
#needsReviewBadge[hidden] {
  display: none;
}

#needsReviewBadge:hover {
  filter: brightness(1.12);
}

.upload-modal-status-text {
  margin: 0.625rem 0 0;
  color: var(--fg-success);
  font-weight: 650;
}

/* The text of the page notice (#status, inside .page-notice - see the ux:U0b
   block). It owns no space of its own any more: the box around it floats, so an
   empty one costs the page nothing and there is no :empty rule to take it out of
   the accessibility tree, which a live region must stay in. */
.status {
  color: var(--fg-success);
  font-size: 0.875rem;
  margin: 0;
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.error { color: var(--fg-error); }

/* MAP BUTTONS: MapLibre's zoom, compass and locate buttons, on every map in the
   app - Flights, Airspace, the job's area editor, its expanded location map and
   the report maps. MapLibre draws them as white boxes with grey icons in both
   themes; they wear the app's own map chrome instead (Josh, 2026-11-02: "the
   locate me and orientation buttons should be properly themed"). On a computer
   that is the map bar's (.map-context-bar): its floating surface, hairline,
   corner and dividers. On a phone it is the floating buttons' (.mobile-map-fab,
   the Logs map's layers button), in the mobile section below. The site
   questionnaire's map is not one of these: site.html has a stylesheet of its own.

   The hairline (--map-ctrl-edge) is drawn INSIDE the group, as an inset
   shadow, not as a border: a border would add to the buttons' size, which the
   phone's touch floor and the map-corners arithmetic both price exactly, and a
   lone button would come out larger than the layers button beside it.
   --map-ctrl-r is the group's corner. The buttons at the group's ends take it
   on their outer corners, so a hover, a press and a focused button's inset ring
   follow the group's curve instead of being cut off by it (MapLibre rounds a
   focused end button by a fixed 4px, and nothing at rest); the corners next to
   a divider stay square, so no divider bends. */
.maplibregl-ctrl.maplibregl-ctrl-group {
  --map-ctrl-r: 0.5rem;
  --map-ctrl-edge: rgba(var(--rgb-slate), 0.16);
  border-radius: var(--map-ctrl-r);
  overflow: hidden;
  background: var(--map-float-bg-2);
  box-shadow: inset 0 0 0 0.0625rem var(--map-ctrl-edge), var(--shadow-map-controls);
  backdrop-filter: var(--backdrop-float);
}
/* Where the system forces its own colours it drops every shadow, the hairline
   with it, so the group gets a real edge there. */
@media (forced-colors: active) {
  .maplibregl-ctrl.maplibregl-ctrl-group { border: 0.0625rem solid; }
}
.maplibregl-ctrl-group button {
  color: var(--fg-control-2);
  border-radius: 0;
}
.maplibregl-ctrl.maplibregl-ctrl-group button:first-child { border-radius: var(--map-ctrl-r) var(--map-ctrl-r) 0 0; }
.maplibregl-ctrl.maplibregl-ctrl-group button:last-child { border-radius: 0 0 var(--map-ctrl-r) var(--map-ctrl-r); }
.maplibregl-ctrl.maplibregl-ctrl-group button:only-child { border-radius: var(--map-ctrl-r); }
.maplibregl-ctrl-group button + button {
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}
/* MapLibre tints a hovered button black at 5%, which a tap leaves stuck on a
   phone's button, and the app brightens every hovered button. Neither, here:
   a pointer that hovers gets the map bar's own hover, and a press shows on
   every device. */
.maplibregl-ctrl-group button:not(:disabled):hover {
  background-color: transparent;
  filter: none;
}
@media (hover: hover) {
  .maplibregl-ctrl-group button:not(:disabled):hover { background-color: rgba(var(--rgb-overlay), 0.06); }
}
.maplibregl-ctrl-group button:not(:disabled):active { background-color: rgba(var(--rgb-overlay), 0.12); }
/* The app paints a disabled button with its own fill (button:disabled); a zoom
   button at the end of its range is MapLibre's to disable, and it is the icon
   that dims, as a disabled control's text does - to --fg-disabled, which the
   colour takes with !important: the app's own disabled-label rules (the dark
   theme's at six classes) would otherwise give it the page's faint text colour,
   which in the light theme is barely off the map buttons' resting ink. */
.maplibregl-ctrl.maplibregl-ctrl-group button:disabled {
  background-color: transparent;
  color: var(--fg-disabled) !important;
}
.maplibregl-ctrl.maplibregl-ctrl-group button:disabled .maplibregl-ctrl-icon { opacity: 1; }

/* The icons are MapLibre's own shapes, drawn as a mask over currentColor, so
   the button's colour is the icon's: the theme's ink at rest, and a token for
   every state below. MapLibre paints each icon - and each state of the locate
   button - as a background image of its own with the fill written into it, at
   up to four classes of specificity; the one !important replaces all of them
   with a fill the mask cuts out. forced-color-adjust keeps that fill when the
   system forces its colours, where it would otherwise become the canvas, the
   same as the button, and the icon would vanish: it is then the button's
   forced text colour. The masks carry no colour of their own (a mask reads
   only its alpha), and the compass's south half is drawn at 35% of it. Only
   the four buttons the app's maps carry are named: a MapLibre control added
   later keeps MapLibre's own icon until it has a mask here, rather than
   showing as a solid square of ink. */
.maplibregl-ctrl-group :is(.maplibregl-ctrl-zoom-in, .maplibregl-ctrl-zoom-out, .maplibregl-ctrl-compass, .maplibregl-ctrl-geolocate) .maplibregl-ctrl-icon {
  background: currentColor !important;
  forced-color-adjust: none;
  -webkit-mask-image: var(--map-ctrl-icon);
  mask-image: var(--map-ctrl-icon);
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 2.125rem 2.125rem;
  mask-size: 2.125rem 2.125rem;
}
.maplibregl-ctrl-group .maplibregl-ctrl-zoom-in {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E");
}
.maplibregl-ctrl-group .maplibregl-ctrl-zoom-out {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E");
}
.maplibregl-ctrl-group .maplibregl-ctrl-compass {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill-opacity='.35' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E");
}
.maplibregl-ctrl-group .maplibregl-ctrl-geolocate {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E");
}

/* The locate button's states, as MapLibre names them on the button (the app
   keeps it enabled through all of them - wireLocateControl):
     -waiting           finding a fix; always with -active. The accent, turning.
     -active            following the fix. The accent, and the group edged in it:
                        the "on" of the Logs map's layers button
                        (.mobile-map-layers-toggle.open).
     -background        the dot is on the map but the map was panned off it, so
                        it no longer follows. The accent, without the edge.
     -active-error      on, but no fix came (refused, timed out). The error red,
                        edged in red while it is still on.
     -background-error  a fix was lost after the map was panned. The red alone.
   The locate button is always a group of its own, so its edge is the group's.

   Colour is never the only cue (green and red are 1.05:1 apart in lightness,
   1.16:1 to a deuteranope, and off and panned-away differ in hue alone), so
   each state also has a shape of its own: off is the ring with its dot;
   following (and finding) fills the ring solid; panned away leaves it hollow,
   no dot; an error crosses it out, with a gap cut either side of the stroke so
   it reads at 18px. Finding also turns. */
.maplibregl-ctrl-group .maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='3.6'/%3E%3C/svg%3E");
}
.maplibregl-ctrl-group .maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E");
}
/* The gap is an alpha mask inside the icon (the whole box less a band along
   the stroke, cut out by the even-odd rule), so the icon still carries no
   colour of its own. */
.maplibregl-ctrl-group .maplibregl-ctrl-geolocate:is(.maplibregl-ctrl-geolocate-active-error, .maplibregl-ctrl-geolocate-background-error) {
  --map-ctrl-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cmask id='gap' mask-type='alpha'%3E%3Cpath fill-rule='evenodd' d='M0 0h20v20H0z M3.54 0.86 19.14 16.46 16.46 19.14 0.86 3.54z'/%3E%3C/mask%3E%3Cg mask='url(%23gap)'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/g%3E%3Cpath d='M3.84 2.56 17.44 16.16 16.16 17.44 2.56 3.84z'/%3E%3C/svg%3E");
}
.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active,
.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background {
  color: var(--green);
}
.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error,
.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error {
  color: var(--fg-error);
}
.maplibregl-ctrl.maplibregl-ctrl-group:has(> button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active) {
  --map-ctrl-edge: rgba(var(--rgb-green), 0.55);
}
.maplibregl-ctrl.maplibregl-ctrl-group:has(> button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error) {
  --map-ctrl-edge: rgba(var(--rgb-red), 0.55);
}
/* Turning is MapLibre's own (maplibregl-spin); with reduced motion asked for,
   the finding state is a slow fade instead of a turn. */
@media (prefers-reduced-motion: reduce) {
  .maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon {
    animation: map-ctrl-finding 1.6s ease-in-out infinite alternate;
  }
}
@keyframes map-ctrl-finding {
  from { opacity: 1; }
  to { opacity: 0.4; }
}
/* Where the system forces its own colours, it forces the button's text colour
   (and so the icon's) to one ButtonText for every state, and drops the edge
   (a shadow). So the locate button takes its colours into its own hands there
   (forced-color-adjust: none), from the system's palette and nothing else:
   following and finding in Highlight, panned away in GrayText, an error in
   LinkText, and the "on" states edged with a real ring of the same colour drawn
   inside the button (an outline here is the keyboard's). The shapes above still
   tell the states apart if two system colours come out alike. */
@media (forced-colors: active) {
  .maplibregl-ctrl.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate {
    forced-color-adjust: none;
    color: ButtonText;
    background-color: transparent;
  }
  .maplibregl-ctrl.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active {
    color: Highlight;
    box-shadow: inset 0 0 0 0.1875rem Highlight;
  }
  .maplibregl-ctrl.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background { color: GrayText; }
  .maplibregl-ctrl.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate:is(.maplibregl-ctrl-geolocate-active-error, .maplibregl-ctrl-geolocate-background-error) { color: LinkText; }
  .maplibregl-ctrl.maplibregl-ctrl-group button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error {
    box-shadow: inset 0 0 0 0.1875rem LinkText;
  }
}

/* Default scale control renders as a filled bar - replace it with a bare
   line and small end ticks so it reads as a ruler instead of a solid block
   sitting on top of the map. */
.maplibregl-ctrl-scale {
  background: none !important;
  border: none !important;
  border-bottom: 0.125rem solid var(--on-map) !important;
  position: relative;
  color: var(--on-map);
  text-shadow: var(--map-label-shadow);
  padding: 0 0.125rem 0.1875rem !important;
}

.maplibregl-ctrl-scale::before,
.maplibregl-ctrl-scale::after {
  content: "";
  position: absolute;
  bottom: -0.125rem;
  width: 0.125rem;
  height: 0.375rem;
  background: var(--on-map);
}

.maplibregl-ctrl-scale::before { left: 0; }
.maplibregl-ctrl-scale::after { right: 0; }

/* Map credits (app.js createMapCreditControl). MapLibre's own attribution is
   off on every map - it writes credits as HTML, and it would not follow the
   basemap - so this is the one credit a map shows.

   ONE QUIET LINE IN THE BOTTOM-RIGHT CORNER, ALWAYS ON SCREEN, and that is a
   licence term rather than a preference: LINZ asks that its credit "always
   visibly show" and Esri that "Powered by Esri" is never hidden, and LINZ
   Hybrid is the default basemap. The provider's short credit
   (.map-credit-short) is therefore never cut; the rest of the line
   (.map-credit-rest) is, with an ellipsis. The line or its "i" opens the whole
   credit, wrapped (.is-expanded), and Escape or a pan closes it.

   THE "i" IS ALWAYS THERE (fixed 2026-09-27). It used to appear only when the
   rest had been cut short, so the basemaps whose credit fits whole -
   OpenStreetMap, OpenTopoMap, LINZ Topographic - carried a strip of text with
   no control on it and no way to see the credit wrapped.

   .is-folded, which hides the line and leaves the "i" alone, is kept for a
   provider whose terms permit it. NOTHING SETS IT: folding away LINZ's or
   Esri's credit would breach the terms above. Check every basemap it would
   apply to before wiring it up.

   The control sets --map-credit-map-w, the map's own width, so a small map gets
   a short line and the scale in the bottom-left corner keeps its room, and
   --map-credit-short-w, the short credit's width on one line, so the cap never
   wraps it where the map has room for it. */
.maplibregl-ctrl.map-credit {
  display: flex;
  align-items: flex-end;
  gap: 0.125rem;
  box-sizing: border-box;
  max-width: max(9rem, min(max(24rem, calc(var(--map-credit-short-w, 0rem) + 1.5rem)), calc(var(--map-credit-map-w, 100vw) - 9.5rem)));
  margin: 0 0.5rem 0.5rem 0;
  padding: 0 0.125rem 0 0.375rem;
  border: 0.0625rem solid var(--map-credit-border);
  border-radius: 0.375rem;
  background: var(--map-credit-bg);
  color: var(--map-credit-fg);
  font-size: 0.625rem;
  line-height: 1.4;
  pointer-events: auto;
}
.maplibregl-ctrl.map-credit.is-expanded {
  max-width: max(9rem, min(max(30rem, calc(var(--map-credit-short-w, 0rem) + 1.5rem)), calc(var(--map-credit-map-w, 100vw) - 9.5rem)));
}
.map-credit a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.125rem;
}
.map-credit a:focus {
  outline: var(--keyboard-ring-width) solid currentColor;
  outline-offset: 0.0625rem;
}
/* One line: the short credit at its own width (it wraps only on a map too
   narrow for it, never cuts), the rest in whatever is left. The gap stands in
   for the space a flex item drops from the start of the rest. */
.map-credit-text {
  display: flex;
  align-items: baseline;
  column-gap: 0.3em;
  flex: 0 1 auto;
  min-width: 0;
  padding: 0.0625rem 0;
}
.map-credit-short { flex: 0 1 auto; }
.map-credit-rest {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* CLOSED, THE CREDIT IS THE PROVIDER'S ATTRIBUTION AND NOTHING ELSE (Josh,
   2026-09-28: "the map attribution still doesn't collapse right down"). The
   rest used to stay on the line, cut short with an ellipsis, so LINZ's closed
   credit read "© CC BY 4.0 LINZ – New Zealand 8m DEM Texture Shade (2012) &
   others 20…" and took most of the map's width to say it.

   What the licences require on screen is the attribution itself - LINZ's
   "© CC BY 4.0 LINZ", Esri's "Powered by Esri" - which is exactly the short
   credit. The datasets and dates after it are detail, and detail belongs
   behind the "i". So the rest is hidden until the credit is opened, rather
   than truncated. A provider whose whole required credit is long, like
   OpenTopoMap, still shows all of it: that is its attribution, not detail.

   SUPERSEDED THE SAME DAY - the credit now folds away entirely, see
   .is-folded below. Kept because it still decides what the line looks like in
   the moment between opening and closing. */
.map-credit:not(.is-expanded) .map-credit-rest { display: none; }
.map-credit-rest:empty { display: none; }
/* The line opens and closes the credit, so it offers to be clicked in both
   states. It used to depend on .has-more, which is how the basemaps whose
   credit fitted ended up with a line that did nothing. */
.map-credit-text { cursor: pointer; }
.map-credit.is-expanded .map-credit-text { display: block; padding: 0.1875rem 0; }
.map-credit.is-expanded .map-credit-rest { white-space: normal; }
.map-credit.is-folded { padding: 0; }
.map-credit.is-folded .map-credit-text { display: none; }
.map-credit-toggle {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  min-height: 0;
  margin: 0 0 0.0625rem;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  font: italic 700 0.6875rem/1 Georgia, "Times New Roman", serif;
  cursor: pointer;
}
/* On the line it is a ringed "i", the usual sign for "more about this map";
   folded, the chip itself is the ring. */
.map-credit:not(.is-folded) .map-credit-toggle {
  box-shadow: inset 0 0 0 0.0625rem color-mix(in srgb, currentColor 45%, transparent);
}
/* The ring above outranks the app's button focus ring (a box-shadow too), so
   the "i" draws its own, as the links beside it do - without it a keyboard
   user cannot see they are on it. */
.map-credit .map-credit-toggle:focus {
  outline: var(--keyboard-ring-width) solid currentColor;
  outline-offset: 0.0625rem;
}
/* The "i" is ALWAYS there. It used to be hidden unless the line had been cut
   short, which meant the three basemaps whose credit fitted - OpenStreetMap,
   OpenTopoMap and LINZ Topographic - showed a strip of text with no way to put
   it away. Every map now rests as the "i" alone (.is-folded). */
.map-credit.is-expanded .map-credit-toggle { background: var(--map-credit-border); }
.map-credit.is-folded .map-credit-toggle {
  width: 1.375rem;
  height: 1.375rem;
  margin: 0;
  font-size: 0.8125rem;
}
/* On a phone the "i" stays small - it sits over the map - and gets the touch
   floor as an invisible hit box, the pattern the other small icon controls use
   (see "Hit box" below). Folded, it is the size it has always been. */
html.mobile .map-credit-toggle {
  position: relative;
  width: 1.25rem;
  height: 1.25rem;
  min-height: 0;
}
html.mobile .map-credit.is-folded .map-credit-toggle {
  width: 1.75rem;
  height: 1.75rem;
}
html.mobile .map-credit-toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--touch-min);
  height: var(--touch-min);
  transform: translate(-50%, -50%);
}
/* The Logs map's Upload and Flights buttons own its bottom-left corner, and
   on the Airspace map the scale does (up to 100px wide, 0.625rem in); the
   credit, open or not, stays clear of both. */
html.mobile .maplibregl-ctrl.map-credit,
html.mobile .maplibregl-ctrl.map-credit.is-expanded {
  max-width: calc(var(--map-credit-map-w, 100vw) - 5.5rem);
}
html.mobile #flightsPage .maplibregl-ctrl.map-credit,
html.mobile #mapPage .maplibregl-ctrl.map-credit { max-width: calc(var(--map-credit-map-w, 100vw) - 8.75rem); }
/* The NOTAM time bar owns the bottom middle of the Airspace map, and on a
   computer the credit keeps the bottom-right corner beside it (Josh,
   2026-11-02: "the map info button and flyout should stay in the bottom corner
   when the notam display is active"). It used to be lifted over the bar, 5rem
   up and 6.5rem with the stale line, a corner button floating over the middle
   of the map's edge. The bar is centred and two thirds of the map wide, so it
   leaves the corner a sixth of the map, which holds the folded "i" and its
   margins (about 35px) on any map a computer draws. The credit OPENED is wider
   than that corner, so while it is open the bar rises clear of it instead
   (--airspace-timebar-foot, with the bar). On a phone the bar spans the width
   higher up and the credit fits beneath it, where it already is. */
/* ux:k1 - the chart legend's bar is flush in the same corner as the credit.
   A chart is credited in the "i" (app.js setMapExtraCredit), so with a chart up
   the "i" is there, and the bar sat on top of it: present, and unreachable. The
   stack clears the collapsed bar. (An open legend covers the corner while it is
   open; clicking off closes it.) */
#mapPage:has(#airspaceLegendPanel:not([hidden])) .maplibregl-ctrl-bottom-right { margin-bottom: 2.75rem; }
/* ux:k1 - and on a phone, where the bar sits on the tab bar's edge, the credit
   is lifted over it whether or not the time bar is up. */
html.mobile #mapPage:has(#airspaceLegendPanel:not([hidden])) .maplibregl-ctrl-bottom-right {
  margin-bottom: calc(var(--mobile-tab-bar-inset) + 2.75rem);
}
/* ux:k1 - with the time bar up as well, that lift puts the credit inside the
   time bar, which is higher and wider and on top of it: the credit was on the
   page and could not be pressed. The time bar is built to clear a credit on the
   bottom row (3rem above it), so the credit goes back down there and steps left
   of the legend bar instead (8rem: the collapsed bar is about 7.4rem wide). An
   open credit keeps its left edge where it was, clear of the scale. */
html.mobile #mapPage:has(#airspaceLegendPanel:not([hidden])):has(#airspaceTimeBar:not([hidden])) .maplibregl-ctrl-bottom-right {
  margin-bottom: var(--mobile-tab-bar-inset);
  margin-right: 8rem;
}
html.mobile #mapPage:has(#airspaceLegendPanel:not([hidden])):has(#airspaceTimeBar:not([hidden])) .maplibregl-ctrl.map-credit,
html.mobile #mapPage:has(#airspaceLegendPanel:not([hidden])):has(#airspaceTimeBar:not([hidden])) .maplibregl-ctrl.map-credit.is-expanded {
  max-width: calc(var(--map-credit-map-w, 100vw) - 8.75rem - 8rem);
}

.maplibregl-popup-content {
  background: var(--popup-bg);
  color: var(--text);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  border-radius: 0.5rem;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 0.8125rem;
}

/* A spray point's box leads with the volume: it is the figure the person
   clicked for, so it is the heading, with "sprayed" beside it rather than a
   label above it saying what they already know they clicked. */
.map-popup-headline {
  display: inline;
  font-size: 1.25rem;
  line-height: 1.2;
}
.map-popup-headline-note {
  margin-left: 0.25rem;
  color: var(--muted);
}
.map-popup-headline + .map-popup-headline-note { display: inline; }

.maplibregl-popup-close-button {
  color: var(--fg-strong-2);
  font-size: 1.125rem;
  padding: 0.25rem 0.5rem;
  width: auto;
  background: transparent;
}

/* "Where am I standing?" notice, shown beneath the locate button on both maps.
   It rides in MapLibre's own top-right control stack, so on desktop it lands
   under that button with no hard-coded offset to keep in step with the column
   above it. (The mobile map section moves it out beside the column - the
   touch-floor buttons make the column too tall to sit under.) It carries the
   accuracy radius and the caveat that a position is not a clearance, so it
   stays up for as long as the dot does; pointer-events are off because it is
   read, never touched, and the map underneath must keep its drag. */
.map-locate-notice {
  width: min(17rem, calc(100vw - 3rem));
  padding: 0.5rem 0.625rem;
  border-radius: 0.5rem;
  background: var(--map-float-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  box-shadow: var(--shadow-map-notice);
  color: var(--text);
  font-size: 0.75rem;
  line-height: 1.4;
  text-align: left;
  pointer-events: none;
}
.map-locate-notice-heading {
  display: block;
  font-weight: 700;
  font-size: 0.8125rem;
}
/* Only a transient notice (an error answering a tap) has a close button, and
   it is the one part of the notice that takes pointer events. The heading
   stops short of it rather than running underneath. */
.map-locate-notice { position: relative; }
.map-locate-notice.is-transient .map-locate-notice-heading { padding-right: 1.5rem; }
.map-locate-notice-close {
  position: absolute;
  top: 0.25rem;
  right: 0.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  min-height: 1.5rem;
  padding: 0;
  border: 0;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--text);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
}
.map-locate-notice-close:hover { background: rgba(var(--rgb-slate), 0.2); }
.map-locate-notice-detail,
.map-locate-notice-caveat {
  margin: 0.25rem 0 0;
}
.map-locate-notice-caveat { color: var(--fg-label); }
/* Amber and red follow the NOTAM time bar's stale treatment rather than
   inventing a second warning palette for maps. */
.map-locate-notice.is-warn {
  border-color: rgba(var(--rgb-orange), 0.8);
  background: var(--notice-warn-bg);
}
.map-locate-notice.is-warn .map-locate-notice-heading { color: var(--fg-warn-2); }
.map-locate-notice.is-error {
  border-color: rgba(var(--rgb-red), 0.75);
  background: var(--notice-error-bg);
}
.map-locate-notice.is-error .map-locate-notice-heading { color: var(--fg-error); }

@media (max-width: 1100px) {
  /* The nav keeps its full width here (no narrower column): the wordmark plus
     the top-right collapse toggle need about 213px of it, and a 13.75rem nav
     put the toggle on top of the brand text. */
  /* Narrow the Jobs pane and the floating log list through their variables, so
     the track list itself is declared in exactly one place. */
  .layout { --filters-col: 21.25rem; --flights-col: 15rem; }
  .app-nav-link { font-size: 1rem; padding: 0 0.875rem; }
}

/* (The old pre-mobile @media (max-width: 900px) linearization fallback was
   removed - the html.mobile system owns everything at/below 1024px now.) */


.corner-widget-stack {
  position: fixed;
  right: 1.25rem;
  /* Lifted clear of the map credit and the phone's map buttons while a widget shows
     (syncCornerWidgetLift measures it; 0 when nothing is under the stack). */
  bottom: calc(1.25rem + var(--corner-widget-lift, 0px));
  /* Stays visible above open modals (parity with the pre-1000 ladder, where
     it outranked them) - it's the only way back into a running upload. */
  z-index: 1200;
  display: flex;
  flex-direction: column-reverse;
  align-items: stretch;
  gap: 0.625rem;
  max-width: 20rem;
}

.upload-status-widget {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 0.875rem;
  background: var(--float-panel-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.24);
  border-radius: 0.625rem;
  box-shadow: var(--shadow-float-panel);
  cursor: pointer;
}

.upload-status-widget[hidden] { display: none; }

.zip-download-widget {
  cursor: default;
}

/* The server builds the whole zip in one shot with no per-file progress to
   report, so this ring can't fill like the upload one does - it keeps the
   same look by showing a fixed arc and spinning it continuously instead. */
.zip-download-widget-ring {
  animation: zip-download-ring-spin 1s linear infinite;
}

.zip-download-widget-ring-fill {
  stroke-dasharray: 28 85;
}

@keyframes zip-download-ring-spin {
  from { transform: rotate(-90deg); }
  to { transform: rotate(270deg); }
}

.upload-status-widget-body {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--fg-bright);
}

/* The outbox indicator is a <button>, not a <div> like its neighbours: it is
   the only way into the queue of logs still on the device, so it has to be
   reachable by keyboard and announced as a control. These reset the button
   defaults the shared .upload-status-widget look does not cover. */
.upload-outbox-widget {
  font: inherit;
  text-align: left;
  width: 100%;
}

/* Only when everything waiting has actually failed. A queue that is merely
   waiting for signal is not a problem and must not be dressed as one. */
.upload-outbox-widget-failed {
  border-color: rgba(var(--rgb-red), 0.55);
}

.upload-outbox-widget-icon {
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 62.4375rem;
  background: rgba(var(--rgb-blue), 0.20);
  color: var(--pill-blue-fg);
  font-weight: 750;
}

.upload-outbox-widget-failed .upload-outbox-widget-icon {
  background: rgba(var(--rgb-red), 0.24);
  color: var(--pill-red-fg);
}

.upload-outbox-connection {
  color: var(--pill-sand-fg);
}

.outbox-row-actions {
  display: flex;
  gap: 0.375rem;
  margin-top: 0.375rem;
  justify-content: flex-end;
}

.upload-status-widget-ring {
  flex-shrink: 0;
  transform: rotate(-90deg);
}

.upload-status-widget-ring-track {
  stroke: rgba(var(--rgb-slate), 0.24);
  stroke-width: 4;
}

.upload-status-widget-ring-fill {
  stroke: var(--green);
  stroke-width: 4;
  stroke-linecap: round;
  transition: stroke-dashoffset 200ms ease;
}

#uploadCompleteSummary {
  list-style: none;
  padding: 0;
  margin: -0.125rem 0 1rem;
}

#uploadCompleteSummary li {
  padding: 0.375rem 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  color: var(--fg-copy);
  font-size: 0.9375rem;
}

#uploadCompleteSummary li:last-child {
  border-bottom: none;
}

/* Login two-factor step */
.login-mfa {
  margin: 0.25rem 0 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}
/* display:flex above would otherwise beat the native [hidden] attribute and
   leave the MFA step visible on the first (credentials) screen. */
.login-mfa[hidden] { display: none; }
#loginMfaBackButton { margin-top: 0.5rem; }
.login-mfa-qr {
  display: block;
  width: 11.25rem;
  height: 11.25rem;
  background: var(--paper);
  border-radius: 0.5rem;
  padding: 0.375rem;
  margin: 0.25rem auto;
}
/* No word-break here: the key breaks between its groups of four (the
   .login-mfa-secret rule in the ux:U2 block, word-break: normal with
   overflow-wrap: anywhere), and a break-all here came later in the file and won,
   splitting a group across two lines ("STUV WXY" / "Z 2345"). An unspaced key
   (Settings' authenticator setup) still breaks where it must: overflow-wrap:
   anywhere. */
.login-mfa-secret {
  font-size: 0.75rem;
  color: var(--muted);
  text-align: center;
}
.login-mfa-secret code {
  color: var(--text);
}
/* "Keep me signed in on this device", under the password on the first step:
   one line, a little apart from the fields so it reads as its own choice. */
.login-keep-signed-in {
  display: flex;
  align-items: center;
  margin-top: 0.625rem;
}
/* Same trap as .login-mfa above: display:flex would beat the [hidden]
   attribute. */
.login-keep-signed-in[hidden] { display: none; }

/* Recovery code entry at the sign-in prompt. Same [hidden] trap as
   .login-mfa and .login-keep-signed-in above: a display rule on the class beats
   the attribute, which would put the field on screen for everyone. */
/* The region the two second factors swap inside. Both are prompt + label +
   one control, but the recovery copy wraps to two lines where the code prompt
   fits on one, so the card grew as you switched. A floor on the region absorbs
   that: whichever is showing, the card is the height of the taller. Measured,
   not guessed - see the comment below for the numbers. */
.login-2fa-region {
  min-height: var(--login-2fa-min-height, 10.5rem);
  /* The size both second factors are derived from. Making the REGION the
     container rather than .otp-field means the recovery input can size itself
     off the same width the boxes do, even though it is not inside them — which
     is what keeps the two the same height without either one hard-coding a
     number the other has to be kept in step with by hand. */
  container-type: inline-size;
}

/* The width of one box, and the height that follows from it. Written once,
   here, because three rules need it and a fourth will eventually.
   --otp-gap is also the grid gap below; if you change one, change both. */
.login-2fa-region {
  --otp-gap: 0.375rem;
}

/* DECLARED ON THE CHILDREN, NOT ON THE CONTAINER. cqw resolves against the
   nearest ANCESTOR container, and an element is never its own container, so
   these sat on .login-2fa-region resolving against nothing: the whole
   font-size declaration below was thrown away as invalid and the digits fell
   back to an inherited 10.2px inside a 69px box, while the recovery input took
   its 2.75rem fallback height. Both of these elements are inside the region,
   so here the unit means what it says. */
.otp-field,
.login-recovery {
  --otp-box-width: calc((100cqw - (5 * var(--otp-gap, 0.375rem))) / 6);
  --otp-box-height: calc(var(--otp-box-width) / 0.75);
}

/* --- The two second factors, built to be the same component ----------------
   The code step and the recovery step swap in place on one card. Measured
   before this existed: the card jumped 318px -> 341px between them, because
   the recovery block carried two lines of explanation and the code step
   carried none. Nothing about a card resizing under you as you switch reads as
   deliberate. So both now carry exactly ONE line of helper text and share a
   control height, and the card holds still.

   Six boxes for the code, one real input underneath. .otp-field is the frame;
   the boxes are painted spans and the input sits on top of them, transparent,
   its own text hidden. It keeps the caret, the selection, autocomplete and
   paste - all the things six separate inputs give up. */
.otp-field {
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--otp-gap, 0.375rem);
  /* Room above. The boxes sat hard against the prompt, which read as one
     block rather than a label and the thing it labels. */
  margin-top: 0.875rem;
}
/* THESE BOXES WERE WHITE, AND THE CHARACTERS ON THEM WERE VERY NEARLY WHITE
   TOO. They were written against --input-bg and --input-border, and NEITHER
   TOKEN EXISTS in this stylesheet - so the background fell through to the
   hardcoded #fff and the text to var(--text), which is #f8fafc. Six white
   boxes on a dark card, with the code all but invisible on them, and nothing
   in the rule looked wrong at a glance because the var() names read as though
   they meant something.

   So the surface is now the one every other field on this card already uses
   (.form-field input, §form controls) rather than a fourth opinion about what
   an input looks like: same background, same border, same radius. The
   characters are larger and semibold on top of that, because a one-time code
   is read aloud off a phone and typed under time pressure, and it is the one
   string in the product where a misread character costs you the whole
   attempt. */
/* `.otp-field .otp-box`, not `.otp-box`. These spans sit inside a .form-field,
   and `.form-field span { font-size: 0.75rem }` is specificity 0-1-1 against a
   bare class's 0-1-0, so it won and every digit rendered at 10.2px inside a
   55px box. The clamp here was never the problem; it never applied. */
.otp-field .otp-box {
  /* PORTRAIT, IN THE PROPORTIONS OF A CHARACTER. These were 2.75rem tall and
     wider than they were high, so a single digit sat in a landscape box — the
     shape said "short text field", not "one character". The ratio is the
     monospace advance width, 0.6, so the box is the shape of its contents.
     aspect-ratio rather than a height, because the width is set by the grid
     and the height should follow it at any card size. */
  /* 3:4, not the 3:5 this was. 3:5 is the monospace CELL, and a digit only
     inks about the top seven tenths of it, so the box read as a slot with a
     small character adrift in it. */
  aspect-ratio: 3 / 4;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* SIZED FROM THE BOX, NOT FROM A GUESS. A monospace glyph is 0.6em wide, so
     at 1.15x the box width the character occupies about seventy per cent of
     it — filling the box without touching its sides at any card width. The
     clamp stops it running away on a very wide viewport or collapsing on a
     very narrow one. */
  font-size: clamp(1.25rem, calc(var(--otp-box-width) * 1.0), 3.25rem);
  font-weight: 600;
  line-height: 1;
  color: var(--fg-body);
}
/* Which box the next character lands in. Only while the field has focus, so a
   blurred field does not look like it is waiting for something. */
/* --accent does not exist either, so this was falling back to a blue that
   appears nowhere else in the product. The accent here is --green. */
.otp-field.is-focused .otp-box.is-next {
  border-color: var(--green);
  box-shadow: 0 0 0 0.125rem color-mix(in srgb, var(--green) 25%, transparent);
}
/* The real control: full width across all six boxes, invisible but entirely
   functional. Transparent text rather than opacity:0 so the caret and the
   native selection still behave, and a tap anywhere on the row focuses it. */
.otp-field input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: transparent;
  caret-color: transparent;
  /* In step with .otp-box above: this invisible control still owns the caret
     and the native selection, and both are sized from ITS font. */
  font-size: clamp(1.25rem, calc(var(--otp-box-width) * 1.0), 3.25rem);
  font-weight: 600;
  letter-spacing: 0;
  text-align: center;
}
.otp-field input:focus { outline: none; box-shadow: none; }
/* The recovery field is the same control wearing the same clothes - same
   height, same monospace - so switching between the two changes what is being
   asked for and nothing else. It cannot be boxed the way the code is: a
   recovery code is five groups of four. */
.login-recovery input {
  /* The same height the boxes now work out to, from the same derivation, so
     switching between the two still changes what is being asked for and
     nothing else. Its FONT does not follow them up: a recovery code is five
     groups of four on one line of a 22.5rem card, where the code has six
     boxes to itself. */
  height: var(--otp-box-height, 2.75rem);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Same size and weight as the boxes, so switching between the two changes
     what is being asked for and nothing else. It stops HERE rather than going
     larger with them: a recovery code is five groups of four and has to fit
     one line of a 22.5rem card, where the code has six boxes to itself. */
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-align: center;
}
.login-recovery {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.login-recovery[hidden] { display: none; }
.login-recovery .card-copy {
  font-size: 0.75rem;
  margin: 0;
}
/* The switch between the two second factors. Centred and quiet: it is the way
   out for someone who has lost their authenticator, not a second call to
   action competing with the code they are being asked for. */
.login-recovery-switch {
  display: flex;
  justify-content: center;
  padding-top: 0.25rem;
}
.login-recovery-switch[hidden] { display: none; }
/* Recovery codes, shown once. Monospace and generously spaced because these
   get copied onto paper by hand, and a mistaken character costs a code. */
/* Sized for the code that actually goes in it: five groups of four plus their
   hyphens, 24 characters. At the previous 28rem/two-fixed-columns a code was
   wider than its cell and the last group scrolled out of sight - which on the
   one screen these are ever shown on is the difference between a usable card
   and a useless one. auto-fit means the column count follows the space
   available instead of being asserted, so a cell can never be narrower than a
   whole code. */
.recovery-codes-card { max-width: 32rem; }
.recovery-codes-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 0.5rem;
  margin: 0.75rem 0;
}
.recovery-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  text-align: center;
  padding: 0.4375rem 0.25rem;
  border: 1px solid rgba(var(--rgb-slate), 0.25);
  border-radius: 0.375rem;
  background: rgba(var(--rgb-slate), 0.08);
  color: var(--text);
  /* A code is one unbreakable token; without this a narrow phone tears one
     across two lines and it stops being transcribable. */
  white-space: nowrap;
  overflow-x: auto;
}
html.mobile .recovery-codes-grid { grid-template-columns: minmax(0, 1fr); }

/* Trusted devices list (Settings > Profile). A grid item defaults to
   min-width:auto, so the 1fr column sizes to the widest unbreakable content -
   and a user agent is one long unbroken token. Measured before this rule: the
   544px settings panel scrolled to 796px and the Remove button sat off-screen.
   min-width:0 lets .queue-name's existing ellipsis actually apply. */
.trusted-device-row > :first-child { min-width: 0; }
.trusted-device-row .queue-meta { overflow-wrap: anywhere; }

/* Platform audit log */
.audit-log-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.audit-log-wrap {
  max-height: 60vh;
  overflow-y: auto;
  overflow-x: auto;
}
/* The audit log is a READING surface - it is the one platform screen someone
   sits and scrolls through - so it takes the height instead of stopping at 60vh
   with a band of dead space under it (measured: 600px of table with 130.7px
   wasted below, over 3544px of content).
   Scoped with :has() to the audit subpanel only. The panel's align-content:start
   is right about every OTHER subpanel: those are short cards, and stretching
   them leaves a dead band inside a bordered card (see the note on
   .settings-layout). minmax(0, 1fr) rather than 1fr because a bare 1fr track
   takes its min-content floor from the table and refuses to shrink, which turns
   the inner scroller into a page-length overflow instead. */
.settings-layout[data-settings-panel="platform"]:has(> [data-platform-subpanel="audit"]:not([hidden])) {
  align-content: stretch;
  grid-template-rows: auto minmax(0, 1fr);
}
.settings-layout[data-settings-panel="platform"] > [data-platform-subpanel="audit"]:not([hidden]) {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
[data-platform-subpanel="audit"] .audit-log-wrap {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
}
/* Content-fit columns: every column hugs its content (nowrap) and the Details
   column (last) absorbs the remaining width and wraps. table-layout:auto lets
   the browser size columns to content; width:100% on the last cell makes it
   take the slack. Once a column is drag-resized, JS switches the table to
   table-layout:fixed with explicit widths (see initResizableColumns). */
.audit-log-table {
  table-layout: auto;
  width: 100%;
}
.audit-log-table th,
.audit-log-table td {
  font-size: 0.75rem;
  white-space: nowrap;
}
.audit-log-table th {
  position: relative;
}
.audit-log-table th:first-child,
.audit-log-table td:first-child {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* Details column fills the rest and is the only one that wraps. */
.audit-log-table th:last-child,
.audit-log-table td:last-child {
  white-space: normal;
  word-break: break-word;
  width: 100%;
}
.audit-log-detail {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  color: var(--muted);
}
/* Drag handle on the right edge of each header cell. */
.audit-log-table th .col-resizer {
  position: absolute;
  top: 0;
  right: 0;
  width: 0.4375rem;
  height: 100%;
  cursor: col-resize;
  user-select: none;
  touch-action: none;
}
.audit-log-table th .col-resizer:hover,
.audit-log-table th.col-resizing .col-resizer {
  background: rgba(var(--rgb-blue), 0.45);
}
.audit-log-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  margin: 0.75rem 0 1rem;
}
.audit-filter-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: var(--muted);
}
/* The selects take the shared dropdown metrics; the search box matches them,
   and both need the same floor width or the row jumps as the filters change.
   (A searchable filter's trigger replaces its select in the layout, so the
   wrapper carries the width too.) */
.audit-filter-field select,
.audit-filter-field .searchable-select,
.audit-filter-field input {
  min-width: 12rem;
  width: 100%;
}
.audit-filter-field input {
  box-sizing: border-box;
  margin: 0;
  border: 0.0625rem solid var(--control-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  color: var(--fg-body);
  padding: var(--dropdown-pad-y) var(--dropdown-pad-x);
  font: inherit;
  font-size: 0.875rem;
  line-height: var(--dropdown-line);
  outline: none;
}
/* Normalise the search input's native appearance so it matches the selects. */
.audit-filter-field input[type="search"] {
  -webkit-appearance: none;
  appearance: none;
}
.audit-log-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.75rem;
  flex-wrap: wrap;
}
.audit-log-count {
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.audit-log-pager {
  display: flex;
  gap: 0.5rem;
}

/* Platform settings sub-tabs: a horizontal tab strip inside the platform panel,
   distinct from the vertical main settings rail. */
.platform-subtabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  padding-bottom: 0.5rem;
  margin-bottom: 0.25rem;
}
.platform-subtab {
  background: transparent;
  color: var(--muted);
  border-radius: 0.5rem;
  padding: 0.4375rem 0.875rem;
  font-size: 0.875rem;
  font-weight: 650;
}
.platform-subtab:hover:not(:disabled) {
  background: rgba(var(--rgb-slate), 0.12);
  filter: none;
}
.platform-subtab.active {
  background: var(--subtab-active-bg);
  color: var(--button-fg);
}

/* Email settings form */
.email-settings-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 0.25rem;
}
.email-settings-form .form-grid {
  margin: 0;
}
.email-settings-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.25rem;
}
.email-test-row {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
}
.email-test-row #emailTestSendButton {
  margin-top: 0.75rem;
}

/* Encrypted off-host backups (Platform > Backups). */
.backup-settings-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 0.25rem;
}
.backup-settings-form .form-grid {
  margin: 0;
}
.backup-warning {
  color: var(--fg-warn-deep);
}
.backup-fieldset {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0;
  padding: 0.75rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  border-radius: 0.375rem;
}
.backup-fieldset legend {
  padding: 0 0.375rem;
  font-weight: 600;
}
.backup-fieldset #backupS3Fields,
.backup-fieldset #backupSshFields {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.backup-settings-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.25rem;
}
.backup-fieldset #backupGeneratePassphraseButton {
  align-self: flex-start;
}
.backup-passphrase-reveal {
  align-self: stretch;
  padding: 0.75rem;
  border: 0.0625rem solid var(--control-border-3);
  border-radius: 0.375rem;
}
.backup-passphrase-value {
  display: block;
  margin-top: 0.375rem;
  padding: 0.5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9rem;
  word-break: break-all;
  background: rgba(var(--rgb-slate), 0.14);
  border-radius: 0.25rem;
  user-select: all;
}
.backup-last-run {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
}
#backupLastRunStatus.success {
  color: var(--fg-success-deep);
}
#backupLastRunStatus.error {
  color: var(--fg-error-deep);
}

/* Card header row with a "?" help affordance in the corner. */
.card-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.help-button {
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  min-height: 0;
  padding: 0;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.16);
  color: var(--muted);
  border: 0.0625rem solid var(--control-border-3);
  font-size: 0.9375rem;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
}
.help-button:hover:not(:disabled) {
  background: rgba(var(--rgb-blue), 0.2);
  color: var(--fg-info-hover);
  filter: none;
}

/* Email setup help modal */
.email-help-card {
  max-width: 44rem;
  width: 100%;
}
.email-help-body {
  overflow-y: auto;
  max-height: 70vh;
}
.email-help-list {
  margin: 0.25rem 0 0.75rem;
  padding-left: 1.125rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.email-help-list li {
  font-size: 0.875rem;
  line-height: 1.5;
}
.email-help-body code {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.8125rem;
  background: rgba(var(--rgb-slate), 0.14);
  padding: 0.0625rem 0.3125rem;
  border-radius: 0.25rem;
}
.email-help-table th,
.email-help-table td {
  font-size: 0.8125rem;
  white-space: normal;
}
.email-help-muted {
  color: var(--muted);
  font-size: 0.8125rem;
}

/* Reusable in-app confirmation dialog (replaces the native window.confirm).
   Sits above other modals so it can confirm leaving one of them. */
.confirm-dialog-overlay {
  z-index: 1100;
}
.confirm-dialog-card {
  max-width: 26rem;
  width: 100%;
}
.confirm-dialog-card #confirmDialogMessage {
  margin-top: 0.5rem;
}
.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

/* Password requirements list + forced-rotation notice on the change form. */
.password-requirements-block {
  margin: 0.75rem 0;
}
.password-requirements-title {
  font-size: 0.8125rem;
  color: var(--muted);
}
.password-requirements {
  margin: 0.375rem 0 0;
  padding-left: 1.125rem;
  display: flex;
  flex-direction: column;
  gap: 0.1875rem;
}
.password-requirements li {
  font-size: 0.8125rem;
  color: var(--muted);
}
.password-rotation-notice {
  border: 0.0625rem solid rgba(var(--rgb-yellow), 0.4);
  background: rgba(var(--rgb-yellow), 0.1);
  color: var(--fg-warn-2);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  margin-bottom: 0.5rem;
}

/* Feedback inbox header: the kind filter, the show-archived toggle and the
   refresh button, wrapping under the title when the card is narrow. */
.platform-fr-header-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
}
.platform-fr-header-actions .filter-checkbox-label {
  margin: 0;
  white-space: nowrap;
}
/* Compact inline action buttons in the feature-requests table */
.fr-row-actions {
  display: flex;
  gap: 0.375rem;
  flex-wrap: wrap;
}
.fr-row-actions button {
  padding: 0.3125rem 0.5625rem;
  font-size: 0.8125rem;
}

/* Feature-request lifecycle status: a coloured pill, plus the admin's inline
   status <select> and the submitter's Rescind button. */
.fr-status-badge {
  display: inline-block;
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  vertical-align: middle;
  border: 0.0625rem solid transparent;
}
.fr-status-submitted { background: rgba(var(--rgb-slate), 0.18); color: var(--fg-label); border-color: rgba(var(--rgb-slate), 0.4); }
.fr-status-in_progress { background: rgba(var(--rgb-blue), 0.18); color: var(--pill-blue-fg-2); border-color: rgba(var(--rgb-blue), 0.45); }
.fr-status-done { background: rgba(var(--rgb-green), 0.18); color: var(--pill-green-fg-2); border-color: rgba(var(--rgb-green), 0.45); }
.fr-status-rejected { background: rgba(var(--rgb-red-soft), 0.16); color: var(--fg-error); border-color: rgba(var(--rgb-red-soft), 0.4); }
.fr-status-rescinded { background: rgba(var(--rgb-slate), 0.12); color: var(--fg-subtle); border-color: rgba(var(--rgb-slate), 0.3); }

/* Sized to its cell; everything else is the shared dropdown style. The cell is
   named because the picker's own <select> is hidden behind a trigger, so the
   width has to go on what actually stands in the row. */
.fr-status-select,
.fr-status-cell .searchable-select {
  width: auto;
  min-width: 8.5rem;
}
.fr-rescind-button {
  margin-top: 0.5rem;
  padding: 0.25rem 0.625rem;
  font-size: 0.8125rem;
}
/* Mute archived rows' content but keep the action buttons legible (opacity on
   the row's td would also fade its child buttons, since children can't exceed a
   parent's alpha) — so fade every cell except the last (actions). */
.fr-archived-row td:not(:last-child) {
  opacity: 0.55;
}

.spray-diary-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Override the global `button { width: 100% }` so these sit inline in the flex
   row instead of each taking a full line. */
.spray-diary-actions button { width: auto; }

/* The amendment block reads as a caution, not as another form row: the user is
   editing a record that is meant to be fixed. */
.spray-diary-lock {
  background: rgba(var(--rgb-amber), 0.1);
  border: 0.0625rem solid rgba(var(--rgb-amber), 0.4);
  border-radius: 0.5rem;
  padding: 0.75rem;
  margin-bottom: 0.75rem;
}

.spray-diary-lock .card-copy { margin-top: 0; }

/* ---- What a job has to answer for (jobIssues) ------------------------- */
/* In the jobs list: one mark, whatever and however many. Red when any of it
   is serious, amber when it is only "not ready yet". */
.job-flag-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 50%;
  font-size: 0.75rem;
  font-weight: 800;
  line-height: 1;
  cursor: default;
}
.job-flag-serious { background: var(--flag-mark-red-bg); color: var(--pill-red-fg); }
.job-flag-minor { background: rgba(var(--rgb-amber), 0.24); color: var(--pill-amber-fg); }

/* On the job card: the pills under the title. Buttons, because each one takes
   you to the section that explains it - so the button defaults are undone
   back to a pill.
   The row is ALWAYS on the card, empty when the job has nothing to flag, and
   always one pill tall: the tabs below it sit at the same height on every job
   (a job with a pill used to push them 13px lower than a job without). Nothing
   hides it, so there is no [hidden] state to keep. A second line of pills makes
   the header one line taller, which is the only time the tabs move.
   One pill's size is written once, here, as three tokens: the pills below are
   drawn at it, and the row's minimum height is the pill's own height from the
   same three numbers. The line height is a number, not `normal`, which would
   make the pill's height (and so the row's) depend on the font that happens to
   draw it. */
.job-preview-flags {
  --job-pill-font: 0.6875rem;
  --job-pill-line: 1.2;
  --job-pill-pad-y: 0.1875rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.375rem;
  min-height: calc(var(--job-pill-font) * var(--job-pill-line) + 2 * var(--job-pill-pad-y));
}
button.job-flag-pill {
  min-height: 0;
  padding: var(--job-pill-pad-y) 0.4375rem;
  border: 0;
  border-radius: 62.4375rem;
  font-size: var(--job-pill-font);
  line-height: var(--job-pill-line);
  cursor: pointer;
}
/* One pill shape, four tones - told apart by fill, tint and outline as well
   as colour. Red is filled: a rule broken, or it should not fly as planned.
   Amber is tinted: look at this, or we could not check. Information is
   outlined: NOTAMs, to read, not a fault. Pending is muted: a check running. */
.job-flag-pill.job-flag-red { background: rgba(var(--rgb-red), 0.3); color: var(--pill-red-fg); }
.job-flag-pill.job-flag-amber {
  background: rgba(var(--rgb-amber), 0.14);
  color: var(--pill-amber-fg);
  box-shadow: inset 0 0 0 0.0625rem rgba(var(--rgb-amber), 0.45);
}
.job-flag-pill.job-flag-info {
  background: transparent;
  color: var(--fg-soft);
  box-shadow: inset 0 0 0 0.0625rem rgba(var(--rgb-slate), 0.55);
}
.job-flag-pill.job-flag-pending {
  background: rgba(var(--rgb-slate), 0.12);
  color: var(--muted);
  font-weight: 600;
  padding: var(--job-pill-pad-y) 0.4375rem;
  font-size: var(--job-pill-font);
  line-height: var(--job-pill-line);
}

/* And in the section each issue concerns, what it actually is. */
.job-section-flags {
  background: rgba(var(--rgb-red), 0.10);
  border: 0.0625rem solid rgba(var(--rgb-red), 0.45);
  color: var(--text);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  margin: 0 0 0.75rem;
  font-size: 0.8125rem;
}
.job-section-flags-minor {
  background: rgba(var(--rgb-amber), 0.12);
  border-color: rgba(var(--rgb-amber), 0.45);
}
.job-section-flags strong {
  display: block;
  color: var(--pill-red-fg);
  font-size: 0.75rem;
}
.job-section-flags-minor strong { color: var(--pill-amber-fg); }
.job-section-flags ul {
  margin: 0.25rem 0 0;
  padding-left: 1.125rem;
}
.job-section-flags li + li { margin-top: 0.25rem; }
/* Each item keeps its own tone in a mixed note: the bullet says it. */
.job-section-flags li.job-issue-red::marker { color: var(--fg-error-2); }
.job-section-flags li.job-issue-amber::marker { color: var(--fg-warn-3); }
/* "Blocks go-live", "Rule not met", "Warning", "Not checked". */
.job-issue-tag {
  display: inline-block;
  padding: 0 0.3125rem;
  border-radius: 0.25rem;
  font-size: 0.6875rem;
  font-weight: 700;
  vertical-align: 0.0625rem;
}
.job-issue-red .job-issue-tag { background: var(--issue-tag-red-bg); color: var(--pill-red-fg); }
.job-issue-amber .job-issue-tag { background: rgba(var(--rgb-amber), 0.2); color: var(--pill-amber-fg); }

/* A section with something in it to read is marked on its tab as well.
   ::after, because ::before is the active-tab underline. */
.job-preview-tab.has-flag::after,
.job-preview-tab.has-flag-minor::after {
  content: "";
  display: inline-block;
  width: 0.375rem;
  height: 0.375rem;
  margin-left: 0.3125rem;
  border-radius: 50%;
  background: var(--danger);
  vertical-align: 0.125rem;
}
.job-preview-tab.has-flag-minor::after { background: rgb(var(--rgb-amber)); }

/* Location: what the plan's approvals cover, and the evidence the check ran. */
.job-covered {
  margin: 0 0 0.75rem;
  font-size: 0.8125rem;
}
.job-covered strong {
  display: block;
  font-size: 0.75rem;
  color: var(--pill-green-fg);
}
.job-covered ul {
  margin: 0.25rem 0 0.375rem;
  padding-left: 1.125rem;
  color: var(--fg-soft);
}
.job-covered li::marker { color: var(--fg-go); }
.job-covered-evidence {
  margin: 0;
  color: var(--muted);
}

/* Overview: the plan (pilot in command, crew, booked aircraft) beside the
   record (who and what the job's logs say flew). The two disagreeing is
   information, never a fault - plain rows, no amber. */
.job-flown { margin: 0.25rem 0 0.875rem; font-size: 0.8125rem; }
.job-flown-title {
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-nav);
}
.job-flown-group { margin: 0; }
/* A heading within Pilots or Aircraft ("Not planned") sits close under the
   rows above it; Aircraft itself starts further down, and so does anything
   after a note ("1 log names no pilot"), which otherwise ran into it. */
.job-flown-group + .job-flown-group,
.job-flown-note + .job-flown-group { margin-top: 0.5rem; }
.job-flown-group.is-section + .job-flown-group.is-section,
.job-flown-group + .job-flown-group.is-section,
.job-flown-note + .job-flown-group.is-section { margin-top: 0.875rem; }
/* Two classes deep on purpose: the block sits in a .card, and `.card strong`
   (a heading size, 1.25rem, with its own margin) outranks one class - the
   group titles came out larger than the section's own title. */
.job-flown .job-flown-group-title {
  display: block;
  margin: 0 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-soft);
}
.job-flown-list {
  display: grid;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.job-flown-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.625rem;
  min-width: 0;
}
.job-flown-name { color: var(--fg-body-2); font-weight: 600; }
.job-flown-meta { color: var(--muted); }
.job-flown-tag { font-size: 0.75rem; color: var(--fg-soft); }
.job-flown-row .secondary-button {
  margin-left: auto;
  min-height: 0;
  padding: 0.125rem 0.5rem;
  font-size: 0.75rem;
}
.job-flown-note { margin: 0.25rem 0 0; color: var(--muted); }

/* Location: the NOTAMs affecting the job. Information, not faults - plain
   rows, no amber. */
.job-notams { margin: 0 0 0.875rem; }
.job-notams-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  margin-bottom: 0.375rem;
}
.job-notams-title {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-nav);
}
.job-notams-title.job-notams-clear {
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
  font-size: 0.8125rem;
}
.job-notams-freshness { font-size: 0.75rem; color: var(--muted); }
.job-notams-list {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.job-notams-list[hidden] { display: none; }
.job-notam-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.job-notam-swatch {
  flex: 0 0 auto;
  width: 0.625rem;
  height: 0.625rem;
  margin-top: 0.1875rem;
  border-radius: 0.125rem;
}
.job-notam-body { display: grid; gap: 0.125rem; min-width: 0; }
.job-notam-title { font-size: 0.8125rem; font-weight: 600; color: var(--fg-body-2); }
.job-notam-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--fg-soft);
  overflow-wrap: anywhere;
}
.job-notam-text.expanded {
  display: block;
  -webkit-line-clamp: unset;
}
.job-notam-toggle,
.job-notams-more {
  justify-self: start;
  min-height: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--green);
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
}
.job-notams-more { margin-top: 0.5rem; }
.job-notams-more[hidden] { display: none; }
/* Thumb-sized on a phone, without making the rows any taller to look at. */
html.mobile .job-notam-toggle,
html.mobile .job-notams-more {
  min-height: var(--touch-min, 2.75rem);
  display: inline-flex;
  align-items: center;
}

/* Location: the airspace zones the plan touches. A row says what the zone is,
   whether it stops the job, and what clears it - and, while it is not cleared,
   carries the button that records the zone holder's permission. Tone follows
   the Needs attention note above it: red blocks, amber warns, green is cleared. */
.job-zones { margin: 0 0 0.875rem; }
.job-zones-title {
  margin: 0 0 0.375rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-nav);
}
/* Why a draft is still a draft after a permission was saved (job_note). */
.job-zones .form-message { margin: 0.5rem 0 0; }
.job-zones-list {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.job-zone-row {
  display: grid;
  gap: 0.25rem;
  min-width: 0;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0.5rem;
  background: var(--tile-bg);
  font-size: 0.8125rem;
}
.job-zone-row.is-blocked { border-color: rgba(var(--rgb-red), 0.45); }
.job-zone-row.is-warned { border-color: rgba(var(--rgb-amber), 0.45); }
.job-zone-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  min-width: 0;
}
.job-zone-name {
  min-width: 0;
  font-weight: 600;
  color: var(--fg-body-2);
  overflow-wrap: anywhere;
}
.job-zone-tag {
  display: inline-block;
  padding: 0 0.3125rem;
  border-radius: 0.25rem;
  font-size: 0.6875rem;
  font-weight: 700;
  white-space: nowrap;
}
.job-zone-tag.is-red { background: var(--issue-tag-red-bg); color: var(--pill-red-fg); }
.job-zone-tag.is-amber { background: rgba(var(--rgb-amber), 0.2); color: var(--pill-amber-fg); }
.job-zone-tag.is-green { background: rgba(var(--rgb-green-2), 0.24); color: var(--pill-green-fg); }
.job-zone-meta,
.job-zone-detail {
  font-size: 0.75rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.job-zone-note { color: var(--fg-soft); }
.job-zone-clearances {
  display: grid;
  gap: 0.25rem;
  margin: 0.125rem 0 0;
  padding: 0;
  list-style: none;
}
.job-zone-clearance {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.125rem 0.625rem;
  min-width: 0;
  font-size: 0.75rem;
  color: var(--fg-soft);
}
.job-zone-clearance.is-not-covering { color: var(--muted); }
.job-zone-clearance-text { min-width: 0; overflow-wrap: anywhere; }
/* Text buttons: Edit and Document beside a clearance, and the file line in the
   permission forms. */
.job-zone-link {
  min-height: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--green);
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
}
.job-zone-actions { margin-top: 0.125rem; }
.job-zone-actions .secondary-button {
  min-height: 0;
  padding: 0.25rem 0.625rem;
  font-size: 0.75rem;
}
/* Thumb-sized on a phone, without making the rows any taller to look at. */
html.mobile .job-zone-link,
html.mobile .job-zone-actions .secondary-button {
  min-height: var(--touch-min, 2.75rem);
  display: inline-flex;
  align-items: center;
}

/* The permission and arrangement forms. */
.zone-modal-zone {
  margin: 0 0 0.75rem;
  font-weight: 600;
  color: var(--fg-body-2);
}
.zone-modal-needs { margin: 0; }
.zone-modal-file { font-size: 0.8125rem; color: var(--muted); }
.zone-modal-file .job-zone-link { font-size: inherit; }
.zone-modal-summary p { margin: 0 0 0.375rem; }
/* Settings > Compliance: an arrangement's status pill, and beside it the
   amber one when its zone has left the airspace data. */
.arrangement-pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.375rem;
}

/* Claiming unassigned logs for a new job: a list of checkable logs.
   The card is a column and the LIST takes whatever height is left, so a long
   backlog uses the screen: the card's own cap (82vh on a desktop, the whole
   screen on a phone, where modal cards are full-screen sheets) decides the
   height, and the heading, Select all and the buttons stay in view while only
   the list scrolls. It was a fixed 22rem, which on a phone left half the
   screen empty under the buttons and showed seven logs at a time out of 300.
   The card keeps .modal-card's overflow: if even the list's floor does not
   fit a very short window, the card scrolls rather than clipping a button. */
.claim-logs-card {
  width: min(40rem, 100%);
  display: flex;
  flex-direction: column;
}
.claim-logs-card > * {
  flex: none;
}
.claim-logs-all {
  margin: 0.75rem 0 0.375rem;
}
/* Recording the job's pilot on the claimed logs that name nobody. */
.claim-logs-pilot { margin: 0.625rem 0 0; }

.claim-logs-card > .claim-logs-list {
  flex: 1 1 auto;
  min-height: 8rem;
}
.claim-logs-list {
  overflow-y: auto;
  /* Reaching either end of the list does not start scrolling the page
     behind the dialog. */
  overscroll-behavior: contain;
  border: 0.0625rem solid var(--frame-border);
  border-radius: 0.5rem;
  background: var(--control-bg);
  padding: 0.25rem;
}
.claim-log-row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.4375rem 0.5rem;
  border-radius: 0.375rem;
  cursor: pointer;
}
.claim-log-row:hover { background: rgba(var(--rgb-overlay), 0.04); }
.claim-log-row input {
  margin: 0;
  flex: none;
  accent-color: var(--green);
}
.claim-log-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.claim-log-name {
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.claim-log-meta {
  font-size: 0.75rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Edit job, Location step, a job with logs and no location: the fact, and the
   way to fix it, on one line that wraps on a phone. */
.job-location-from-logs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin: 0.25rem 0 0.5rem;
}
.job-location-from-logs .card-copy {
  margin: 0;
  flex: 1 1 12rem;
}

/* Under the job card's location map: what the white lines are. */
.job-tracks-note {
  margin: 0.375rem 0 0;
  font-size: 0.8125rem;
}


/* ==========================================================================
   Mobile shell (stage 1)
   Every rule below is keyed off `html.mobile` - a class toggled from JS by a
   media query (max-width: 1024px, or coarse-pointer up to 1180px) - rather than
   an @media block. This is deliberate: the mobile layout can be forced on a
   desktop for testing with `document.documentElement.classList.add("mobile")`.
   This stage provides only the shell (single-column grid, bottom tab bar + More
   sheet, full-screen modals, touch-friendly foundations); stages 2-3 restyle
   the individual pages.
   ========================================================================== */

/* Shared mobile layout constants. Everything that has to line up with the
   fixed bottom tab bar reads these, so the bar's height is stated once.

   --touch-min is the product's touch-target floor: 3.25rem is 45.5px at the
   phone's 87.5% root (html.mobile, near the top of this file), past the 44px a
   fingertip needs and every platform guideline names. It is applied in the
   Touch targets section below, which also explains what is exempt from it and
   why.

   --mobile-tab-bar-inset is the space reserved along the bottom of the screen,
   exactly the bar's height with no slack: its 0.0625rem hairline top border,
   its 0.25rem top padding, one tab (--touch-min), and below the tabs
   max(0.25rem, --safe-bottom) - the whole home-indicator strip where there is
   one, kept clear as Apple's own apps keep it, and a 0.25rem floor where there
   is none. At the phone's 14px root that is 0.875 + 3.5 + 45.5 + 34 = 83.875px
   in the installed iPhone app (a 34pt home indicator; the old bar, with 0.5rem
   of padding on both sides of the tabs, was 94.375 under a 95.25 reserve) and
   0.875 + 3.5 + 45.5 + 3.5 = 53.375px in a browser tab. The tabs add only
   --touch-min because their min-height is their height: what is inside one
   (its padding, the icon, the gap and the label's line) must stay within it. It is written in terms of --touch-min on purpose -
   raising the floor raises the bar, and the reserve has to follow it in the
   same edit or the map pages strand a gap.
   --mobile-tab-bar-outset MUST stay its exact negative: the two full-screen map
   pages cancel the reserve with it so the map runs edge to edge under the bar,
   and any mismatch either strands a gap or hides content behind the bar.
   The bar's own top border and padding (Bottom tab bar, below) must say the
   same numbers; ux-standalone-bottom-frontend.mjs adds them up.
   (--safe-bottom is the home indicator's, in the installed app only - see SAFE
   AREA near the top of this file; it is 0 in a browser tab, and 0 too under
   html.viewport-short, where the indicator sits in iOS's band below the page
   and the floor is all the bar keeps.) */
html.mobile {
  --touch-min: 3.25rem;
  --mobile-tab-bar-inset: calc(var(--touch-min) + 0.3125rem + max(0.25rem, var(--safe-bottom)));
  --mobile-tab-bar-outset: calc((var(--touch-min) + 0.3125rem) * -1 - max(0.25rem, var(--safe-bottom)));
  /* The selected-job bar's width once a rotation turns its row into a side
     rail (a job picked). The map beside it is the viewport less this, which is
     what anything sized against that map has to subtract; with no job picked
     there is no rail, and a width that subtracts it is only narrower than it
     could be. */
  --mobile-logs-rail-w: 17rem;
}

/* Shared full-bleed helper. Edge-to-edge on mobile means cancelling
   .app-main's 1rem side gutter with a NEGATIVE MARGIN on the container and
   restoring the inset on the cells inside it - never as padding back on the
   container, which just re-imposes the gutter it removed. html.mobile
   .settings-tabs is the worked example (its sticky chip row uses the same
   idiom with a negative top margin as well); the two full-screen map pages are
   the other, and they restore nothing because they want every pixel.
   Set --mobile-fullbleed-gutter on the container when its own padding differs
   from .app-main's 1rem (a card with 1.125rem padding, say). */
html.mobile .mobile-fullbleed {
  margin-left: calc(-1 * var(--mobile-fullbleed-gutter, 1rem));
  margin-right: calc(-1 * var(--mobile-fullbleed-gutter, 1rem));
}
html.mobile .mobile-fullbleed-inset {
  padding-left: var(--mobile-fullbleed-gutter, 1rem);
  padding-right: var(--mobile-fullbleed-gutter, 1rem);
}

/* Collapse the desktop two-column grid to a single column; the fixed bottom tab
   bar replaces the sidebar. */
html.mobile .app-shell {
  grid-template-columns: 1fr;
  grid-template-areas:
    "banner"
    "update"
    "main";
}
/* A thumb has to find it: the touch floor on the one button the bar has, and
   the text may wrap onto a second line on a narrow screen rather than push the
   button off it. */
html.mobile .app-update-bar { padding: 0.25rem 1rem; }
html.mobile .app-update-bar-button { min-height: var(--touch-min); }
html.mobile .app-nav { display: none; }

/* The desktop shell locks body scroll and scrolls inside per-page containers
   (.app-page and its children each own overflow). On mobile we instead let the
   whole main column scroll as one document: .app-main scrolls, and each page
   flows at natural height (overflow visible) in a single column. Bottom padding
   keeps content clear of the fixed tab bar. Complex full-height pages (Logs map,
   Airspace) still rely on their own inner layout and will get per-page mobile
   treatment in stage 2 - this default just keeps them usable. */
/* The ACTIVE PAGE is the one scroll container on mobile - not .app-main -
   so the scrollbar starts below the header instead of running over it. The
   header sits outside the scroller and needs no stickiness; scrolled content
   is clipped at the page's top edge, so nothing can ever show behind the
   header. .app-main keeps the side gutters and the tab-bar clearance (which
   grows by the device's bottom safe-area inset); the page cancels the side
   padding with negative margins so the scrollbar hugs the screen edge. */
html.mobile .app-main {
  overflow: hidden;
  padding: 0 1rem var(--mobile-tab-bar-inset);
}
html.mobile .app-page {
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  margin: 0 -1rem;
  padding: 0 1rem 0.5rem;
}

/* Keep the phone's sticky bar visible while the content scrolls. Negative side
   margins let it span the full width with the app background behind it.
   EQUAL PADDING ABOVE AND BELOW THE ROW, so the row (the organisation and the
   bell) sits on the bar's middle. It was 0.75rem above and 0.5rem below, which
   put the bell 1.75px under the bar's centre, at the foot of the dark band the
   installed app paints from the status bar down (Josh, 2026-11-01: "not in the
   middle"). 0.625rem each way keeps the bar 63px tall at the 14px root.
   ux-bell-centred-frontend.mjs holds it.
   NO BOTTOM MARGIN, ON EVERY PAGE (Josh, 2026-11-06: "We can remove the spacer
   under the header also. It appears on a few pages."). The bar used to carry a
   0.875rem bottom margin. A margin sits outside the page's scroller, so it was
   a strip of the shell's gradient between the bar and the page, there at rest
   and still there while the page scrolled. Pages that run edge to edge (the
   Plan list, Flights, Airspace) showed it as a dead band, and five :has()
   rules switched it off page by page. The Plan one did not hold on Josh's
   iPhone: his screenshot shows the 12pt band. Now the bar has no margin
   at all, and a page whose first card is inset gets its breathing room inside
   its own scroller (THE PHONE PAGE'S TOP GAP, next), where it scrolls away with
   the content. A new page starts flush under the bar.
   ux-phone-header-gap-frontend.mjs holds it. */
html.mobile .app-main > header {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: 0 -1rem;
  padding: 0.625rem 1rem;
  background: var(--bg);
}
/* THE PHONE PAGE'S TOP GAP. The pages whose first thing is an inset card (the
   Dashboard's tiles, the Clients and Aircraft cards) get 0.875rem above it,
   roughly the 1rem side gutter at the 14px root. It is the page's own top
   padding, inside the scroller, so it scrolls away. Only for a page with no
   pinned row: a scroller's top padding moves where a sticky child freezes, so
   Billing, whose side-tab row pins under the bar, takes the same gap on its
   layout instead (html.mobile #financialsPage .financials-layout, with the
   Billing page; measured: on the scroller its row froze 5px under the bar).
   Pages that start with something full-bleed (the Plan list's card, the tab
   rows on Documents & Reporting, Settings and Account, the two maps) are not
   listed and sit flush under the bar. */
html.mobile :is(#dashboardPage, #clientsPage, #aircraftPage) {
  padding-top: 0.875rem;
}
/* The bell sits hard against the screen edge, where a thumb is least accurate;
   its padding made it 38.7px wide at the 14px root. The floor gives it the
   height, this gives it the width; the bell stays in the middle of the wider
   box because the button centres it by flex (.alerts-bell). */
html.mobile .alerts-bell { min-width: var(--touch-min); }
/* Downwards and right-aligned from the sticky bar, which is the top of the
   screen - the opposite of the sidebar footer it opens from on a desktop. */
html.mobile .alerts-dropdown {
  top: calc(100% + 0.5rem);
  bottom: auto;
  left: auto;
  right: 0;
}
/* THE PHONE'S STICKY BAR: the organisation's name, and the alerts bell on its
   right. One treatment on every page, because there is nothing else in the bar
   to share it with - the page title and subtitle that used to fill it are gone
   (Josh, 2026-09-27), and on a phone the highlighted bottom tab says which page
   this is.
   Every page, not the three it used to be. The sidebar that names the active
   organisation on every desktop screen is not on a phone at all, so a contract
   pilot flying for two operators could work a whole day - book jobs, file
   flights, raise an invoice - with nothing on screen saying whose books it was
   going into. The Dashboard, Jobs and Logs were the first three because they
   were where getting it wrong cost the most; the bar is free now, so the answer
   is simply always there. */
html.mobile .header-org {
  display: flex;
  flex: 1 1 auto;
  margin-bottom: 0;
  gap: 0.625rem;
  min-width: 0;
}
html.mobile .header-org .dashboard-org-logo-wrap {
  width: 2rem;
  height: 2rem;
  border-radius: 0.5rem;
  flex: 0 0 auto;
}
html.mobile .header-org .dashboard-org-fallback { font-size: 1rem; }
html.mobile .header-org .dashboard-org-name {
  font-size: 1.0625rem;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile #dashboardPage > .dashboard-org { display: none; }
/* It opens the switcher (see the click binding in app.js), so it has to answer
   a press like the other things that do. */
html.mobile .header-org { cursor: pointer; }
html.mobile .header-org:active { opacity: 0.7; }
/* The page notice floats under this bar and takes none of its height (the
   html.mobile ux:U0b block at the foot of the UX rules). */

/* The corner widgets are fixed to the bottom-right of the viewport, which on a
   phone is exactly where the tab bar sits - the upload progress ring and the
   outbox indicator were being covered by it. Lift the whole stack clear.
   Desktop has no tab bar and is untouched. */
html.mobile .corner-widget-stack {
  bottom: calc(0.75rem + var(--mobile-tab-bar-inset) + var(--corner-widget-lift, 0px));
  left: calc(1rem + var(--safe-left));
  right: calc(1rem + var(--safe-right));
  max-width: none;
}

/* ---- Bottom tab bar ----
   As short as Apple's own apps make it (Josh, 2026-11-01): a sliver of padding
   above the tabs, the tabs at the full touch floor, and below them the home
   indicator's strip, never less than a sliver. The top border, the two padding
   lengths and the tab's own contents (its padding, the icon, the gap and the
   label's line, which must fit inside --touch-min) are the arithmetic
   --mobile-tab-bar-inset adds up (Shared mobile layout constants, above):
   change one and the other in the same edit. */
.mobile-tab-bar { display: none; }
html.mobile .mobile-tab-bar {
  display: flex;
  position: fixed;
  left: var(--safe-left);
  right: var(--safe-right);
  bottom: 0;
  z-index: 60;
  padding: 0.25rem 0.25rem max(0.25rem, var(--safe-bottom));
  background: var(--tab-bar-bg);
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  backdrop-filter: var(--backdrop-bar);
}
html.mobile .mobile-tab {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-height: var(--touch-min);
  padding: 0.25rem 0;
  background: transparent;
  border: 0;
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
html.mobile .mobile-tab.active { color: var(--green); }
html.mobile .mobile-tab-icon { width: 1.5rem; height: 1.5rem; display: block; }
html.mobile .mobile-tab-icon svg { width: 100%; height: 100%; }
html.mobile .mobile-tab-label { line-height: 1; }

/* ---- "More" bottom sheet ---- */
.mobile-more-sheet-overlay { display: none; }
html.mobile .mobile-more-sheet-overlay {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 70;
}
/* The global `[hidden] { display: none !important }` rule keeps the sheet closed
   until JS clears the attribute; restate it so specificity never surprises us. */
html.mobile .mobile-more-sheet-overlay[hidden] { display: none; }
.mobile-more-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim-2);
}
/* The panel scrolls, and what scrolls inside it is clipped at its bottom edge, so
   with the edge at the screen's a row could rest inside the home indicator's band
   (landscape, the account row). The panel stops above the band instead
   (--safe-bottom is 0 in a browser tab, so nothing moves there) and the shadow
   carries the sheet's colour down behind the indicator. */
.mobile-more-panel {
  position: absolute;
  left: var(--safe-left);
  right: var(--safe-right);
  bottom: var(--safe-bottom);
  max-height: calc(80dvh - var(--safe-bottom));
  overflow-y: auto;
  padding: 0.5rem 0.75rem 1rem;
  background: var(--sheet-bg);
  box-shadow: 0 var(--safe-bottom) 0 0 var(--sheet-bg);
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  animation: mobileMoreSlideUp 0.22s ease;
}
@keyframes mobileMoreSlideUp {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
.mobile-more-handle {
  width: 2.5rem;
  height: 0.25rem;
  margin: 0.25rem auto 0.75rem;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.4);
}
.mobile-more-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 2.75rem;
  padding: 0.625rem 0.75rem;
  background: transparent;
  border: 0;
  border-radius: 0.625rem;
  color: var(--text);
  font-size: 0.9375rem;
  text-align: left;
  cursor: pointer;
}

/* Account/organisation row at the foot of the More sheet (opens the org
   switcher - the sidebar footer control it mirrors is hidden on mobile). */
.mobile-more-divider {
  border-top: 0.0625rem solid var(--line);
  margin: 0.375rem 0;
}
.mobile-more-account .app-nav-context-text { min-width: 0; flex: 1; }
.mobile-more-account .app-nav-username { font-size: 0.75rem; color: var(--muted); }

/* "Install app" at the foot of the More sheet (app.js, openMobileMore), only
   while the app is not installed. Where the browser has no prompt it opens the
   steps under itself rather than leaving the sheet. */
.mobile-more-hint {
  margin: 0 0 0.375rem;
  /* Lines up under the label: the row's padding, its icon and the gap between. */
  padding: 0 0.75rem 0 2.875rem;
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.45;
}
/* A phone held sideways is short: the sheet is 80% of a viewport of 390px or less,
   and the Install row (a suggestion, and its steps when opened) above the account row
   pushed the organisation switcher and Sign out off the first screen. Where the
   viewport is that short the Install row, and the steps under it, go after the
   account row and the sheet scrolls to them; the markup keeps its order (the row
   before the account row), so portrait, and an installed app, which has no Install
   row, are laid out exactly as they were. 520px is the cut-off the map controls use
   for the same reason: no phone is that short held upright. */
@media (max-height: 520px) {
  html.mobile .mobile-more-list:has(> .mobile-more-install) {
    display: flex;
    flex-direction: column;
  }
  html.mobile .mobile-more-install,
  html.mobile .mobile-more-hint { order: 1; }
}

/* The offer to install after a sign-in (app.js, maybeOfferInstall): a bottom sheet
   on a phone, the More sheet's look, over the tab bar. Only ever opened under
   html.mobile; elsewhere it would be an ordinary small dialog. Its card stops above
   the home indicator and keeps off the notch by its own padding (the overlay has
   none), so its words never sit under either. In the installed iPhone app's short
   viewport (html.viewport-short) it is a confirm card like any other and nothing
   here singles it out: whatever that viewport does to --safe-bottom and a confirm
   card's scrim, the sheet follows. */
html.mobile .modal-overlay.install-offer { align-items: flex-end; justify-content: center; }
/* .modal-confirm in the selector: the small-dialog rule further down sets the same
   properties at one class less, and would otherwise win by coming later. */
html.mobile .modal-overlay.modal-confirm.install-offer > .modal-card {
  width: 100%;
  max-width: none;
  max-height: 85dvh;
  margin: 0;
  padding-left: calc(1.125rem + var(--safe-left));
  padding-right: calc(1.125rem + var(--safe-right));
  padding-bottom: calc(1.125rem + var(--safe-bottom));
  border: 0;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  border-radius: 1rem 1rem 0 0;
  background: var(--sheet-bg);
  animation: mobileMoreSlideUp 0.22s ease;
}
.install-offer-handle { display: none; }
html.mobile .install-offer-handle {
  display: block;
  width: 2.5rem;
  height: 0.25rem;
  margin: -0.5rem auto 0.75rem;
  border-radius: 999px;
  background: rgba(var(--rgb-slate), 0.4);
}
.install-offer #installOfferTitle { font-size: 1.125rem; margin-bottom: 0.5rem; }
.install-offer .card-copy { margin: 0 0 1rem; }
.install-offer-more { margin: 0 0 1rem; }
.install-offer-more summary { cursor: pointer; color: var(--muted); font-weight: 600; min-height: var(--touch-min, 2.75rem); display: flex; align-items: center; gap: 0.5rem; list-style: none; }
/* display: flex drops the disclosure triangle: the Support guide's chevron stands in,
   turning down when the steps are open. */
.install-offer-more summary::-webkit-details-marker { display: none; }
.install-offer-more summary::before {
  content: "\25B8";
  color: var(--muted);
  font-size: 0.85em;
  transition: transform 0.15s ease;
}
.install-offer-more[open] summary::before { transform: rotate(90deg); }
.install-offer-more .card-copy { margin: 0.25rem 0 0; }
.install-offer .modal-actions-row { justify-content: flex-start; }
/* The by-hand install steps (lib/install.js, renderInstallCopy): a numbered list in
   the Preferences card, the offer after a sign-in and the More sheet. A button the person
   has to find is drawn beside its word, sized to the text and kept on one line with
   it. */
.install-steps { margin: 0; padding-left: 1.25rem; }
.install-steps li { margin: 0 0 0.375rem; padding-left: 0.25rem; }
.install-steps li:last-child { margin-bottom: 0; }
.install-term { white-space: nowrap; font-weight: 600; color: var(--text); }
.install-glyph { width: 1.15em; height: 1.15em; vertical-align: -0.2em; margin-right: 0.25em; }
.install-term-bare .install-glyph { margin: 0 0 0 0.125em; }
/* Preferences > This device: while its button row is closed the line under the
   title is the last thing in the card, and keeps no gap for what is not there. */
#installCard:has(#installCardActions[hidden]) #installCardText { margin-bottom: 0; }
/* ...and its alerts part (app.js renderPushAlerts), ruled off from the install
   line above it the way Profile's security parts are. Hidden, it draws nothing. */
.push-alerts {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
}
.push-alerts .card-copy { margin-bottom: 0.625rem; }
.push-alerts .push-alerts-hint { margin-top: -0.375rem; color: var(--muted); font-size: 0.875rem; }
/* The answer to a press sits level with the buttons it answers, not at the top of
   the row (a flex row stretches its items, and the text starts at the top). */
.push-alerts .modal-actions-row > .save-status { align-self: center; margin-inline-start: 0; }
/* Where focus goes after a press when no button is left to take it
   (app.js focusPushCardAfter): a programmatic stop, not a control, so no ring. */
.push-alerts #pushAlertsText:focus,
.push-alerts #pushAlertsStatus:focus { outline: none; }
.push-alerts .push-devices { margin-top: 0.875rem; }
.push-alerts .push-devices .scope-line { margin: 0 0 0.5rem; }
/* PHONES (16 px root): a device row keeps its name and dates beside Remove, and the
   name wraps rather than being cut: the browser's own label is what tells two
   devices apart. The row that carries the "This device" marker has three things to
   hold beside the text, and "Chrome on Android" was cut to "Chrome on An..." at
   360 px: its marker and Remove take a row of their own under the name and dates. */
html.mobile .push-device-row .queue-name { white-space: normal; overflow-wrap: anywhere; }
html.mobile .push-device-row:has(.status-pill) { grid-template-columns: minmax(0, 1fr); }
html.mobile .push-device-row:has(.status-pill) .personnel-cert-row-actions > .danger-button { margin-inline-start: auto; }
.platform-push-facts { margin: 0; color: var(--muted); overflow-wrap: anywhere; }

/* Org switcher popover: centre it as a dialog on mobile (its desktop
   placement hugs the now-hidden sidebar footer). */
html.mobile .modal-overlay.popover {
  align-items: center;
  justify-content: center;
  background: var(--scrim-4);
}
html.mobile .modal-overlay.popover .modal-card {
  width: min(22rem, calc(100vw - var(--safe-left) - var(--safe-right) - 2rem));
  margin: 0;
}

/* Desktop-site-on-touch: portrait shows a full-screen rotate prompt; landscape
   gets the desktop layout. Driven by html.force-landscape + html.is-portrait
   (set from matchMedia in app.js), so it's forceable on desktop for testing. */
.rotate-overlay { display: none; }
html.force-landscape.is-portrait .rotate-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 100000;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 2rem;
  text-align: center;
  background:
    var(--rotate-overlay-bg);
}
.rotate-overlay-icon { width: 3rem; height: 3rem; color: var(--green); }
.rotate-overlay-icon svg { width: 100%; height: 100%; }
.rotate-overlay strong { font-size: 1.25rem; color: var(--text); }
.rotate-overlay p { margin: 0; max-width: 24rem; color: var(--muted); line-height: 1.5; }
.mobile-more-item:hover,
.mobile-more-item:active { background: rgba(var(--rgb-slate), 0.1); }
.mobile-more-item .nav-icon {
  display: inline-flex;
  width: 1.375rem;
  height: 1.375rem;
  color: var(--muted);
  flex: none;
}
.mobile-more-item .nav-icon svg { width: 100%; height: 100%; }

/* ---- Full-screen modal sheets ---- */
/* Every .modal-card becomes a full-screen sheet on mobile. Compound variants
   (.modal-card.risk-assessment-card, .document-viewer-card, etc.) set their own
   width/max-width at higher specificity, so force the overrides. Popover
   overlays (dropdown menus) are excluded - they are not dialogs. */
html.mobile .modal-overlay:not(.popover):not(.modal-confirm) {
  align-items: stretch;
  justify-content: stretch;
  /* The sheet sits inside the safe area: below the status bar, above the home
     indicator, off the notch in landscape. All four are 0 in a browser tab, so
     the sheet is the whole screen there, as it was. */
  padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
}
html.mobile .modal-overlay:not(.popover):not(.modal-confirm) > .modal-card {
  width: 100% !important;
  max-width: none !important;
  height: calc(100dvh - var(--safe-top) - var(--safe-bottom)) !important;
  max-height: calc(100dvh - var(--safe-top) - var(--safe-bottom)) !important;
  margin: 0 !important;
  border-radius: 0 !important;
  overflow-y: auto;
}
/* The copy line under a sheet's header pulls up 0.125rem (.card-copy), which
   on a phone painted over the bottom 2px of the header's Close button, a
   --touch-min target (50 of its 52px could be hit). */
html.mobile .modal-header + .card-copy { margin-top: 0; }
/* The Create job steps share the sheet's width instead of running past it.
   At the 16px phone base the five tabs needed 417px: on a 390px phone "Risk
   assessment" was cut at the edge, and on a 360px one, on its last step, the
   active tab was almost all off-screen - and these tabs take no taps, so it
   could not be brought back. Each tab sizes to its label from 0.25rem of
   padding a side, with no gap between them, and the room left over is shared
   out (a label may wrap rather than be cut). */
html.mobile #jobCreateTabs { gap: 0; }
html.mobile #jobCreateTabs .job-preview-tab {
  flex: 1 1 auto;
  min-width: 0;
  padding-inline: 0.25rem;
  white-space: normal;
}
/* The two parts share the row the same way: Plan, with four steps, takes what is
   left; the part names stay one small line over their steps. */
html.mobile .job-create-part[data-create-part="plan"] { flex: 1 1 auto; }
html.mobile .job-create-part + .job-create-part {
  margin-left: 0.25rem;
  padding-left: 0.25rem;
}
html.mobile .job-create-part-steps { gap: 0; }
html.mobile .job-create-part-label {
  padding-inline: 0.25rem;
  font-size: 0.6875rem;
  line-height: 1.2;
}
/* Small confirm dialogs (delete / discard) stay compact centred cards on
   mobile - a two-button question doesn't warrant a full-screen sheet. */
html.mobile .modal-overlay.modal-confirm > .modal-card {
  width: min(24rem, calc(100vw - var(--safe-left) - var(--safe-right) - 2rem));
  height: auto;
  max-height: 80dvh;
  overflow-y: auto;
}

/* ---- Forms ---- */
/* Collapse multi-column form grids (and their compound variants) to one column. */
html.mobile .form-grid { grid-template-columns: 1fr !important; }

/* Form text is 16px on a phone. iOS zooms the page into any text field under
   16px the moment it is focused, and leaves it zoomed. The fields' own rules
   (0.875rem and down) are written for the desktop's 85% root, where they are
   11.9px; at the phone's 87.5% root that is 12.25px, so this rule lifts
   them. max() keeps 16px even where a reader has set a smaller default font.
   The specificity is deliberate: (0,5,2) for the fields and (0,2,1) for the
   widgets, written out rather than in :where(), so the per-component field
   rules (0,1,1 to 0,3,2) do not win back. Two widget rules still do, and
   both are buttons: the Airspace layers sheet's map picker (an ID rule,
   0.9375rem, 13.125px at the 14px root) and the Airspace time bar's date
   button (0,3,1, 0.8125rem, 11.375px), one compact control in a row of three.
   The four widget triggers are buttons, and iOS never focus-zooms a button;
   they are here so a dropdown and the text field beside it stay the same
   height. The dropdown's search box is a real input. */
html.mobile :is(input, select, textarea):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),
html.mobile :is(.searchable-select-trigger, .multiselect-dropdown-trigger, .date-trigger, .searchable-select-search) {
  font-size: max(1rem, 16px);
}
/* Two fields are already past 16px and keep their own size: the code step's
   invisible input, which owns the caret and the selection over the six boxes
   and is sized from them (.otp-field input), and the recovery code
   (.login-recovery input). */
html.mobile #loginMfaCode { font-size: clamp(1.25rem, calc(var(--otp-box-width) * 1.0), 3.25rem); }
html.mobile #loginRecoveryCode { font-size: 1.25rem; }
/* The quote and invoice Job picker: the row of picker and "open this job"
   button is a grid item of its field, and a grid item's automatic minimum is
   its min-content width - the whole job label on one line, plus the button.
   At the 16px phone base that is 364px for "[52] BC2 (invoiced) - Blue
   Cliffs Station", so the row ran past its card and, on a 360px phone, put the
   button off the screen. As .searchable-select and .multiselect-dropdown
   already do, the row may be narrower than its content; the label ellipsises. */
html.mobile .job-select-row { min-width: 0; }

/* ---- Touch targets ---- */
/* Text inputs and ordinary buttons get the floor (--touch-min, declared with
   the other mobile constants above). It was 2.75rem, which read as a floor but
   left the whole product 6.8px short of a fingertip: a phone sweep of the nine
   pages and every sheet, drawer and modal on them measured 37.4px on the Jobs
   list rows, the Logs band's flight rows, the settings chips, and every input,
   select and button in the app.
   Checkboxes and radios are excluded: a min-height on a native square control
   stretches it into a tall narrow rectangle in every engine that scales it. The
   touch target for those goes on the label row instead, which is what a person
   actually taps. */
html.mobile button,
html.mobile input:where(:not([type="checkbox"]):not([type="radio"])),
html.mobile select,
html.mobile textarea,
html.mobile .app-nav-link {
  min-height: var(--touch-min);
}

html.mobile label:where(:has(> input[type="checkbox"])),
html.mobile label:where(:has(> input[type="radio"])) {
  min-height: var(--touch-min);
}

/* A dropdown's options are TAPPED on a phone. The app draws its own option list
   rather than opening the platform's wheel picker, so each row has to be as easy
   to hit as the control that opened it - at the authored 0.5rem padding a row
   came out about 28px, well under the floor every other target in the app
   keeps. Centred by its line box rather than by flex: a flex row turns the
   label into an anonymous item and `text-overflow: ellipsis` stops working on
   it, so a long client name would be cut off mid-letter with no ellipsis. */
html.mobile .searchable-select-option,
html.mobile .searchable-select-option-disabled {
  min-height: var(--touch-min);
  line-height: calc(var(--touch-min) - 1rem);
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

/* Square and circular icon controls keep their 1:1 shape. The floor above
   outranks their authored `height`, turning a 22px circle into a 22x44px
   capsule and the legend minimise control into a box twice as tall as it is
   wide; they get a real square target instead of being stretched, and the hit
   box below carries them the rest of the way to the floor. */
html.mobile .help-button,
html.mobile .map-legend-minimize-button,
html.mobile .log-details-toggle,
html.mobile .mobile-drawer-close {
  min-height: 0;
  width: 2.25rem;
  height: 2.25rem;
}
/* The time segments are tapped to open the keypad, so they need the floor on
   the width axis too - the generic input rule above only gives them height.
   They are readonly here (the sheet draws its own keypad rather than letting
   the OS keyboard rise over it), so suppress the caret and the text-selection
   handles that would otherwise say "type into me". */
html.mobile .date-cal-timeseg {
  width: var(--touch-min);
  caret-color: transparent;
  user-select: none;
}

/* Hit box. A control that has to stay visually small - an icon square in a
   sheet header, a compact row chip (guide §5), a delete cross on a list row -
   still gets a full-size target: an invisible box centred on it,
   `max()` so it never shrinks a control already past the floor on one axis.
   It is out of flow, so no row grows and no layout moves, and each selector
   below was measured to spill only into padding or a gap - never over the
   neighbouring control, which would be a worse bug than the small target. */
html.mobile .help-button,
html.mobile .map-legend-minimize-button,
html.mobile .log-details-toggle,
html.mobile .mobile-drawer-close,
html.mobile .hazard-actions-head button,
html.mobile .flights-list-header-actions .secondary-button,
html.mobile .flights-list-footer .secondary-button {
  position: relative;
}
html.mobile .help-button::after,
html.mobile .map-legend-minimize-button::after,
html.mobile .log-details-toggle::after,
html.mobile .mobile-drawer-close::after,
html.mobile .hazard-actions-head button::after,
html.mobile .flights-list-header-actions .secondary-button::after,
html.mobile .flights-list-footer .secondary-button::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--touch-min));
  height: max(100%, var(--touch-min));
  transform: translate(-50%, -50%);
}

/* The calendar's month arrows and its Clear / Done row are ordinary buttons, so
   the floor above already gives them the height; they were 27.2px and 31.7px
   WIDE, which is the axis a date picker is actually tapped along. */
html.mobile .date-cal-nav {
  width: var(--touch-min);
  height: var(--touch-min);
}
html.mobile .date-cal-action { min-width: var(--touch-min); }
/* A day is its column's square. At the 16px phone base the calendar's 23.5rem
   cap is 376px, wider than the sheet on any phone under 408px, so seven 52px
   squares no longer fit across: the floor above made each day 52px wide in a
   45px column (360) or a 49px one (390) and neighbours overlapped, splitting a
   tap between two days. Square to the column, a day is 45 to 49px (44px at the
   old root) and the seven still line up under MON..SUN. */
html.mobile .date-cal-day { min-height: 0; }

/* ---- Checklists ---- */
/* A checklist template's item row carries a grip, the label, a "Required"
   toggle and a delete control - one control too many for a phone-width row.
   Let it wrap so the toggle drops to its own line instead of crushing the
   label input down to a few characters. */
/* A row with no toggle (a hazard) keeps one line: the field takes what the
   three touch-sized buttons leave. A row WITH one (a checklist item) puts the
   field on its own line and the toggle and buttons on the next, so the label
   is never crushed to a few characters. */
html.mobile .list-editor-input { flex: 1 1 0; }
html.mobile .list-editor-row:has(.list-editor-toggle) { flex-wrap: wrap; }
html.mobile .list-editor-row:has(.list-editor-toggle) .list-editor-input { flex: 1 1 calc(100% - 1.5rem); }
html.mobile .list-editor-toggle { margin-right: auto; }

/* Answering a checklist is the one thing in this product done at the aircraft
   rather than at a desk, so the answer buttons get the floor on BOTH axes - the
   generic button rule only sets the height, and these are tapped along their
   width as much as anything in the app. */
html.mobile .checklist-choice,
html.mobile .field-choice {
  min-height: var(--touch-min);
  min-width: var(--touch-min);
}

/* ---- Tables ---- */
/* Let wide data tables scroll horizontally inside their wrapper instead of
   forcing the page wide. */
html.mobile .table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
html.mobile .table-wrap .data-table { min-width: max-content; }

/* ==========================================================================
   Mobile core pages (stage 2): Dashboard, Jobs (drill-in), Settings
   All rules keyed off `html.mobile` (no @media) - forceable on desktop with
   document.documentElement.classList.add("mobile").
   ========================================================================== */

/* ---- Dashboard ---- */
/* Stat tiles: two per row, with the desktop's oversized numbers dialled back. */
html.mobile .dashboard-headline {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
html.mobile .dashboard-headline .stat-tile-value { font-size: 1.125rem; }
/* Donut cards sit side by side, scaled down: tighter padding, no subtitle,
   smaller title, and a shorter chart area (the legend moves below the donut -
   see renderDashboardDoughnut's mobile legend config). */
html.mobile .dashboard-charts-row {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}
html.mobile .dashboard-charts-row .card {
  padding: 0.75rem;
  margin-bottom: 0;
}
html.mobile .dashboard-charts-row .card > .card-title {
  font-size: 0.875rem;
  margin-bottom: 0.125rem;
}
html.mobile .dashboard-charts-row .card-copy { display: none; }
html.mobile .dashboard-chart-aircraft,
html.mobile .dashboard-chart-pilot { display: flex; flex-direction: column; }
html.mobile .dashboard-chart-body {
  display: block;
  max-height: none;
  margin-top: 0;
}
html.mobile .dashboard-chart-wrap {
  flex: none;
  aspect-ratio: auto;
  min-height: 0;
  max-width: none;
  height: clamp(9rem, 42vw, 13rem);
  margin-top: 0.25rem;
  margin-bottom: 0.25rem;
}
/* The legend under the fixed-height canvas, so both donuts stay the same size no matter
   how many series each lists (built in renderDashboardDoughnut; on a computer the same
   list sits beside the ring). */
html.mobile .dashboard-donut-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.5rem;
  justify-content: center;
  max-height: none;
  overflow-y: visible;
  margin-top: 0.375rem;
  font-size: 0.6875rem;
  color: var(--fg-soft);
}
html.mobile .dashboard-donut-legend:empty { display: none; }
html.mobile .dashboard-donut-legend-item {
  display: inline-flex;
  gap: 0.25rem;
  max-width: 100%;
  padding: 0;
}
html.mobile .dashboard-donut-legend-swatch {
  width: 0.5rem;
  height: 0.5rem;
}
html.mobile .dashboard-donut-legend-time { margin-inline-start: 0; color: inherit; }
/* The phone's order (Josh, 2026-11-01): the headline figures, Quick actions,
   This week, the two rings side by side, Recent occurrences. app.js MOVES the
   actions card and the This week card out of .dashboard-main into the page,
   under the figures' card, in front of .dashboard-main (syncDashboardPhoneOrder,
   which puts them back when the layout becomes a computer's); moved rather than
   reordered with `order`, so Tab and a screen reader meet the cards in the order
   they are seen. The page is a flex column holding those three cards and
   .dashboard-main, so it spaces them with its own gap, the same 1rem
   .dashboard-main puts between the rings and Recent occurrences, and no card
   keeps a margin of its own (the figures' card had 0.75rem, on top of the gap).
   The figures' card is the first thing on the page (the org header above it is
   the computer's; a phone shows the org in the top bar), so it starts at the
   page's top edge, as the actions card did when it led: a gap is only ever
   between two cards.
   The desktop's flex:1 + internal list scrolling assumes a fixed-height page;
   on the mobile page-scroller everything flows at natural height instead
   (the page itself scrolls). */
html.mobile #dashboardPage { gap: 1rem; }
html.mobile #dashboardPage > .card {
  flex: none;
  margin-bottom: 0;
}
html.mobile .dashboard-main {
  flex: none;
  min-height: auto;
  margin-top: 0;
}
html.mobile .dashboard-charts-row {
  flex: none;
  max-height: none;
}
html.mobile .dashboard-bottom-row {
  grid-template-columns: minmax(0, 1fr);
  flex: none;
  min-height: auto;
}
html.mobile .dashboard-thisweek-list,
html.mobile .dashboard-occurrence-list {
  flex: none;
  min-height: auto;
  max-height: none;
  overflow-y: visible;
}
/* Quick actions: heading back on its own line above a 2-column button grid. */
html.mobile .dashboard-actions {
  flex-direction: column;
  align-items: stretch;
  gap: 0.75rem;
}
html.mobile .card.dashboard-actions > .card-title { white-space: normal; }
html.mobile .dashboard-actions-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* Upload logs is not among a phone's quick actions (Josh, 2026-11-01): the Flights
   page has its own. The button stays in the page, marked data-phone-hidden in
   index.html, and syncDashboardActionCount leaves it out of the grid's count on a
   phone, so the count is what is shown. */
html.mobile .dashboard-action[data-phone-hidden] { display: none; }
/* Three or five shown (by the person's billing and documents) would leave one
   button alone on the last of the two columns' rows. Create job, always offered
   and always first, takes the whole first row instead, and the rest pair up. */
html.mobile .dashboard-actions-grid:is([data-count="1"], [data-count="3"], [data-count="5"], [data-count="7"]) > .dashboard-action[data-action="job"] {
  grid-column: 1 / -1;
}
/* Occurrence rows: let the description wrap to its own line under the title
   instead of ellipsizing inline on narrow screens. */
html.mobile .dashboard-occurrence-titlerow {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
}
html.mobile .dashboard-occurrence-desc {
  white-space: normal;
}

/* ---- Jobs page ---- */
/* Single column; the jobs list runs full width. The desktop grid placement
   (grid-column/row) on the cards is inert in this flex column. */
html.mobile .jobs-page-layout {
  display: flex;
  flex-direction: column;
  height: auto;
  overflow: visible;
}

/* Toolbar. The card's own "Jobs" heading is dropped: it was a second copy of
   the sticky bar's "Jobs / Manage jobs" between the pilot and the first job,
   and now that bar carries no title at all, the highlighted bottom tab is what
   says which page this is - the same answer the whole app gives since
   2026-09-27. A title here would be the only one in the app. */
html.mobile .jobs-toolbar-head > strong,
html.mobile .jobs-toolbar-head > .card-title { display: none; }
/* One grid on a phone: the head and filter wrappers step aside (display:
   contents) so the four controls are cells of it - status, client, search, and
   Sort, by the names the computer's toolbar gives them (.jobs-toolbar-filters).
   Its rows, columns and gaps are THE PLAN PAGE ON A PHONE's (further down): the
   two pickers, then the search box with Sort at its end. (The computer's Sort
   row had a wrapper too, until 2026-11-02.) */
html.mobile .jobs-table-toolbar {
  display: grid;
  align-items: center;
}
html.mobile .jobs-toolbar-head,
html.mobile .jobs-toolbar-filters { display: contents; }
/* Thumb-sized on a phone; the grid above already lays them out. */
html.mobile .jobs-client-filter,
html.mobile .jobs-status-filter,
html.mobile .jobs-search-input,
html.mobile .jobs-filter-field .searchable-select-trigger { height: var(--touch-min, 2.75rem); }

/* Create job leads the card on mobile (thumb-reachable, no scroll-to-bottom
   hunt); the flex order puts it between the toolbar and the table, centred
   like the desktop button. */
html.mobile .jobs-table-card {
  display: flex;
  flex-direction: column;
}
html.mobile .jobs-table-card > .jobs-table-toolbar { order: -2; }
html.mobile .jobs-table-card > #addJobButton {
  order: -1;
  align-self: center;
  margin: 0.75rem 0;
}

/* ---- Jobs list rows on mobile ----------------------------------------- */
/* Five columns never fitted a phone, and the columns that were dropped to make
   them fit (Date and Client) are the two fields that tell one job from another:
   most names are "<work type> - <place>" and half of them repeat. Worse, the
   warning pills jobBadges() appends after the name (airspace, no-go risk,
   compliance, weather, overdue) shared that one ellipsised cell, so on a phone
   they were painted past its clip edge and the safety flag the app had already
   computed never reached the screen.
   So on mobile the row stops being a table row and becomes a stacked block:
     line 1  #number  name…  [!]                     [status]
     line 2  date · client
   The name gives way before the attention mark loses its room, so nothing
   can clip it: it wraps to a second line where it must (a row 19px taller)
   and ellipsises only after that.
   The body table and its tbody become plain blocks so the grid rows aren't
   wrapped in anonymous table boxes. The HEADER table keeps its table layout -
   it is a separate table (see .jobs-table-head) and stays the sort control. */
/* Two selectors because the general mobile rule above,
   `html.mobile .table-wrap .data-table { min-width: max-content }`, is more
   specific than `.jobs-table` alone - and it made the table as wide as its
   longest job name, pushing the status pill off the side of the screen. */
html.mobile .jobs-table,
html.mobile .table-wrap .jobs-table { min-width: 0; width: 100%; }
html.mobile #jobsTable,
html.mobile #jobsTable > tbody { display: block; }

/* Header: Date and Client stay hidden as COLUMN LABELS - the stacked row
   carries them inline instead - and Status only needs to clear its widest pill
   ("Completed", 55.7px), not the desktop 7.5rem. By class, not position, so a
   column added or removed cannot quietly unhide the wrong one. */
html.mobile .jobs-table-head .job-col-date,
html.mobile .jobs-table-head .job-col-client { display: none; }
html.mobile .jobs-table-head .job-col-status { width: 4.75rem; }

html.mobile #jobsTableBody tr.job-row {
  display: grid;
  /* Column 1 is a FIXED width, not auto: each row is its own grid, so an auto
     column is measured per row and a longer job number would step that one
     row's name out of line with both the header label and every other row.
     2.5rem (34px) clears a four-digit number (32.7px) - job numbers start at
     1000 - and a longer one runs into the 0.5rem gap rather than moving the
     name. Column 2 is sized by the date, column 3 takes the rest; the name
     spans both and the status pill sits in its own content-sized column. Both
     spanning items span the flexible track, so neither can widen column 2. */
  grid-template-columns: 2.5rem max-content minmax(0, 1fr) auto;
  /* The pill spans both lines and is centred in the row (below). */
  grid-template-areas:
    "num name name   status"
    "num date client status";
  align-items: start;
  column-gap: 0.5rem;
  row-gap: 0.125rem;
  /* 1rem of inset restored inside the full-bleed wrap below, and a row tall
     enough to be a real touch target (the 33.1px table row was not).
     The row's height follows its contents (the desktop row's fixed 2.5rem
     does not apply), so a name on two lines (below) makes its row taller
     instead of running into the date; 0.375rem keeps a one-line row at the
     touch floor, 52px, as before. */
  height: auto;
  padding: 0.375rem 1rem;
  min-height: var(--touch-min);
  align-content: center;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}
html.mobile #jobsTableBody td {
  padding: 0;
  border: 0;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
  width: auto;
}
html.mobile #jobsTableBody .job-col-number { grid-area: num; }
/* min-width 0: the name is one line and ellipsises, and a grid item's
   automatic minimum would otherwise be the whole unwrapped name, widening the
   row past the screen. */
html.mobile #jobsTableBody .job-col-name { grid-area: name; min-width: 0; overflow: hidden; }
html.mobile #jobsTableBody .job-col-status {
  grid-area: status;
  text-align: right;
  align-self: center;
  /* The status pill, then the "..." that opens the row's menu (item 25). */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.25rem;
}
/* The "..." stands in the row's own 1rem right inset, so it takes no room from the row: a row is no
   wider or taller for it on any phone (375 and 320 wide included). Its glyph is the inset wide; its
   hit area is a positioned ::after that reaches over only the pill's last 0.3rem, so a tap on the
   pill's middle still opens the job (a gap between them would cost the narrowest phone a row). */
html.mobile #jobsTableBody .job-col-status { gap: 0; }
html.mobile #jobsTableBody .job-col-status .row-menu-button {
  position: relative;
  width: 1rem;
  min-width: 0;
  height: 1.5rem;
  min-height: 0;
  margin: 0 -1rem 0 0;
  padding: 0;
  font-size: 0.8125rem;
  text-align: center;
  line-height: 1;
}
html.mobile #jobsTableBody .job-col-status .row-menu-button::after {
  content: "";
  position: absolute;
  inset: -0.9rem -0.15rem -0.9rem -0.3rem;
}
html.mobile #jobsTableBody .job-col-date { grid-area: date; }
/* The client, too, takes a second line before it ellipsises: at the 16px base
   a long client name ran a few characters past line 2 on a 360px phone (4 of
   58 rows), and on most rows at 320. */
html.mobile #jobsTableBody .job-col-client {
  grid-area: client;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Line 2 is supporting detail, so it reads a step down from the name: the
   secondary style, from the tokens of LIST ROW TEXT. */
html.mobile #jobsTableBody .job-col-date,
html.mobile #jobsTableBody .job-col-client {
  font-size: var(--list-secondary-size);
  line-height: var(--list-line-height);
  color: var(--muted);
}
html.mobile #jobsTableBody .job-col-client::before {
  content: "\00b7";
  margin-right: 0.375rem;
}
/* The name and its warning pills share line 1 here too (see .job-name-line):
   a long name gives way rather than the pills being pushed off it. It used to
   run to two lines with the pills on a third, so rows came in three heights. */
/* At the 16px phone base the usual "<work type> - <place>" name no longer
   fits line 1 (31 rows cut at 360 wide, against 1 at the old root), and the
   cut took the place, the part that tells one job from the next. So the name
   takes a second line where it needs one and only then ellipsises; the mark
   stays level with its first line. A row with a long name (or client, above)
   is 19px taller, 71px, or 89px with both; every other row keeps its 52px. */
html.mobile #jobsTableBody .job-name-line { align-items: flex-start; }
html.mobile #jobsTableBody .job-name-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* The jobs list is the page, so on a phone its panel runs the full width of the
   screen: the card cancels the page gutter (the .mobile-fullbleed idiom) and
   drops the side border and the corner radius that a full-width surface has no
   room for. The toolbar and Create job keep the card's own padding; only the
   list itself goes edge to edge, below. */
html.mobile .all-jobs-card {
  --mobile-fullbleed-gutter: 1rem;
  margin-left: calc(-1 * var(--mobile-fullbleed-gutter));
  margin-right: calc(-1 * var(--mobile-fullbleed-gutter));
  border-left: 0;
  border-right: 0;
  border-radius: 0;
}
/* And the list crosses the card's own 1.125rem padding as well, so a row runs
   the whole width of the screen; the rows put a 1rem inset back themselves
   (padding above) and the header cells below do the same. Together with the
   card rule this takes a row from 320.2px to 390px at 390 wide. */
html.mobile .all-jobs-card > .table-wrap,
html.mobile .all-jobs-card > .jobs-table-head {
  --mobile-fullbleed-gutter: 1.125rem;
  margin-left: calc(-1 * var(--mobile-fullbleed-gutter));
  margin-right: calc(-1 * var(--mobile-fullbleed-gutter));
}
/* The wrap is a block and fills the widened box on its own; a table resolves
   width: 100% against the card's content box instead, so the header needs the
   two gutters added back explicitly or it stops short of the rows. */
html.mobile .all-jobs-card > .jobs-table-head {
  width: calc(100% + 2 * var(--mobile-fullbleed-gutter));
}
/* Line the labels up with the stacked row underneath. 4rem = the row's 1rem
   inset + its 2.5rem number column + the 0.5rem gap, so the Name label lands
   exactly on the job names, and Status sits over the right-aligned pill. (The
   row's number column is a fixed width rather than auto because each row is
   its own grid: an auto column would step right on a longer job number. Change
   the two together or the header stops lining up with the rows.) */
html.mobile .jobs-table-head .job-col-number {
  width: 4rem;
  padding-left: 1rem;
  padding-right: 0;
}
html.mobile .jobs-table-head .job-col-name { padding-left: 0; }
html.mobile .jobs-table-head .job-col-status {
  text-align: right;
  padding-right: 1rem;
}
html.mobile .jobs-table-head .job-col-scroll-spacer { display: none; }
/* A touch device has no scrollbar to reserve room for, and the gutter took
   10px out of every row. syncJobsTableScrollSpacer() measures the gutter, so
   the header's spacer column follows this to 0 on its own. */
html.mobile .all-jobs-card > .table-wrap { scrollbar-gutter: auto; }

/* Drill-in preview: hidden until a job is selected, then the whole screen
   above the bottom tab bar. Open state = the class `jobs-preview-open` on
   .jobs-page-layout (added by selectJob, removed by the Back button, the
   phone's Back gesture and on navigation - see app.js). The pinned bar that
   carries Back is the card's first child, .job-preview-bar; it is a phone
   control only, so on a desktop it is display: none, as the Back button
   always was. */
html.mobile .job-preview-card { display: none; }
.job-preview-bar { display: none; }

/* THE OPEN JOB ON A PHONE: THE WHOLE SCREEN, BACK PINNED TO THE TOP (branch
   ux-job-card-full-phone; Josh, 2026-11-01: "When a job is selected on mobile,
   the job card should be full screen, not in a panel, and the back button needs
   to be anchored to the top." He kept the bottom tab bar: Apple's drill-in, as
   in Mail and Settings). Phone layout only; desktop and tablets without
   html.mobile are untouched. ux-job-card-full-phone-frontend.mjs holds it and
   scripts/job-card-full-mutations.sh proves each check can go red.

   1. Full screen. The job replaces the list and runs edge to edge, from the
      status bar to the tab bar: the page drops its side and bottom padding, and
      the card its margin, padding, border, corner radius and shadow, so nothing
      reads as a panel. One surface, the card's own --sheet-bg, from the bar to
      the tab bar; the text inside keeps a 1rem inset from the scroller's padding.
   2. The app's own header (the organisation and the alerts bell) gives its
      place to the job's bar while a job is open, and comes back with the list.
      Its visible row goes, not the header: the header also holds the page's
      heading for a screen reader, the live region that names the page, and the
      page notice, which still has to be seen while a job is open (it hangs
      under the job's bar instead, by the bar's height). A :has() on .app-main,
      so it follows the class with no script, and only while the Plan page is
      the one showing. The header has no bottom margin on any phone page (NO
      BOTTOM MARGIN, ON EVERY PAGE), so only its padding goes here.
   3. The bar: Back on the left, a full touch target, and the job's number and
      name beside it on one line, cut with an ellipsis. It is exactly
      --job-phone-bar-h tall (the touch floor and a sliver), painted with the
      header's own token, var(--bg), so the status-bar band THE PLAN PAGE ON A
      PHONE (point 1) paints above the header is the bar's colour as well: in the
      installed app the bar runs up under the clock, and the light theme keeps
      its dark strip over the band as it does over the header. --safe-top is
      the shell's padding, so the bar starts below the status bar there and at
      the top of the screen in a browser tab. A hairline separates it from the
      job's surface.
   4. Pinned by construction, the Plan list's way (THE PLAN PAGE ON A PHONE,
      point 5): the page, the layout and the card fill the screen as flex
      columns, and only #jobPreviewContent scrolls. The bar is the card's first
      child, outside that box, so it stays put at rest, scrolled, with the
      keyboard up and on a job too short to scroll; nothing scrolls under it.
      Not a sticky bar in the page scroller: a sticky bar and a sticky tab row
      under it would need the row's offset to be the bar's height, kept equal by
      hand. Inside the box the job's header (title, pills, the stage buttons)
      scrolls away as before, and the tab row is the one sticky thing, at top 0
      of its own scroller, so it pins directly under the bar with no offset to
      keep. app.js (jobPreviewScroller) scrolls this box to a tab's panel or a
      flag's note, and opens it at its top.
   5. Every height. Unlike the list, this applies on a phone held sideways too:
      the bar and the tab row are about 110px together, less than the header
      and the sticky tab row pinned there before (about 124px), and the rest of
      the screen scrolls. The page's bottom edge is .app-main's tab-bar reserve
      (--mobile-tab-bar-inset), which nothing here cancels, so the last line of
      the job clears the tab bar, home indicator included. */
html.mobile .app-main:has(> #jobsPage:not([hidden]) > .jobs-page-layout.jobs-preview-open) {
  --job-phone-bar-h: calc(var(--touch-min) + 0.25rem);
}
html.mobile .app-main:has(> #jobsPage:not([hidden]) > .jobs-page-layout.jobs-preview-open) > header {
  padding: 0;
}
html.mobile .app-main:has(> #jobsPage:not([hidden]) > .jobs-page-layout.jobs-preview-open) > header > .header-top-row {
  display: none;
}
html.mobile .app-main:has(> #jobsPage:not([hidden]) > .jobs-page-layout.jobs-preview-open) > header > .page-notice {
  top: calc(100% + var(--job-phone-bar-h) + 0.375rem);
}
html.mobile #jobsPage:has(> .jobs-page-layout.jobs-preview-open) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
html.mobile #jobsPage > .jobs-page-layout.jobs-preview-open {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  overflow: hidden;
}
html.mobile .jobs-preview-open .all-jobs-card { display: none; }
html.mobile .jobs-preview-open .job-preview-card {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
  background: var(--sheet-bg);
}
html.mobile .jobs-preview-open .job-preview-bar {
  display: flex;
  align-items: center;
  flex: none;
  gap: 0.25rem;
  height: var(--job-phone-bar-h);
  padding: 0 1rem 0 0.25rem;
  background: var(--bg);
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
}
/* Back: the text-style control, a full touch target on the bar's left. Its own
   0.75rem padding and the bar's 0.25rem put the chevron 1rem in, on the line
   the job's text starts on. */
html.mobile .jobs-preview-open .job-preview-back-button {
  display: inline-flex;
  align-items: center;
  flex: none;
  gap: 0.25rem;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  margin: 0;
  padding: 0 0.75rem;
  border: 0;
  background: transparent;
  color: var(--green);
  font-size: 0.9375rem;
  font-weight: 700;
  cursor: pointer;
}
/* Its keyboard ring is drawn inset. Back sits 1.75px from the card's top
   edge and 3.5px from its left, and the card clips (overflow: hidden), so the
   ring a button draws outside itself (--keyboard-ring-width wide, standing off
   by --button-focus-offset) lost its top and part of its left side in both
   themes. Inset by its own width, it lies wholly inside the button: the
   stylesheet's idiom for a ring next to a clipping edge (button:focus,
   further down). Back paints no fill, so there is nothing for the ring to be
   kept apart from. */
html.mobile .jobs-preview-open .job-preview-back-button:focus {
  outline-offset: var(--keyboard-ring-inset);
}
html.mobile .jobs-preview-open .job-preview-bar-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 1rem;
  font-weight: 700;
}
html.mobile .jobs-preview-open #jobPreviewContent {
  flex: 1 1 auto;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  padding: 0 1rem 0.5rem;
}
/* A column that scrolls: its children keep their own height rather than
   shrinking to fit it (the tab row has an overflow, so nothing else would stop
   it being squeezed). */
html.mobile .jobs-preview-open #jobPreviewContent > * { flex-shrink: 0; }
/* The header's top gap, which the Back button's margin used to give it. */
html.mobile .jobs-preview-open #jobPreviewContent > .modal-header { padding-top: 0.875rem; }
html.mobile .jobs-preview-open .job-preview-tab-content {
  flex: none;
  min-height: auto;
  overflow-y: visible;
}
/* The tab row: sticky at the top of the card's own scroller, so directly under
   the bar; full-bleed across the scroller's 1rem padding, on the card's opaque
   surface so nothing shows through it while it is pinned. */
html.mobile .jobs-preview-open .job-preview-tabs {
  position: sticky;
  top: 0;
  z-index: 15;
  background: var(--sheet-bg);
  margin: 0 -1rem;
  padding: 0.5rem 1rem 0.625rem;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
html.mobile .jobs-preview-open .job-preview-tabs .job-preview-tab {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* Text-style Back button: a phone control, shown only in the open job's bar
   (above). */
.job-preview-back-button { display: none; }

/* The card's title gets the whole header on a phone, and the stage controls
   (Edit job beside "Submit for approval", or Approve beside Send back) take a
   row of their own under it: side by side they squeezed the name into a third
   of the width and stacked the buttons against it. */
html.mobile #jobPreviewContent > .modal-header {
  flex-wrap: wrap;
}
html.mobile #jobPreviewContent > .modal-header > div:first-child {
  flex: 1 1 100%;
}
html.mobile .job-preview-header-actions {
  flex: 1 1 100%;
  margin-left: 0;
  justify-content: flex-start;
}
/* A job with nothing to offer there (a completed one) leaves no empty row: the
   header's gap would otherwise still open a line under the pills. */
html.mobile .job-preview-header-actions:not(:has(> :is(button, .job-close-wait-note):not([hidden]))) {
  display: none;
}

/* ---- Settings ---- */
/* And the Account, Clients and Aircraft pages, made of its former sections (2026-11-02). */
/* Vertical rail becomes a horizontally scrollable chip row across the top. */
html.mobile :is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) {
  flex-direction: column;
  /* Spacing below the chip row comes from its own opaque padding (see the
     sticky notes on .settings-tabs) - a flex gap here would be a transparent
     strip content scrolls through. */
  gap: 0;
}
html.mobile .settings-tabs {
  flex-direction: row;
  flex-wrap: nowrap;
  gap: 0.375rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  /* Sticky chip row (html.mobile .side-tabs is NOT built the same way: it wraps instead of scrolling and has no negative
     top margin, so it travels before it pins). Frozen below the sticky page
     header and full-bleed:
     `top: -0.5rem` paired with `margin-top: -0.5rem` puts the pinned and the
     natural position at the same place, so the row never travels before it
     freezes, and the 0.5rem overhang is clipped by the scrollport. The page
     scrolls on mobile, not .app-main, so the header sits outside the scroller
     and needs no offset variable. Spacing below the row comes from its own
     opaque padding - a flex gap on the parent would be a transparent strip
     that content scrolls through. */
  position: sticky;
  top: -0.5rem;
  z-index: 15;
  background: var(--bg);
  width: auto;
  margin: -0.5rem -1rem 0;
  padding: 0.5rem 1rem 0.625rem;
}
html.mobile .settings-tab {
  width: auto;
  flex: 0 0 auto;
  white-space: nowrap;
}
/* Move the active-tab accent bar from the left edge to the bottom edge, which
   reads correctly on a horizontal chip. */
html.mobile .settings-tab.active::before {
  left: 0.5rem;
  right: 0.5rem;
  top: auto;
  bottom: 0;
  width: auto;
  height: 0.1875rem;
  border-radius: 0.5rem 0.5rem 0 0;
}
/* Drop the spacer's dividing line: a row of chips is read along, not down. */
html.mobile .settings-tabs-spacer { display: none; }
/* Panels full width, single column. */
html.mobile :is(#settingsPage, #accountPage, #clientsPage, #aircraftPage) .settings-layout {
  max-width: none;
  height: auto;
  grid-template-columns: minmax(0, 1fr) !important;
  /* Flatten per the mobile scroll model: the desktop panel keeps
     overflow-y:auto, and a scroll container's flex minimum is zero, so the
     fixed-height page column silently shrank the panel into a nested
     scroller on short viewports. The page is the one scroller. */
  flex: none;
  overflow-y: visible;
}
html.mobile #settingsPage .settings-layout[data-settings-panel="users"] {
  overflow: visible;
}
/* Users (personnel) master-detail: list card then detail card, stacked. */
html.mobile .personnel-layout {
  grid-template-columns: minmax(0, 1fr);
  height: auto;
}

/* ---- Per-package anchors (phone), the foot of the Settings block ----------
   The phone counterpart of the anchors in SETTINGS SHARED: a package's rules
   for html.mobile go directly below its own line, never at the end of the file. */
/* settings:P1 phone */
/* Shell and navigation: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The chip row has more chips than the screen is wide. A fade on the edge there
   is more to scroll to says so (app.js sets data-fade="start end" on the row
   from its scroll position). Two sticky pseudo-elements, one at each end of the
   flex row, pinned to the row's own edges so they stay put while the chips move
   under them: each takes no room (its negative margin cancels its width and the
   row's gap) and passes every tap through to the chip beneath. The gradient is
   the row's own background through its channels, so it vanishes into the row. */
html.mobile .settings-tabs::before,
html.mobile .settings-tabs::after {
  content: "";
  position: sticky;
  z-index: 2;
  flex: 0 0 1.75rem;
  align-self: stretch;
  pointer-events: none;
  opacity: 0;
}
html.mobile .settings-tabs::before {
  left: 0;
  margin-right: calc(-1.75rem - 0.375rem);
  background: linear-gradient(to right, var(--bg) 20%, rgba(var(--rgb-bg), 0));
}
html.mobile .settings-tabs::after {
  right: 0;
  margin-left: calc(-1.75rem - 0.375rem);
  background: linear-gradient(to left, var(--bg) 20%, rgba(var(--rgb-bg), 0));
}
html.mobile .settings-tabs[data-fade~="start"]::before { opacity: 1; }
html.mobile .settings-tabs[data-fade~="end"]::after { opacity: 1; }

/* settings:P2 phone */
/* Your account: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Change email and Change password stay reachable with a thumb: the email
   row's button drops under the address rather than squeezing it. */
html.mobile .profile-email-row { align-items: flex-start; flex-direction: column; }
html.mobile .profile-email-row .profile-fact { flex: 0 0 auto; width: 100%; }
html.mobile .pref-row > .save-status { min-width: 0; }

/* settings:P3a phone */
/* Organisation save path: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* All six NOTAM approval types without a scroll inside the list: at phone row
   heights the desktop cap hid two of them behind one, with no cue. */
html.mobile #organizationNotamApprovalDropdown .multiselect-dropdown-panel {
  max-height: none;
}

/* settings:P3b phone */
/* Organisation layout and calendar: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

html.mobile .org-layout {
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}
html.mobile .org-jump { display: none; }
html.mobile .org-sections { gap: 1.5rem; }
html.mobile .org-section-heading { font-size: 1.3125rem; }
html.mobile .organization-logo-field { flex-wrap: wrap; }
html.mobile .organization-logo-plate { width: 8rem; height: 4rem; }
html.mobile .sq-question-card .form-grid { grid-template-columns: minmax(0, 1fr); }
/* A link inside a sentence is part of the sentence: the phone's touch floor on
   every button made "Users" in the approval hint a 44px-tall box that pushed its
   two lines of text apart. */
html.mobile .org-section .settings-link,
html.mobile #calendarFeedCard .settings-link {
  display: inline;
  min-height: 0;
  line-height: inherit;
}
/* The phone's NOTAM list showed about four of its six types behind a small
   scroll with nothing to say there were more. All six, then. */
html.mobile #organizationNotamApprovalDropdown .multiselect-dropdown-panel {
  max-height: none;
}

/* settings:P4 phone */
/* Billing: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A link inside a sentence is not a button to size for a thumb: the phone's
   touch floor (html.mobile button) would give the line the height of a button
   and split the sentence into two widely spaced lines. */
html.mobile #chargeRulesLabourNote .settings-link { min-height: 0; }

/* settings:P5 phone */
/* Users: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Users on a phone: the list first, then the person in the page. `users-detail-open`
   is on the Users panel while a person is open (the Jobs drill-in, same idea). The
   chip row above stays the only sticky row; the "All users" row is not sticky. */
html.mobile .personnel-layout .personnel-detail-card { display: none; }
html.mobile .users-detail-open .personnel-layout .personnel-list-card { display: none; }
html.mobile .users-detail-open .personnel-layout .personnel-detail-card {
  display: flex;
  overflow: visible;
}
html.mobile .users-detail-open > .access-review-banner { display: none; }
.personnel-back-button { display: none; }
html.mobile .personnel-back-button {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 0.25rem;
  margin: -0.25rem 0 0.5rem -0.5rem;
  padding: 0.25rem 0.5rem;
  min-height: var(--touch-min, 2.75rem);
  border: 0;
  background: transparent;
  color: var(--green);
  font-size: 0.9375rem;
  font-weight: 700;
  cursor: pointer;
}
/* The access review is one row: its name and where it stands on the left (wrapping, so the
   people, locked and invited counts are never cut off), the button on the right. What a
   review is stays in the confirm that the button opens. */
html.mobile .access-review-banner {
  padding: 0.625rem 0.75rem;
  margin-bottom: 0.75rem;
  gap: 0.625rem;
}
html.mobile .access-review-text { flex: 1 1 0; min-width: 0; }
html.mobile .access-review-explain { display: none; }
html.mobile .access-review-status {
  overflow-wrap: anywhere;
}
/* Search and filters are thumb-sized. */
html.mobile .personnel-search { height: var(--touch-min, 2.75rem); }
html.mobile .personnel-chip { min-height: 2.25rem; padding: 0.25rem 0.75rem; }
html.mobile .personnel-role-filter > summary {
  display: flex;
  align-items: center;
  min-height: 2.25rem;
}
html.mobile .personnel-row { min-height: var(--touch-min, 2.75rem); }
html.mobile .personnel-fact { grid-template-columns: 4.5rem minmax(0, 1fr); }
html.mobile .personnel-rate-grid { grid-template-columns: minmax(0, 1fr); }

/* settings:P6 phone */
/* Compliance: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* settings:P7 phone */
/* Risk assessment and Checklists: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The risk matrix on a phone: all five severity columns, never a sideways
   scroll that hides the two highest. The likelihood labels wrap in a narrow
   column, each severity name is turned on its side over its column, and each
   cell shows its score and one letter (G, M or N; the cell's name carries the
   word). Written for a 390px screen and shrinks with it. */
html.mobile .risk-matrix-grid { width: 100%; }
html.mobile .risk-matrix-grid thead th:first-child,
html.mobile .risk-matrix-grid tbody th {
  width: 4.25rem;
  padding: 0.25rem 0.375rem;
  white-space: normal;
  font-size: 0.6875rem;
  line-height: 1.2;
}
html.mobile .risk-matrix-corner { font-size: 0.5625rem !important; white-space: normal; }
html.mobile .risk-matrix-grid thead th:not(:first-child) {
  padding: 0.375rem 0.125rem;
  text-align: center;
  vertical-align: bottom;
}
html.mobile .risk-matrix-axis-name {
  display: inline-block;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-size: 0.6875rem;
}
html.mobile .risk-matrix-cell { padding: 0.125rem !important; }
html.mobile .risk-matrix-swatch { height: 2.75rem; padding: 0.1875rem 0.125rem; border-radius: 0.375rem; }
html.mobile .risk-matrix-cell-grade { display: none; }
html.mobile .risk-matrix-cell-letter { display: block; }
html.mobile .risk-matrix-modal-card .table-wrap { overflow-x: visible; }
/* A list-editor row on a phone: the field takes the line, the buttons that
   follow stay at a touch-sized width. */
html.mobile .list-editor-move,
html.mobile .list-editor-del { width: var(--touch-min); min-height: var(--touch-min); }

/* settings:P8 phone */
/* Clients and Aircraft: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Clients and Aircraft on the phone (settings design P8). A row is its name and
   meta line across the full width with the pills on a line of their own below:
   three pills beside a serial number did not fit, and a defect summary of any
   length pushed "Grounds aircraft" and its status out of the row where they
   could not be seen - the safety fact was the one that disappeared. */
html.mobile #aircraftFleetList .queue-item,
html.mobile #aircraftRetiredList .queue-item,
html.mobile #aircraftDefectList .queue-item,
html.mobile #aircraftMaintenanceList .queue-item,
html.mobile #clientsList .queue-item,
html.mobile #clientsInactiveList .queue-item {
  grid-template-columns: minmax(0, 1fr);
  row-gap: 0.5rem;
}
html.mobile #aircraftFleetList .queue-item:not(.rec-row) > div:last-child:not(:first-child),
html.mobile #aircraftRetiredList .queue-item:not(.rec-row) > div:last-child:not(:first-child),
html.mobile #aircraftDefectList .queue-item > div:last-child:not(:first-child),
html.mobile #aircraftMaintenanceList .queue-item > div:last-child:not(:first-child),
html.mobile #clientsInactiveList .queue-item:not(.rec-row) > div:last-child:not(:first-child) {
  justify-content: flex-start;
}
html.mobile #aircraftFleetList .queue-name,
html.mobile #aircraftRetiredList .queue-name,
html.mobile #aircraftDefectList .queue-name,
html.mobile #aircraftMaintenanceList .queue-name,
html.mobile #clientsList .queue-name,
html.mobile #clientsInactiveList .queue-name {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* The list view on the phone: no heading row. A row is the name with its badges
   to the right, and the facts on a line below, run together with a dot. */
html.mobile .records-colhead { display: none; }
html.mobile :is(#clientsPage, #aircraftPage) .queue-item.rec-row {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name pills" "cells cells";
  column-gap: 0.5rem;
  row-gap: 0.1875rem;
  padding: 0.625rem 0.25rem;
}
html.mobile :is(#clientsPage, #aircraftPage) .rec-name {
  grid-area: name;
  white-space: normal;
  overflow-wrap: anywhere;
}
html.mobile :is(#clientsPage, #aircraftPage) .queue-item.rec-row > .rec-pills {
  grid-area: pills;
  justify-content: flex-end;
  max-width: 11rem;
}
html.mobile :is(#clientsPage, #aircraftPage) .rec-cells {
  grid-area: cells;
  display: flex;
  flex-wrap: wrap;
  column-gap: 0.375rem;
  min-width: 0;
}
html.mobile :is(#clientsPage, #aircraftPage) .rec-cell {
  max-width: 100%;
}
html.mobile :is(#clientsPage, #aircraftPage) .rec-cell:not(:last-child)::after {
  content: "\00b7";
  margin-left: 0.375rem;
}

/* settings:P9 phone */
/* Support and feedback: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The Feedback inbox on a phone. Five columns scrolled sideways put the request
   itself, the thing being read, off the right-hand edge, so each request is a
   block instead: who and when on one line, the organisation, the text with its
   tag, then the status picker. The header row stays for screen readers but is
   not drawn. */
html.mobile [data-platform-subpanel="feature-requests"] .table-wrap .data-table {
  display: block;
  min-width: 0;
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table thead {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table tbody {
  display: block;
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table tbody tr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0.25rem 0.75rem;
  padding: 0.75rem 0;
  border-top: 0.0625rem solid var(--line);
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table tbody tr:first-child {
  border-top: 0;
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table td {
  display: block;
  min-width: 0;
  padding: 0;
  border: 0;
  white-space: normal;
}
/* Date and sender share the first line, the organisation takes the second. The
   "Loading" and "Couldn't load" row is one cell and spans the lot. */
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(1),
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(2),
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(3) {
  color: var(--muted);
  font-size: 0.8125rem;
}
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(2) { text-align: right; }
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(3),
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(4),
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr:not(.list-state) > td:nth-child(5),
html.mobile [data-platform-subpanel="feature-requests"] .data-table tr.list-state > td {
  grid-column: 1 / -1;
}
html.mobile [data-platform-subpanel="feature-requests"] .feature-request-cell {
  max-width: none;
  margin-top: 0.25rem;
}
/* The td rule above (white-space: normal) outranks the desktop fix, which would
   collapse a report's line breaks here. Keep them. */
html.mobile [data-platform-subpanel="feature-requests"] .data-table td.feature-request-cell {
  white-space: pre-wrap;
}
html.mobile [data-platform-subpanel="feature-requests"] .fr-status-cell {
  margin-top: 0.25rem;
}

/* ==========================================================================
   UX SHARED, phone
   The html.mobile half of the UX SHARED block (see it): a package's rules for the
   phone go directly below its own anchor line, never at the end of the file.
   ========================================================================== */

/* The "..." on a row is always there on a phone, with no hover to reveal it, and
   at the 44 px floor a thumb needs (the glyph stays small, the hit area is not). */
html.mobile .row-menu-button {
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  opacity: 1;
}

/* ---- Per-package anchors (phone), the foot of the UX block ----------------- */
/* ux:U0b phone */
/* Page notice and disabled style: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The page notice on a phone: directly under the sticky header, inside the 16px
   gutters (the header spans the screen, so left and right are the gutter; app.js
   narrows the right one only where a map's zoom column starts right under the
   header), and never down by the tab bar where a sheet keeps its Save. Dismiss is
   a full touch target, with negative margins so the box does not grow around it. */
html.mobile .page-notice {
  top: calc(100% + 0.375rem);
  left: var(--page-notice-left, 1rem);
  right: var(--page-notice-right, 1rem);
}
html.mobile .page-notice[data-show] {
  align-items: center;
  width: auto;
  max-width: none;
  margin: 0;
  padding-block: 0.5rem;
}
html.mobile .page-notice-dismiss {
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  margin: -0.5rem -0.5rem -0.5rem 0;
}
/* The words take no taps on a phone: the box sits over the pinned Status and
   Client filters on Plan (and other pages' first row), and a tap on them was lost
   for the four seconds a "Draft saved" stayed up. Dismiss still takes its own. */
html.mobile .page-notice[data-show] { pointer-events: none; }
html.mobile .page-notice[data-show] .page-notice-dismiss { pointer-events: auto; }

/* ux:U1a phone */
/* Alerts and account: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The organisation block in the phone's bar opens the account popover: the touch floor
   for a thumb (it was 27 px tall). */
html.mobile .header-org { min-height: var(--touch-min); align-items: center; }
/* The page the person is on, behind More: the same marks as the sidebar's current link. */
html.mobile .mobile-more-item.is-current {
  color: var(--fg-strong);
  background: var(--active-item-bg);
  box-shadow: inset 0.1875rem 0 0 var(--green);
}
html.mobile .mobile-more-item.is-current .nav-icon { color: var(--green); }
/* Text links on a phone: the touch floor. */
html.mobile .alert-group-toggle,
html.mobile .app-update-bar-later { min-height: var(--touch-min); }
html.mobile .orgswitcher-account-button { min-height: var(--touch-min); }

/* ux:U1b phone */
/* Dashboard: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A name that does not fit wraps on a phone: the page is the scroller there, so a taller row costs
   nothing, and a job cut at 25 characters is a job that cannot be told from its neighbour. (A
   computer keeps one line a row, so that three rows are a known height.) */
html.mobile .dashboard-thisweek-name,
html.mobile .dashboard-thisweek-sub { white-space: normal; overflow: visible; text-overflow: clip; }

/* ux:U1c phone */
/* Phone Back: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U2 phone */
/* Signed-out screens, questionnaire portal, User guide: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE SIGN-IN PAGE'S LOOK on a phone: the panel keeps its height and its top and
   bottom padding, and gives some of its sides back, so on a 390-wide phone the
   card runs to the view's 1rem gutter and its fields keep most of the width. */
html.mobile #loginCard,
html.mobile #acceptInviteView .login-card,
html.mobile #securityLockView .login-card,
html.mobile #passwordResetView .login-card {
  padding-inline: 1.5rem;
}
/* The phone's plain code step is taller than its password step (the six boxes
   sit at the touch floor), so on a phone with the room for it (a window 700px
   tall or more) the panel is 45rem, as tall as that code step, and the card holds
   still between the two. "Trust this device" and the two-factor setup make the
   card taller still, and it grows for them (THE SIGN-IN PAGE'S LOOK, 1). Under
   700px the panel stays at the look block's 40rem, which a phone browser tab's
   600-odd px still holds without scrolling; under 600px it gives its height
   back (the max-height: 600px rule in the look block). */
@media (min-height: 700px) {
  html.mobile #loginCard { min-height: 45rem; }
}

/* ux:U3 phone */
/* Plan list: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Sort is an icon at the end of the search box's row here as on a computer (THE SORT ICON,
   ux:U3); a phone's filter cue sits at the end of the Create job row (THE PLAN PAGE ON A
   PHONE, 3, below). */
html.mobile .jobs-row-menu-hint { display: none; }
/* The list runs edge to edge on a phone and its rows keep a 1rem inset; the state row does too. */
html.mobile #jobsTableBody > tr.list-state > td { padding: 0.875rem 1rem; }
html.mobile .jobs-list-stale { padding: 0 1rem; }

/* THE PLAN PAGE ON A PHONE: COMPACT, PINNED, THE WHOLE SCREEN (branch
   ux-plan-phone-compact; Josh, 2026-10-31, from the installed iPhone app: "the
   filter boxes and create job buttons on the plan page are a little too big. The
   filters should be pinned to the top", and "still not using the whole screen").
   Phone layout only; desktop is untouched. ux-plan-phone-frontend.mjs holds it.

   1. The status bar. In the installed app the shell pads the status bar
      (--safe-top), and that band showed the shell's gradient, not the header's
      colour, so the header read as a bar floating 59pt down the screen. The band
      is now painted with the header's own background (var(--bg), the same token
      as html.mobile .app-main > header), so the header runs up under the clock.
      --safe-top is 0 in a browser tab, so there the band is 0 tall and nothing
      doubles. The light theme keeps its dark strip over the band
      (html.standalone[data-theme=light]::before): iOS draws the clock white in
      the installed app, and a light header behind it would hide it. */
html.mobile .app-shell {
  background:
    linear-gradient(var(--bg), var(--bg)) top / 100% var(--safe-top) no-repeat,
    var(--shell-bg);
}
/* 2. The dead strip under the header: the header's own 0.875rem bottom margin,
      which showed the shell's gradient between the header and the list's card.
      The header has no bottom margin on any phone page now (NO BOTTOM MARGIN,
      ON EVERY PAGE, with html.mobile .app-main > header), and the Plan list is
      not one of the pages given a top gap, so its card sits flush under the bar. */
/* 3. Compact filters: every control in the toolbar 2.5rem (35px at the 14px
      phone root; 40px when it was built at 16px), under the 45.5px floor,
      with tighter gaps. The text keeps its 16px (iOS zooms the page into a focused field
      under 16px), so only the height and the vertical padding change: the line
      box is the control less its border, which centres the text. */
html.mobile .all-jobs-card {
  --plan-phone-control-h: 2.5rem;
  /* The toolbar's control height, which the Sort icon is a square of (THE SORT ICON, ux:U3). */
  --plan-control-h: var(--plan-phone-control-h);
  padding: 0.625rem 1rem 0.25rem;
}
/* The card's side padding is 1rem here, the rows' own inset, so the filters line
   up with the job numbers; the list and its header cross 1rem of it, not 1.125rem. */
html.mobile .all-jobs-card > .table-wrap,
html.mobile .all-jobs-card > .jobs-table-head {
  --mobile-fullbleed-gutter: 1rem;
}
/* Two rows (Josh, 2026-11-01): the two pickers, then the search box with Sort, an
   icon, at its end. "Upcoming" is a choice in the Status picker now,
   not a checkbox of its own, so the third row (Sort, with the checkbox beside
   it) goes: the toolbar is two 2.5rem rows and a gap, 75px at the 14px root
   where it was 116 (measured at 390 wide). The first column is half the row less half the gap and the client picker spans
   the other two, so the pickers stay equal while the second row splits into the
   search box and the icon (auto: the icon's own square). The search box keeps
   the whole row less 2.5rem and a gap, which holds "Search job, number or
   client" whole from 320 wide up (about 252px there for the 224 it needs in SF
   Pro). */
html.mobile .jobs-table-toolbar {
  position: relative;
  grid-template-columns: calc(50% - 0.1875rem) minmax(0, 1fr) auto;
  grid-template-areas: "status client client" "search search sort";
  gap: 0.375rem;
  margin-bottom: 0;
}
html.mobile .jobs-table-toolbar :is(.jobs-search-input, .searchable-select-trigger, select) {
  height: var(--plan-phone-control-h);
  min-height: var(--plan-phone-control-h);
  padding-top: 0;
  padding-bottom: 0;
  line-height: calc(var(--plan-phone-control-h) - 0.125rem);
}
/* Sort is the icon the computer has too (THE SORT ICON, ux:U3: the Sort menu's own
   trigger, the up-and-down arrows in a square of --plan-control-h, its menu hung
   from the icon's right edge with the order in force ticked). Here it is the
   compact rule above that makes it 2.5rem tall, as it does every control in the
   toolbar, and --plan-control-h (the card, above) that makes it as wide. */
/* The filter cue ("1 filter . Clear") used to sit at the right of the Sort row,
   a row the toolbar no longer has (and the heading it sits beside on a computer is
   not shown here). It goes to the right end of the Create job row below
   (the button is centred, and that end is free: 100px at 360 wide for the cue's
   77-84; under 350 wide it is shortened, below), so setting a filter adds no height to the pinned block and moves nothing.
   Out of flow, so it is placed from the toolbar's bottom edge plus Create job's
   top margin, at the controls' height; Clear keeps that height as its target. */
html.mobile .jobs-table-toolbar .jobs-filter-cue {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  height: var(--plan-phone-control-h);
  display: flex;
  align-items: center;
  gap: 0.25rem;
}
html.mobile .jobs-table-toolbar .jobs-filter-cue .settings-link { min-height: var(--plan-phone-control-h); }
/* Below 350px wide the free end of that row is narrower than "3 filters . Clear"
   (80px at 320 for 84px, more in SF Pro), and the cue ran onto the button, so a tap
   on Create job's right end could clear the filters. There it reads "3 . Clear":
   the word stays for a screen reader (hidden the visually-hidden way, not removed).
   Measured at the 16px base; at 14px the full cue clears the button by about 14px
   at 320, so this is a spare now, kept for a reader with a larger default font. */
@media (max-width: 349px) {
  html.mobile .jobs-table-toolbar .jobs-filter-cue-word {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}
/* 4. Create job keeps its own row, centred (Josh's choice, 2026-10-31), at the
      controls' height and narrower: the label plus 1.5rem a side, 8rem at least. */
html.mobile .jobs-table-card > #addJobButton {
  margin: 0.5rem 0;
  min-width: 8rem;
  height: var(--plan-phone-control-h);
  min-height: var(--plan-phone-control-h);
  padding: 0 1.5rem;
}
/* The list's rows start straight under their header row (the general
   .table-wrap top margin left 12px of nothing between them). */
html.mobile .all-jobs-card > .table-wrap { margin-top: 0; }
html.mobile .all-jobs-card > .jobs-pagination { margin-top: 0.25rem; }
/* 5. Pinned. The page (#jobsPage) is the phone's scroller, and the filters, the
      Create job row and the column header are separate children of the card, so
      sticky would need each one's offset from the one above it, which changes
      whenever a label wraps or the filter cue appears. Instead the card fills
      the page and only the rows' box (#jobsTableWrap) scrolls: everything above
      it stays where it is by construction, at rest or scrolled, with the
      keyboard up or a list too short to scroll, and the rows are clipped at the
      box's top edge, so nothing ever shows through the filters. The wrap was
      already the list's scroller on desktop, so its infinite-scroll listener
      now works on a phone too. Only where there is height for it: a phone held
      sideways (or anything under 600px tall) scrolls the whole page as before,
      because the filters alone would leave the rows a strip; app.js
      (JOBS_PIN_QUERY, the same query) keeps the list's place when a rotation or
      a keyboard crosses that line. None of it applies while the job card is
      open (.jobs-preview-open): the open job has a construction of its own, the
      same idea at every height (THE OPEN JOB ON A PHONE, above). */
@media (min-height: 600px) {
  html.mobile #jobsPage:has(> .jobs-page-layout:not(.jobs-preview-open)) {
    display: flex;
    flex-direction: column;
    padding-bottom: 0;
  }
  html.mobile #jobsPage > .jobs-page-layout:not(.jobs-preview-open),
  html.mobile .jobs-page-layout:not(.jobs-preview-open) > .all-jobs-card {
    flex: 1 1 auto;
    min-height: 0;
  }
  html.mobile .jobs-page-layout:not(.jobs-preview-open) > .all-jobs-card > .table-wrap {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }
}

/* ux:U4a phone */
/* Plan card header and lifecycle: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U4b phone */
/* Plan card panels: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE FLAG PILLS (P-19). `html.mobile button` sets the touch floor at (0,1,2), which beat
   `button.job-flag-pill { min-height: 0 }` at (0,1,1): every pill was a 44px blob with 9px text.
   The pill is drawn at its own size again and keeps a hit area round it (a pseudo-element): 26 px
   tall on an 18 px pill, and the gaps between pills (0.65rem between rows, 0.375rem between pills in
   a row) are wider than the two areas' reach into them, so no two areas overlap. */
html.mobile button.job-flag-pill {
  min-height: 0;
  position: relative;
}
html.mobile button.job-flag-pill::before {
  content: "";
  position: absolute;
  inset: -0.3rem -0.15rem;
}
html.mobile .job-preview-flags {
  gap: 0.65rem 0.375rem;
}
/* A tab that carries a flag keeps its dot and nothing more: the fill belongs to the tab showing,
   or two tabs read as selected. */
html.mobile #jobPreviewTabs .job-preview-tab:not(.active),
html.mobile #jobPreviewTabs .job-preview-tab:not(.active):hover,
html.mobile #jobPreviewTabs .job-preview-tab:not(.active):focus {
  background: transparent;
}

/* Site details: the contact's links and the decision links are tapped, so they take the touch
   height (the floor is on `button`, and .settings-link zeroes it). */
html.mobile .sq-recipient-actions .settings-link,
html.mobile .sq-recipient-erase .settings-link,
html.mobile .sq-decided .settings-link,
html.mobile .job-billing-line .settings-link {
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
}
html.mobile .sq-recipient-erase { margin-left: 0; padding-left: 0; border-left: 0; flex: 1 1 100%; }

/* ux:SR1 phone - a Spray diary product on a phone: its boxes are touch height, the remove
   button a touch square, and the label check stacks (Aerial use, then its note). */
html.mobile .spray-product-row input { min-height: var(--touch-min); }
html.mobile .job-mix-line input { min-height: var(--touch-min); }
html.mobile .job-mix-line { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
html.mobile .spray-product-row .line-remove { min-width: var(--touch-min); }
html.mobile .spray-product-check { grid-template-columns: minmax(0, 1fr); }
html.mobile .spray-product-plan-fields { grid-template-columns: minmax(0, 1fr); }

/* ux:SR2 phone - a person on the notification record is a touch-height row, and a
   questionnaire suggestion's Add is a touch target. */
html.mobile .job-notification-row { min-height: var(--touch-min); }
html.mobile .job-notification-suggestion button { min-height: var(--touch-min); }

/* ux:SR3 phone - the two buttons are touch height and share the line; the picker's own head names
   Chance and Effect there, so the dialog's label is dropped. */
html.mobile .job-onsite-actions button { flex: 1 1 10rem; min-height: var(--touch-min); }
html.mobile .job-onsite-score-label { display: none; }
html.mobile .job-onsite-score .risk-seg-row { grid-template-columns: repeat(5, minmax(0, 1fr)); }
/* Job stages step 6: a site hazard's two scores stack, each the full width. */
html.mobile .job-site-hazard-scores { flex-direction: column; }
html.mobile .job-site-hazard-scores .risk-score-group { width: 100%; }
html.mobile .job-site-hazard-scores .risk-seg-row { grid-template-columns: repeat(5, minmax(0, 1fr)); }

/* ux:U5a phone */
/* Create and edit job wizard: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Create job on a phone: the first step is two choices, so it is a compact card like
   the app's small questions (a whole sheet for two buttons was 75% empty). The
   questionnaire step (the searchable client picker and the keyboard) keeps the full
   sheet the other dialogs use. The sheet rules are important, so these are too. */
html.mobile #jobStartOverlay {
  /* The id outranks the sheet rule above, so the top inset has to be given here
     as well: the status bar's in the installed app, 0 in a browser tab. A bare 0
     put the questionnaire step's header under the status bar. */
  padding-top: var(--safe-top);
}
html.mobile #jobStartOverlay:not(:has(#jobStartForm:not([hidden]))) {
  align-items: center;
  justify-content: center;
}
html.mobile #jobStartOverlay:not(:has(#jobStartForm:not([hidden]))) > .modal-card {
  width: min(24rem, calc(100vw - var(--safe-left) - var(--safe-right) - 2rem)) !important;
  height: auto !important;
  max-height: 80dvh !important;
  margin: 0 !important;
  border-radius: 0.5rem !important;
}

/* ux:U5b phone */
/* Risk dialog: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* Risk assessment on a phone: a card for each hazard (its name and where it
   stands, Doesn't apply, Chance and Effect as five segments, the notes behind
   Details) in place of the eight-column table, which was 993px wide in a 357px
   box with the two scores off the right edge. The header (status line and
   Close), the tab row with Next unassessed and Save stay on screen; the cards
   scroll between them. */
html.mobile .modal-overlay:not(.popover):not(.modal-confirm) > .modal-card.risk-assessment-card {
  overflow-y: auto;
}
html.mobile .risk-assessment-header { align-items: center; }

html.mobile .risk-score-guide { margin-bottom: 0.5rem; }
html.mobile .risk-guide-toggle {
  display: inline-flex;
  align-items: center;
}
html.mobile .risk-guide-toggle::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  margin-left: 0.5rem;
  border: solid currentColor;
  border-width: 0 0.125rem 0.125rem 0;
  transform: translateY(-0.1rem) rotate(45deg);
}
html.mobile .risk-guide-toggle[aria-expanded="true"]::after { transform: translateY(0.1rem) rotate(225deg); }
html.mobile .risk-score-guide[data-open="false"] .risk-score-guide-body { display: none; }
html.mobile .risk-score-guide-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Open, it leaves the rest of the sheet (header, tabs, a card and a bit, Save: about
     32rem) on the screen: a short phone gets a short guide that scrolls, so Save is
     never pushed off the bottom. A phone held sideways has its own floor. */
  max-height: max(6rem, min(45dvh, calc(100dvh - 32rem)));
  margin-top: 0.5rem;
  overflow-y: auto;
}
html.mobile .risk-score-guide .risk-guide-scale { overflow-x: visible; }
html.mobile .risk-guide-scale + .risk-guide-scale { border-left: 0; }
html.mobile .risk-score-guide-table th,
html.mobile .risk-score-guide-table td { white-space: normal; }

/* The list never shrinks below what shows a card and a bit (a phone held sideways):
   the sheet scrolls instead. The floor sits on the panel, the flex item that is
   squeezed; on the list inside it, it would overflow the panel into the Save row. */
html.mobile .modal-card.risk-assessment-card > .risk-assessment-panel { min-height: 12rem; }
/* Two selectors: the general phone rule `html.mobile .table-wrap .data-table
   { min-width: max-content }` is more specific than `.risk-checklist` alone, and
   made the list as wide as its widest card's longest line. */
html.mobile .risk-checklist,
html.mobile .table-wrap .risk-checklist,
html.mobile .risk-checklist tbody.risk-checklist-section { display: block; width: 100%; min-width: 0; }
html.mobile .risk-checklist thead { display: none; }
/* The category label sticks at the top of the list for as long as its cards are
   on screen: its row dissolves so the heading's box is the whole group. */
html.mobile .risk-checklist-category { display: contents; }
html.mobile .risk-checklist-category th {
  display: block;
  top: 0;
  padding: 0.5rem 0.25rem;
}

html.mobile .risk-checklist-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0 0 0.625rem;
  padding: 0.75rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.75rem;
  background: var(--surface-raised);
}
html.mobile .risk-checklist-row { scroll-margin-top: 2.75rem; }
html.mobile .risk-checklist-row.risk-row-unassessed {
  background: var(--surface-raised);
  box-shadow: inset 0.25rem 0 0 var(--fg-warn);
}
html.mobile .risk-checklist-row > td {
  display: block;
  width: auto;
  padding: 0;
  border: 0;
  text-align: left;
}
html.mobile .risk-checklist-row > .risk-col-index,
html.mobile .risk-checklist-row > .risk-col-risk { display: none; }

/* The hazard's name, and where it stands as a pill at the end of the line. */
html.mobile .risk-checklist-row > .risk-col-hazard {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  font-size: 0.9375rem;
  font-weight: 600;
}
html.mobile .risk-hazard-name { flex: 1 1 8rem; min-width: 0; }
html.mobile .risk-unassessed-tag,
html.mobile .risk-from-client { margin-left: 0; }
html.mobile .risk-status-pill:not([hidden]) { display: inline-flex; }
/* Dark type on all three grade colours: white on the No-go red is 3.76:1, the dark
   type is 4.8:1 (and well above that on the green and the yellow). */
html.mobile .risk-status-pill.grade-go,
html.mobile .risk-status-pill.grade-manage,
html.mobile .risk-status-pill.grade-nogo { border-color: transparent; color: var(--on-risk); }
html.mobile .risk-status-pill.grade-go { background: var(--risk-go); }
html.mobile .risk-status-pill.grade-manage { background: var(--risk-manage); }
html.mobile .risk-status-pill.grade-nogo { background: var(--risk-nogo); }

html.mobile .risk-na-label { display: inline; font-size: 0.875rem; }

/* Chance and Effect: the name and the picked word on the left, the five segments
   across the rest of the line, each at a fingertip's height. The left column is
   4rem, the width of its longest word ("Sometimes" at 0.75rem): at the 16px
   phone base the 5.25rem it was took 84px and left each segment 40px wide on a
   360px phone (45px before) and 46px at 390 (51px before). */
html.mobile .risk-score-group {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr);
  align-items: center;
  gap: 0.5rem;
}
html.mobile .risk-score-head { display: flex; flex-direction: column; gap: 0.0625rem; }
html.mobile .risk-score-name { display: block; font-size: 0.875rem; font-weight: 600; color: var(--fg-body); }
html.mobile .risk-score-readout { min-height: 0; font-size: 0.75rem; white-space: normal; }
html.mobile .risk-score-readout[data-empty="true"] { visibility: visible; }
html.mobile .risk-seg-row {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  width: 100%;
  border-radius: 0.625rem;
}
html.mobile .risk-seg { height: auto; min-height: var(--touch-min); }
html.mobile .risk-seg-face { font-size: 1rem; }

/* A row marked as not applying dims what no longer matters and keeps its name,
   its pill and its tick at full strength so the tick can be taken off. */
html.mobile .risk-checklist-row.risk-row-excluded { opacity: 1; }
html.mobile .risk-checklist-row.risk-row-excluded > .risk-col-score,
html.mobile .risk-checklist-row.risk-row-excluded > .risk-col-toggle,
html.mobile .risk-checklist-row.risk-row-excluded > .risk-col-detail { opacity: 0.45; }

/* Consequence and action, behind Details. */
html.mobile .risk-col-toggle { display: block; }
html.mobile .risk-details-toggle {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
}
html.mobile .risk-details-toggle::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  margin-left: 0.5rem;
  border: solid currentColor;
  border-width: 0 0.125rem 0.125rem 0;
  transform: translateY(-0.1rem) rotate(45deg);
}
html.mobile .risk-details-toggle[aria-expanded="true"]::after { transform: translateY(0.1rem) rotate(225deg); }
html.mobile .risk-checklist-row:not(.risk-row-details-open) > .risk-col-detail { display: none; }
html.mobile .risk-detail-label {
  display: block;
  margin-bottom: 0.25rem;
  font-size: 0.75rem;
  color: var(--muted);
}
html.mobile .risk-checklist .risk-detail-input { min-width: 0; width: 100%; }

/* The reason box takes a line of its own under the name (its 16px text is the form rule's). */
html.mobile .risk-checklist .risk-reason-input {
  flex: 1 0 100%;
  margin-top: 0;
}

/* A DECIDED CARD FOLDS TO ONE LINE (review fix, decision D2): the hazard's name, then
   its score or "Doesn't apply" and the reason, and an Edit button to open it again.
   The list shrinks as it is done instead of staying a ~9,000px scroll; an undecided
   card stays whole. */
html.mobile .risk-row-edit {
  display: inline-flex;
  align-items: center;
  flex: none;
  margin-left: auto;
  white-space: nowrap;
}
html.mobile .risk-row-edit[hidden] { display: none; }
html.mobile .risk-checklist-row.risk-row-collapsed {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.375rem;
  padding: 0.25rem 0.5rem 0.25rem 0.75rem;
}
html.mobile .risk-checklist-row.risk-row-collapsed > :not(.risk-col-hazard) { display: none; }
html.mobile .risk-checklist-row.risk-row-collapsed > .risk-col-hazard {
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}
html.mobile .risk-checklist-row.risk-row-collapsed .risk-hazard-name {
  /* The name keeps its room; a long reason is what gives way (the pill shrinks six times
     as fast), down to a few characters. */
  flex: 0 1 auto;
  min-width: 6rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
html.mobile .risk-checklist-row.risk-row-collapsed .risk-from-client,
html.mobile .risk-checklist-row.risk-row-collapsed .risk-found-on-site,
html.mobile .risk-checklist-row.risk-row-collapsed .risk-unassessed-tag,
html.mobile .risk-checklist-row.risk-row-collapsed .risk-reason-input { display: none; }
html.mobile .risk-checklist-row.risk-row-collapsed .risk-status-pill:not([hidden]) {
  display: block;
  flex: 0 6 auto;
  min-width: 5.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ux:U5c phone */
/* Area editor and checklist: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U6a phone */
/* Billing dialogs: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A line item on a phone is two rows: the description across the sheet, then
   quantity, unit price, amount and a full-size remove button (B19). The header
   row follows the same two rows, so each label sits over its box. */
html.mobile .billing-dialog-card .line-items-cols,
html.mobile .billing-dialog-card .line-item-row {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) var(--touch-min);
  grid-template-areas: "desc desc desc desc" "qty price amount remove";
  row-gap: 0.375rem;
}
html.mobile .billing-dialog-card .line-item-row {
  margin-bottom: 0.75rem;
  padding-bottom: 0.75rem;
  border-bottom: 0.0625rem solid var(--line);
}
html.mobile .billing-dialog-card .line-items-cols span:nth-child(1),
html.mobile .billing-dialog-card .line-item-row .li-desc { grid-area: desc; }
html.mobile .billing-dialog-card .line-items-cols span:nth-child(2),
html.mobile .billing-dialog-card .line-item-row .li-qty { grid-area: qty; }
html.mobile .billing-dialog-card .line-items-cols span:nth-child(3),
html.mobile .billing-dialog-card .line-item-row .li-price { grid-area: price; }
html.mobile .billing-dialog-card .line-items-cols span:nth-child(4),
html.mobile .billing-dialog-card .line-item-row .li-amount { grid-area: amount; }
html.mobile .billing-dialog-card .line-items-cols span:nth-child(5),
html.mobile .billing-dialog-card .line-item-row .line-remove { grid-area: remove; }
html.mobile .billing-dialog-card .line-item-row .line-remove {
  width: var(--touch-min);
  min-width: var(--touch-min);
}

/* ux:U6b phone */
/* Billing PDFs and tax details: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U6c phone */
/* Billing list, phone and tiles: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* THE BILLING LIST ON A PHONE. A row is a card, never a table scrolled sideways:
     INV-1005  Wellington Wind Farm                 $1,419,753.07   ...
     1 Aug 2026 · due 1 Sep                    [Sent] [In Xero]
     [45] Frost check
   New (and Export) lead the list and stay under the tab row as the page
   scrolls; the status chips are one row that scrolls sideways, with a fade on
   the edge that has more. */

html.mobile .financials-summary .stat.stat-action { min-height: 0; }

/* Xero stays one line: the name of the books gives way to an ellipsis, the
   action does not. */
html.mobile .xero-strip { align-items: center; min-height: var(--touch-min); padding-block: 0; }

/* A side-tab panel's card is the page's own surface here: no second box inside the
   page (Billing's New runs full-bleed under the tab row). The toolbar steps aside
   (display: contents) so its four parts are placed by `order` among the card's
   own children. */
html.mobile .side-panel > .card {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}
html.mobile .billing-toolbar { display: contents; }
/* Sticky under the tab row, which pins at top: -0.5rem and is padded 0.5rem
   above and 0.625rem below its touch-height tabs: its foot is
   touch-min + 0.625rem from the top of the scroller. */
html.mobile .billing-actions {
  order: 1;
  position: sticky;
  top: calc(var(--touch-min) + 0.625rem);
  z-index: 14;
  margin: 0 -1rem;
  padding: 0 1rem 0.5rem;
  background: var(--bg);
}
html.mobile .billing-actions .billing-add-button { order: -1; }
html.mobile .billing-chips {
  order: 2;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  margin: 0 -0.25rem 0.5rem;
  padding: 0 0.25rem;
  -webkit-mask-image: linear-gradient(to right, rgb(var(--rgb-overlay)) calc(100% - 1.5rem), transparent);
  mask-image: linear-gradient(to right, rgb(var(--rgb-overlay)) calc(100% - 1.5rem), transparent);
}
html.mobile .billing-chips::-webkit-scrollbar { display: none; }
html.mobile .billing-chip { flex: 0 0 auto; min-height: var(--touch-min); padding: 0.25rem 1rem; }
html.mobile .billing-toolbar .billing-search {
  order: 3;
  flex: none;
  width: 100%;
  max-width: none;
  margin: 0 0 0.5rem;
}
html.mobile .billing-count-row { order: 4; margin: 0 0 0.5rem; }
/* The count is the tab label's own number until something narrows the list. */
html.mobile .billing-count-row:not(.is-filtered) { display: none; }
html.mobile .billing-count-row .settings-link { min-height: var(--touch-min); padding-inline: 0.5rem; }
html.mobile .billing-panel > .card > .table-wrap { order: 5; }

/* The table is a list of cards. Its header row stays for a screen reader and is
   not drawn. The last column is always the width of the "..." (touch-min), so a
   row with nothing to pick from it (paid, cancelled) keeps its total and status
   in line with the rows that have one. */
html.mobile .table-wrap .financial-table,
html.mobile .financial-table tbody {
  display: block;
  min-width: 0;
  width: 100%;
}
html.mobile .financial-table thead {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
html.mobile .financial-table tbody tr.financial-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto var(--touch-min);
  grid-template-areas:
    "number client total menu"
    "when when status menu"
    "job job job job";
  align-items: center;
  gap: 0.25rem 0.625rem;
  padding: 0.625rem 0.25rem 0.625rem 0.875rem;
  margin-bottom: 0.5rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
html.mobile .financial-table tbody tr.financial-row.selected { background: rgba(var(--rgb-blue), 0.12); }
html.mobile .financial-table tbody tr.financial-row:last-child { margin-bottom: 0; }
html.mobile .financial-table td {
  display: block;
  min-width: 0;
  max-width: none;
  padding: 0;
  border: 0;
  white-space: normal;
}
html.mobile .financial-table tr.list-state { display: block; }
html.mobile .financial-table td.financial-cell-number { grid-area: number; }
html.mobile .financial-table td.financial-cell-client {
  grid-area: client;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
html.mobile .financial-table td.financial-cell-total {
  grid-area: total;
  justify-self: end;
  font-weight: 700;
}
html.mobile .financial-table td.financial-cell-issued {
  grid-area: when;
  color: var(--muted);
  font-size: 0.8125rem;
}
html.mobile .financial-due-inline { display: inline; }
html.mobile .financial-table td.financial-cell-due { display: none; }
html.mobile .financial-table td.financial-cell-status {
  grid-area: status;
  display: flex;
  justify-content: flex-end;
  gap: 0.375rem;
}
html.mobile .financial-cell-status .status-pill + .status-pill { margin-inline-start: 0; }
html.mobile .financial-table td.financial-cell-job {
  grid-area: job;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: 0.8125rem;
}
html.mobile .financial-table td.financial-cell-job:empty { display: none; }
html.mobile .financial-table td.financial-cell-menu {
  grid-area: menu;
  width: auto;
  align-self: center;
  text-align: right;
  padding: 0;
}
html.mobile .financial-open { min-height: 0; }

/* ux:U7a phone */
/* Flights map and list: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The map's "Pick a job" line is for the desktop's empty map; the phone's map
   says its own states (.mobile-flights-notice) and its Jobs button is the way
   to a job. */
html.mobile .flights-map-prompt { display: none; }

/* The needs-review pill in the phone's sticky bar (syncNeedsReviewBadgeHome),
   between the organisation and the alerts bell, in the bar's one row: a pill,
   not the touch floor's 44px slab, with its hit area still the floor's, drawn
   round it by an invisible pseudo-element (so the pill never clips: no
   overflow: hidden here). Its phone label is short ("128 to review",
   renderNeedsReviewBadge), so it never shrinks and the organisation's name,
   the one thing on screen that says whose books these are, keeps the room:
   measured at 320, 375 and 390 wide. */
html.mobile .header-top-row > #needsReviewBadge {
  position: relative;
  flex: none;
  margin: 0;
  min-height: 0;
  padding: 0.25rem 0.625rem;
  white-space: nowrap;
}
html.mobile .header-top-row > #needsReviewBadge::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: var(--touch-min);
  transform: translateY(-50%);
}

/* A bar with nothing to say but a count the strip already gives says nothing. */
html.mobile .mobile-job-bar-status:empty { display: none; }

/* Export, the last row of the layers panel: the panel's width, the touch floor, the
   panel's own padding and a hairline above it. */
html.mobile #flightsPage .map-context-bar .map-bar-button {
  width: 100%;
  min-height: var(--touch-min);
  padding: 0.375rem 0.75rem;
  text-align: left;
  border-left: 0;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  border-radius: 0 0 0.4375rem 0.4375rem;
}

/* The flights sheet leads with the flights: Show details is in the sheet's header,
   and the card's own controls row goes - unless Stop loading is in it. */
html.mobile .mobile-flights-details {
  display: flex;
  padding: 0.125rem 0 0.375rem;
}
html.mobile .mobile-drawer-body .flights-list-header .flights-details-toggle { display: none; }
html.mobile .mobile-drawer-body .flights-list-controls:not(:has(#flightsCancelLoadButton:not([hidden]))) {
  display: none;
}

/* ux:U7b phone */
/* Flights dialogs: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

html.mobile .queue-retry-button { min-height: var(--touch-min); }

/* The fold is the only way to the timeline: a row a thumb can hit (it was 13px tall). */
html.mobile .flight-detail-timeline-section > summary { line-height: var(--touch-min); }

/* ux:U8 phone */
/* Airspace: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* ux:U9 phone */
/* Hazards and Occurrences: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* The filter bar stacks: chips on a row of their own, the two menus side by
   side, then the search and the count, each at a thumb-sized height. */
html.mobile .safety-filters { gap: 0.5rem; }
html.mobile .safety-chips { flex: 1 1 100%; }
html.mobile .safety-chip { min-height: var(--touch-min); padding: 0.25rem 1rem; }
html.mobile .safety-filter-select { flex: 1 1 calc(50% - 0.25rem); }
html.mobile .safety-filters .safety-search { flex: 1 1 100%; max-width: none; }
html.mobile .safety-count-row { flex: 1 1 100%; margin-inline-start: 0; }
html.mobile .safety-count-row .settings-link {
  min-height: var(--touch-min);
  padding-inline: 0.5rem;
}

/* The occurrence list is a list of cards here, never a table scrolled sideways:
   the title and the severity on the first line, when it happened and the status
   on the second, and the CAA date as a chip when the regulator was told. The
   description, outcome, investigator and reporter are in the record. The header
   row stays for screen readers and is not drawn. */
html.mobile .table-wrap .incidents-table,
html.mobile .incidents-table tbody {
  display: block;
  min-width: 0;
  width: 100%;
}
html.mobile .incidents-table thead {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
html.mobile .incidents-table tbody tr.incident-row-clickable {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "title severity"
    "when status"
    "job job"
    "caa caa";
  align-items: center;
  gap: 0.25rem 0.75rem;
  padding: 0.75rem 0.875rem;
  margin-bottom: 0.5rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.06);
}
html.mobile .incidents-table tbody tr.incident-row-clickable:last-child { margin-bottom: 0; }
html.mobile .incidents-table td {
  display: block;
  min-width: 0;
  padding: 0;
  border: 0;
  white-space: normal;
}
html.mobile .incidents-table td.incident-cell-detail { display: none; }
/* The job it happened on stays on the phone's card, under the date (item 42). */
html.mobile .incidents-table td.incident-cell-job { grid-area: job; }
html.mobile .incidents-table td.incident-cell-job.is-empty { display: none; }
html.mobile .incidents-table td.incident-cell-job .incident-job-link,
html.mobile .incidents-table td.incident-cell-job .incident-job-text { font-size: 0.8125rem; text-align: left; }
/* The link is a small box with the touch floor's hit area around it (a card with a job is no taller
   than the card was with the button's own 46px). */
html.mobile .incidents-table td.incident-cell-job .incident-job-link {
  position: relative;
  min-height: 0;
  padding: 0;
}
html.mobile .incidents-table td.incident-cell-job .incident-job-link::after {
  content: "";
  position: absolute;
  inset: -1.05rem -0.5rem;
}
html.mobile .incidents-table td.incident-cell-title {
  grid-area: title;
  font-size: 0.9375rem;
  overflow-wrap: anywhere;
}
html.mobile .incidents-table td.incident-cell-severity { grid-area: severity; justify-self: end; }
html.mobile .incidents-table td.incident-cell-when {
  grid-area: when;
  color: var(--muted);
  font-size: 0.8125rem;
}
html.mobile .incidents-table td.incident-cell-status { grid-area: status; justify-self: end; }
html.mobile .incidents-table td.incident-cell-caa {
  grid-area: caa;
  justify-self: start;
  padding: 0.125rem 0.625rem;
  border: 0.0625rem solid var(--frame-border);
  border-radius: 62.4375rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 650;
}
html.mobile .incidents-table td.incident-cell-caa.is-empty { display: none; }
html.mobile .incident-caa-word { display: inline; }
html.mobile .incidents-table tr.list-state > td { grid-column: 1 / -1; }

/* A long hazard title wraps inside its card instead of pushing the pills (and Edit /
   View) off the right edge; the pills stay on the card's edge. */
html.mobile .hazard-head > :first-child { min-width: 0; flex: 1 1 auto; }
html.mobile .hazard-head .queue-name {
  white-space: normal;
  overflow-wrap: anywhere;
}
html.mobile .hazard-pills { align-items: center; }

/* ux:U10 phone */
/* Documents and Reporting: add rules directly below the anchor line. */
/* Three spare comment lines follow each anchor so that two packages */
/* inserting under neighbouring anchors never touch. Leave them be. */

/* A row is as tall as its name and meta need, and they wrap rather than run off the
   card: the phone's page scrolls, so there is no box to clip them. */
html.mobile #documentsPage .document-row { min-height: var(--touch-min); }
html.mobile #documentsPage .document-row .queue-name,
html.mobile #documentsPage .document-row .queue-meta { white-space: normal; overflow-wrap: anywhere; }
html.mobile .document-filterbar .searchable-select { flex: 1 1 100%; }
/* Viewer: title, then the actions on a line of their own, then the zoom row. */
html.mobile .document-viewer-card > .modal-header { flex-wrap: wrap; }
html.mobile .document-viewer-header-actions { flex: 1 1 100%; justify-content: flex-start; }
html.mobile .document-viewer-header-actions a,
html.mobile .document-viewer-header-actions button { min-width: 0; flex: 1 1 auto; }
html.mobile .document-viewer-tools button { min-width: var(--touch-min); min-height: var(--touch-min); height: var(--touch-min); }
html.mobile .document-viewer-fallback-button { min-height: var(--touch-min); }
html.mobile .document-zoom-level { min-width: 3.5rem; }
html.mobile .document-pdf-scroll { padding: 0.5rem; }

/* ---- end of UX SHARED, phone ---------------------------------------------- */

/* ==========================================================================
   Mobile remaining pages (stage 3): Logs (full-screen map + drawers), Billing,
   Reports, Hazards & Occurrences, Documents, Airspace.
   All rules keyed off `html.mobile` (no @media) - forceable on desktop with
   document.documentElement.classList.add("mobile").
   ========================================================================== */

/* ---- Shared: both full-screen maps (Logs and Airspace) ---- */
/* The card frame exists to separate a map from the page around it. Once the
   map IS the page there is no page left to separate it from, and the border,
   radius and drop shadow only read as a window drawn around the screen.
   Desktop keeps the card - there the map really does sit in a layout. */
html.mobile .map-shell {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* MapLibre's own furniture is positioned against the map box, which now runs
   under the tab bar, so lift the bottom corners clear of it - otherwise the
   scale and the attribution sit behind an opaque bar. The top corner needs
   nothing: the map starts below the sticky header, which is itself below the
   status bar (SAFE AREA, near the top of this file). */
html.mobile #flightsPage .maplibregl-ctrl-bottom-left,
html.mobile #flightsPage .maplibregl-ctrl-bottom-right,
html.mobile #mapPage .maplibregl-ctrl-bottom-left,
html.mobile #mapPage .maplibregl-ctrl-bottom-right {
  margin-bottom: var(--mobile-tab-bar-inset);
}
/* MAP BUTTONS on a phone are the floating buttons' kind (.mobile-map-fab, the
   Logs map's layers button): their opaque surface, stronger edge, shadow and
   ink, and fully round - a lone button is a circle, as the layers button is,
   and a group is a capsule. On every map, so a phone's map buttons look the
   same and are the same size wherever they are. (No map has zoom buttons on
   a phone at all: addMapNavControl in app.js.)
   Zoom and compass ship as 29px squares, and the button floor stretched them
   into 29x44 capsules rather than fixing them - the same trap the icon-square
   rule documents. They are the controls a pilot uses one-handed over a live
   map, so give them the floor on both axes. .maplibregl-ctrl-group clips to its
   own radius, so this has to be real size, not a hit box. */
html.mobile .maplibregl-ctrl.maplibregl-ctrl-group {
  --map-ctrl-r: calc(var(--touch-min) / 2);
  --map-ctrl-edge: rgba(var(--rgb-slate), 0.4);
  background: var(--map-float-bg);
  box-shadow: inset 0 0 0 0.0625rem var(--map-ctrl-edge), var(--shadow-fab);
  backdrop-filter: var(--backdrop-bar);
}
html.mobile .maplibregl-ctrl-group button {
  width: var(--touch-min);
  height: var(--touch-min);
  min-height: 0;
  color: var(--fg-fab);
}
html.mobile .maplibregl-ctrl-group button + button {
  border-top-color: rgba(var(--rgb-slate), 0.22);
}

/* ---- A map too short for a control column gets a control BAND ----
   Thumb-sized buttons make the zoom/compass/locate stack 197px tall, measured
   from the map's top corner. That is more height than a short map has to give:
   held sideways, a 390px-tall phone leaves the Logs map 269px, of which the
   bottom 119px already belong to the scale, the map credit and the tab bar, so
   the foot of the column lands squarely on both - and the credit is the one
   piece of map furniture a basemap licence will not let us move out of the way
   (LINZ asks that its credit "always visibly show", Esri that "Powered by
   Esri" is never hidden). The area editor's map is a panel inside a sheet:
   about 163px whichever way the phone is held, and the column runs into the
   credit there on a 640px-tall phone in portrait too.

   So where the height runs out, spend the width instead - the same trade the
   locate notice below already makes in landscape. The whole stack becomes one
   row along the top edge: each group lays its buttons out side by side, and
   the groups sit side by side too, so it stands 54px tall instead of 197px and
   about 187px wide. Nothing in the bottom corner can be reached from there,
   with room to spare for a landscape viewport shortened by a browser's address
   bar (which a two-row band did not have: at 334px of viewport the locate
   button still clipped the scale).

   Portrait keeps the column: a phone held upright has the height for it.

   Since 2026-11-02 no map has zoom buttons on a phone (pinch zooms;
   addMapNavControl in app.js), so the stack is the compass and the locate
   button - 111px as a column, two buttons as a band - and the area editor's
   the compass alone. The band is kept sideways all the same, and the locate
   notice below is placed against it there.

   The cut-off is the viewport, because the maps' heights follow it: the Logs
   map is the viewport less 121px of header and band, and its corner furniture
   wants 119px, so the column stops fitting below about 437px of viewport. No
   phone is under 520px tall in portrait, so that is a threshold neither
   orientation can straddle. Desktop never sees any of this - every rule here
   is html.mobile - and the editor takes the band on a phone whatever the
   viewport, its map being short in both orientations. */
@media (max-height: 520px) {
  html.mobile #flightsPage .maplibregl-ctrl-top-right,
  html.mobile #mapPage .maplibregl-ctrl-top-right {
    display: flex;
    /* Reversed so the compass (with the zoom pair, before 2026-11-02) keeps
       the corner it has always had and the locate button lands to its left,
       rather than the pair swapping places on a rotation. */
    flex-direction: row-reverse;
    align-items: flex-start;
  }
  html.mobile #flightsPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group,
  html.mobile #mapPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group {
    display: flex;
  }
}
html.mobile .job-area-draw-map .maplibregl-ctrl-top-right .maplibregl-ctrl-group {
  display: flex;
}
/* MapLibre divides stacked buttons with a top border. Side by side they want
   the line between them instead, or buttons two and three wear a stray line
   across their heads and no line between the pair. The ends of the row take
   the group's curve on their left and right corners, where the column's take
   it top and bottom (MAP BUTTONS). */
@media (max-height: 520px) {
  html.mobile #flightsPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button + button,
  html.mobile #mapPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button + button {
    border-top: 0;
    border-left: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
  }
  html.mobile #flightsPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:first-child,
  html.mobile #mapPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:first-child {
    border-radius: var(--map-ctrl-r) 0 0 var(--map-ctrl-r);
  }
  html.mobile #flightsPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:last-child,
  html.mobile #mapPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:last-child {
    border-radius: 0 var(--map-ctrl-r) var(--map-ctrl-r) 0;
  }
  html.mobile #flightsPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:only-child,
  html.mobile #mapPage .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:only-child {
    border-radius: var(--map-ctrl-r);
  }
}
html.mobile .job-area-draw-map .maplibregl-ctrl-top-right .maplibregl-ctrl-group button + button {
  border-top: 0;
  border-left: 0.0625rem solid rgba(var(--rgb-slate), 0.22);
}
html.mobile .job-area-draw-map .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:first-child {
  border-radius: var(--map-ctrl-r) 0 0 var(--map-ctrl-r);
}
html.mobile .job-area-draw-map .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:last-child {
  border-radius: 0 var(--map-ctrl-r) var(--map-ctrl-r) 0;
}
html.mobile .job-area-draw-map .maplibregl-ctrl-top-right .maplibregl-ctrl-group button:only-child {
  border-radius: var(--map-ctrl-r);
}
/* How much of the map's right-hand edge the controls occupy, for the overlays
   that share the top band with them. One button wide while they are a column;
   as a band, two buttons (compass, locate: a phone has no zoom buttons) AND
   the 0.75rem MapLibre leaves between the two groups, which is easy to forget
   and was exactly 3px of the locate button showing through the hint when it
   was. Read it, don't re-type it, or an overlay lands on the buttons the
   moment the band appears. */
html.mobile {
  --map-ctrl-w: var(--touch-min);
}
@media (max-height: 520px) {
  html.mobile {
    --map-ctrl-w: calc(2 * var(--touch-min) + 0.75rem);
  }
}
/* On a phone the notice comes OUT of the control stack and sits beside it,
   pinned to the map's own top-right corner. Stacked underneath, it was pushed
   down by four 44px buttons instead of four 29px ones, and on the Airspace map
   in landscape that lands it - and the locate button above it - underneath the
   NOTAM time bar, which owns the bottom quarter of a map only 308px tall and
   outranks MapLibre's control layer. Beside the stack it clears every piece of
   permanent furniture on both maps in both orientations: the Logs layers
   button and the Airspace fabs are far enough left, and nothing else lives
   along the top edge.
   `.maplibregl-ctrl-top-right` is itself pinned to the map's top-right corner,
   so absolute offsets here are measured from exactly that corner. It is only
   ever covered by a panel the pilot has just opened themselves (the Logs
   layers panel), and closing that brings it straight back. */
html.mobile .map-locate-notice {
  position: absolute;
  /* MapLibre's own 10px control margin is zeroed and written into these
     offsets instead, so they measure from the map corner rather than from an
     inherited margin: top lines the notice up with the zoom button beside it,
     right leaves the same 10px gutter between the two. */
  margin: 0;
  top: 0.75rem;
  right: calc(var(--map-ctrl-w) + 1.5rem);
  width: min(17rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--map-ctrl-w) - 2.125rem));
  font-size: 0.8125rem;
}
html.mobile .map-locate-notice-heading { font-size: 0.875rem; }
/* A full touch target, as every phone button has, anchored in the corner with
   the cross drawn in its top-right so it lines up with the heading instead of
   floating half way down the notice. */
html.mobile .map-locate-notice-close {
  top: 0;
  right: 0;
  width: var(--touch-min);
  height: var(--touch-min);
  min-height: var(--touch-min);
  align-items: flex-start;
  justify-content: flex-end;
  padding: 0.5rem 0.625rem 0 0;
  border-radius: 0 0.5rem 0 0;
}
html.mobile .map-locate-notice-close:hover { background: transparent; }
/* Upright on the Logs map, too, the notice keeps the layers button in the
   top-left corner clear: the map less the controls, less the button and the
   room either side of it. At the 16px phone base its
   17rem is 272px, and sized against the controls alone it reached 50px over
   the button on a 360px phone (35px at 375). The Airspace map has no button
   there and keeps the rule above. */
html.mobile #flightsPage .map-locate-notice {
  width: min(17rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--map-ctrl-w) - var(--touch-min) - 3rem));
}
/* On a short phone (an iPhone SE in Safari with its bars, 375x548) the map is
   too short for the notice to end above the Upload and Jobs buttons, so it
   stops short of their column instead, the Upload button's 5.75rem rather than
   the layers button's one: narrower and longer, it runs down beside them (a
   coarse fix at 375x548 is 185x155px, clear of both buttons and the scale; at
   the full width it covered 28px of the Upload button). */
@media (max-height: 620px) {
  html.mobile.is-portrait #flightsPage .map-locate-notice {
    width: min(17rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--map-ctrl-w) - 5.75rem - 2.875rem));
  }
}
/* Landscape has width to spare and almost no height, so the notice spends the
   width: the map less the controls on the right and the layers button on the
   left - not the whole viewport.
   Sideways at the 16px phone base the tallest notice (a coarse fix: heading,
   detail and caveat, about 100px at its 22rem width) no longer fits a short
   map. A 740x360 phone leaves it 64px above the Logs map's Upload and Jobs
   buttons and 68px above the Airspace time bar (48px with the stale-NOTAM
   line); at the old root the same phone left 101px for an 85px notice. It
   stays what it is everywhere else - read, never touched, so the map under it
   keeps its drag, and never scrolled - and on a map too short for all three
   lines the middle one, the advice to wait for a closer fix, gives way. The
   heading keeps the accuracy (amber when it is coarse) and the caveat stays.
   The heights below put the line back wherever the map has the room.
   22rem is the 26rem it was in pixels (352px; 354 at the old root). On the
   Airspace map the notice also stops short of the Layers button on the left,
   which is wider than one button: its icon and its word, 5.625rem. */
html.mobile:not(.is-portrait) .map-locate-notice {
  width: min(22rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--map-ctrl-w) - 5.625rem - 2.875rem));
}
/* On the Logs map the room beside the control band is a strip once the band
   rail takes its 17rem (148px on a 740px phone, 220px on an 812px one, and
   less again with a notch's safe areas), so the notice sits under the band
   instead, running back from the map's right edge at the full 22rem. Only
   while the controls are a band (the same max-height): a taller screen keeps
   them a column down the right edge, and the notice beside it. */
html.mobile:not(.is-portrait) #flightsPage .map-locate-notice {
  width: min(22rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--mobile-logs-rail-w) - var(--map-ctrl-w) - 5.625rem - 2.875rem));
}
@media (max-height: 520px) {
  html.mobile:not(.is-portrait) #flightsPage .map-locate-notice {
    top: calc(0.75rem + var(--touch-min) + 0.25rem);
    right: 0.625rem;
    width: min(22rem, calc(100vw - var(--safe-left) - var(--safe-right) - var(--mobile-logs-rail-w) - 1.25rem));
  }
}
/* Where the three lines fit, measured at the 16px base: on the Logs map, under
   the band and 1rem above the Upload and Jobs buttons (clear of the scale,
   which sits higher on the right), a phone 404px tall; on the Airspace map,
   above the time bar's one row, 390px (430px where the notice is narrower than
   22rem, under 708px across), and 20px more while the bar carries the
   stale-NOTAM line. Only a notice with the caveat loses its detail: an error
   that answers a tap has no caveat, and its detail is the message. */
@media (max-height: 403px) {
  html.mobile:not(.is-portrait) #flightsPage .map-locate-notice:has(> .map-locate-notice-caveat:not([hidden])) > .map-locate-notice-detail { display: none; }
}
@media (max-height: 389px), (max-width: 707px) and (max-height: 429px) {
  html.mobile:not(.is-portrait) #airspaceMapShell:has(#airspaceTimeBar:not([hidden])) .map-locate-notice:has(> .map-locate-notice-caveat:not([hidden])) > .map-locate-notice-detail { display: none; }
}
@media (max-height: 409px), (max-width: 707px) and (max-height: 449px) {
  html.mobile:not(.is-portrait) #airspaceMapShell:has(#airspaceTimeBar.is-stale:not([hidden])) .map-locate-notice:has(> .map-locate-notice-caveat:not([hidden])) > .map-locate-notice-detail { display: none; }
}
/* Where even the heading and the caveat do not fit (an Airspace map on a phone
   under 334px tall, or with the stale line under 360px; the Logs map on one
   about 331px tall), the notice is drawn over the edge of the time bar or the
   Upload and Jobs buttons rather than under them: the caveat is never the
   part that is hidden. While it is up the top-right controls outrank both (6);
   the Logs layers panel, opened on purpose, is 7 and later, so it still covers
   the notice. The notice still takes no pointer events, so a tap there reaches
   the bar or the button. */
html.mobile:not(.is-portrait) :is(#flightsPage, #mapPage) .maplibregl-ctrl-top-right:has(> .map-locate-notice:not([hidden])) { z-index: 7; }

/* ---- Logs page (full-screen map) ----
   The desktop 3-column grid (filters | flights | map) collapses to just the
   map filling the page; the two side columns are hidden and their content is
   moved into bottom-sheet drawers on demand (see the mobile Logs drawer logic
   in app.js). The desktop collapse toggles are a desktop-only affordance. */
/* The full-screen map pages don't scroll at all - the map flexes into the
   space between the header and the bottom of the screen.
   The side margin is the full-bleed idiom documented at the top of this mobile
   section: it cancels .app-main's 1rem gutter and NOTHING puts padding back,
   which is the whole point - a map wants every pixel. The bottom margin is the
   exact negative of .app-main's tab-bar reserve, so the map runs on under the
   opaque tab bar instead of stopping short of it. The map's own furniture
   (scale, zoom, the Flights button) is lifted back above the bar below, or it
   would sit behind it and stop being tappable. */
html.mobile #flightsPage {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  overflow: hidden;
  margin: 0 -1rem var(--mobile-tab-bar-outset);
  padding: 0;
}
/* Was `height: 100%`, which was right while the map was this page's only
   child. The selected-job bar sits above it while a job is picked, so the map
   takes what is left instead of a full page height that would run its bottom
   edge off-screen; with no job picked what is left is the whole page.
   .map-shell keeps `height: 100%`: a flex item's flexed height is definite, so
   the percentage still resolves - the same mechanism .layout itself relied on. */
html.mobile #flightsPage .layout {
  display: block;
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  height: auto;
  gap: 0;
}
html.mobile #flightsPage .filters-column,
html.mobile #flightsPage .flights-column {
  display: none;
}
html.mobile .flights-collapse-toggle { display: none; }
html.mobile #flightsPage .map-shell {
  display: block;
  height: 100%;
  width: 100%;
}
/* ---- The selected-job bar's row (mobile only) ----
   With no job picked the phone's Flights page is the map and nothing else
   (Josh, 2026-11-06: "Why do we have a jobs list drop down on top?" - the
   band of totals and the three latest jobs went; the Dashboard has the
   totals, the map's Jobs button has the jobs). The row is there only while a
   job is picked (app.js sets [hidden] on it otherwise), and holds the
   selected-job bar alone. It never shrinks: the map below it takes what is
   left, which map-corners-gate.mjs measures with the bar up (the map's
   shortest state). Desktop never sees it. */
.mobile-logs-band { display: none; }
html.mobile #flightsPage .mobile-logs-band {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  min-height: 0;
  padding: 0.25rem 0.75rem 0.5rem;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  background: var(--bg);
}

/* ---- The phone map's notice ----
   What the band used to say with no job picked, over the middle of the map
   and only while it holds: "Loading flights…", "Couldn't load flights" with
   Retry, "Refresh failed, 120 flights may be out of date" with Retry, "No
   flights yet" over "Tap Upload to add a log", "No flights match the filters"
   over "Tap Jobs, then Filters, to change them". Drawn like the map's loading
   badge, which is centred for the same reason: nothing else lives at the
   middle of the map. It takes no pointer, except its Retry.
   Stacking: 6, under the open Map layers panel (7) - the panel is the thing
   being used, and on a 640-667px phone it reaches down over the middle of
   the map, where a notice drawn on top hid its last rows and Retry took the
   taps meant for Export. The sheets are far above (80s). The one exception
   is the "Map didn't load" cover (10, opaque): when it goes up the notice is
   moved INTO it (showFlightsMapUnavailable, as the floating buttons are) and
   sits in its column under the cover's own two lines, or a phone whose map
   and list both failed would see "Flight list and filters still work" and no
   way to try again. */
.mobile-flights-notice { display: none; }
html.mobile #flightsPage .mobile-flights-notice:not([hidden]) {
  position: absolute;
  z-index: 6;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  width: max-content;
  max-width: calc(100% - 2 * var(--touch-min) - 2.5rem);
  padding: 0.75rem 1rem;
  border-radius: 0.75rem;
  background: var(--map-badge-bg);
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.28);
  box-shadow: var(--shadow-badge);
  color: var(--fg-bright);
  text-align: center;
  pointer-events: none;
}
html.mobile .mobile-flights-notice-text {
  font-size: 0.9375rem;
  font-weight: 650;
  line-height: 1.3;
}
html.mobile .mobile-flights-notice-hint {
  font-size: 0.8125rem;
  line-height: 1.3;
  color: var(--muted);
}
html.mobile .mobile-flights-notice-retry {
  pointer-events: auto;
  margin-top: 0.125rem;
}
html.mobile #flightsPage #mapUnavailable > .mobile-flights-notice:not([hidden]) {
  position: static;
  transform: none;
  pointer-events: auto;
}

/* ---- Jobs, the phone's way onto the map ----
   One row, drawn by one function (mobileJobRow), in the jobs sheet: the job's name, then its number, client and date, and on
   the right how many logs picking it puts on the map. The whole row is the
   button and takes the touch floor. The picked job carries a green rule down
   its leading edge and a green count - the same green as "on the map"
   everywhere else on this page. */
.mobile-job-row { display: none; }
html.mobile .mobile-job-row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: var(--touch-min);
  padding: 0.375rem 0.625rem 0.375rem 0.75rem;
  border: 0;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.12);
  border-radius: 0;
  box-shadow: inset 0.1875rem 0 0 transparent;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
html.mobile .mobile-job-row:first-child { border-top: 0; }
html.mobile .mobile-job-row:active { background: rgba(var(--rgb-slate), 0.1); }
html.mobile .mobile-job-row.is-picked {
  box-shadow: inset 0.1875rem 0 0 var(--green);
  background: rgba(var(--rgb-green), 0.08);
}
html.mobile .mobile-job-row-text {
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
  flex: 1 1 auto;
  min-width: 0;
}
html.mobile .mobile-job-row-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile .mobile-job-row-meta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile .mobile-job-row-count {
  flex: none;
  color: var(--fg-body-2);
  font-variant-numeric: tabular-nums;
}
html.mobile .mobile-job-row-count.is-empty {
  color: var(--muted);
}
html.mobile .mobile-job-row.is-picked .mobile-job-row-count { color: var(--green); }
/* The selected-job bar: once a job is picked, ONE row at the touch floor (Josh,
   2026-11-02: the second row went, about 50pt more map) - the job's name and
   client on the left, its Flights button and its clear button on the right.
   The status line (the job's count, a filter narrowing its flights, a load
   under way or failed) sits under the name and client inside the left column,
   and the row grows to fit it; the buttons never leave the row and
   never shrink (flex: none), and the name gives way with an ellipsis (two
   lines in the sideways rail, which is narrow). Picking another job is the
   map's Jobs button (there with a job picked or not), so the bar has no Change
   job of its own. */
.mobile-job-bar { display: none; }
html.mobile #flightsPage .mobile-job-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  min-height: var(--touch-min);
}
html.mobile .mobile-job-bar-title {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}
html.mobile .mobile-job-bar-name {
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile:not(.is-portrait) .mobile-job-bar-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  white-space: normal;
  overflow-wrap: anywhere;
}
html.mobile .mobile-job-bar-meta {
  font-size: 0.6875rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.mobile .mobile-job-bar-meta:empty { display: none; }
/* A sentence, not a label: it wraps inside the column rather than being cut. */
html.mobile .mobile-job-bar-status {
  margin-top: 0.125rem;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg-body-2);
  overflow-wrap: anywhere;
}
/* The clear button is the same square as a sheet's close button, and the same
   character, so it reads as "take this away" at a glance; its hit box is the
   full touch floor, which the 0.5rem gap beside Flights leaves whole (it
   reaches 0.5rem out from the square on each side, exactly the gap). */
html.mobile .mobile-job-bar-clear {
  position: relative;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.12);
  color: var(--text);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
}
html.mobile .mobile-job-bar-clear::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--touch-min);
  height: var(--touch-min);
  transform: translate(-50%, -50%);
}
html.mobile .mobile-job-bar-action {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.375rem;
  padding: 0 0.75rem;
  border-radius: 999px;
  border: 0.0625rem solid rgba(var(--rgb-green), 0.55);
  background: var(--control-bg);
  color: var(--fg-on-green-tint);
  font-size: 0.8125rem;
  font-weight: 700;
  white-space: nowrap;
}
html.mobile .mobile-job-bar-action .mobile-map-fab-icon {
  display: inline-flex;
  width: 0.9375rem;
  height: 0.9375rem;
  color: var(--green);
}
html.mobile .mobile-job-bar-action .mobile-map-fab-icon svg { width: 100%; height: 100%; }
/* Landscape: 390px of height cannot carry a row above a map and leave a map
   worth looking at, so with a job picked the bar's row becomes a side rail
   instead and the map keeps the full height. `is-portrait` is set by applyMobileClass, so this follows a
   rotation without a media query. */
html.mobile:not(.is-portrait) #flightsPage { flex-direction: row; }
html.mobile:not(.is-portrait) #flightsPage .mobile-logs-band {
  width: var(--mobile-logs-rail-w);
  max-width: 45%;
  overflow-y: auto;
  border-bottom: 0;
  border-right: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  padding-bottom: var(--mobile-tab-bar-inset);
}
/* Upload above Jobs is 95px of a map that has only 269 to give on its side,
   and what is left over the two of them is where the locate notice and the
   outside-New-Zealand hint come to rest once the controls take a wider band
   along the top. Side by side they are one button tall, and a landscape map
   has the width to spare. */
html.mobile:not(.is-portrait) #flightsPage .mobile-map-fabs {
  flex-direction: row;
}

/* Upload has its own floating button on the map now, so the sidebar's copy of
   it would be a second one hiding in the Filters sheet. Same for the Summary
   card: the phone shows no totals on this page (2026-11-06; the Dashboard has
   them).
   Neither is scoped to #flightsPage: openFlightsDrawer moves the whole
   .filters-bottom-group out of the page and into the drawer, where a
   page-scoped rule would stop matching and both would reappear. */
html.mobile #uploadLogButton,
html.mobile #summaryCard { display: none; }

/* The desktop's full-width top bar is replaced by a compact layers button
   (top-left); tapping it pops the same controls out as a small stacked panel
   anchored below the button. */
.mobile-map-layers-toggle { display: none; }
html.mobile .mobile-map-layers-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 0.625rem;
  left: 0.625rem;
  z-index: 6;
  width: var(--touch-min);
  height: var(--touch-min);
  min-height: 0;
  padding: 0;
  border-radius: 999px;
  border: 0.0625rem solid rgba(var(--rgb-slate), 0.4);
  background: var(--map-float-bg);
  color: var(--fg-fab);
  box-shadow: var(--shadow-fab);
  backdrop-filter: var(--backdrop-bar);
  cursor: pointer;
}
html.mobile .mobile-map-layers-toggle svg { width: 1.25rem; height: 1.25rem; }
html.mobile .mobile-map-layers-toggle.open {
  border-color: rgba(var(--rgb-green), 0.55);
  color: var(--green);
}
html.mobile #flightsPage .map-context-bar { display: none; }
html.mobile #flightsPage .map-context-bar.open {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  /* Hangs off the bottom of the toggle that opens it: its 0.625rem top offset,
     its own height, then a 0.25rem gap. Written out so the panel follows the
     button when the touch floor moves rather than overlapping or floating. */
  top: calc(0.625rem + var(--touch-min) + 0.25rem);
  left: 0.625rem;
  right: auto;
  transform: none;
  width: 12.5rem;
  max-width: calc(100% - 1.25rem);
  /* One column that scrolls (below), not the bar's own wrap into a second,
     clipped column, and from the top: the bar's own centring would split the
     overflow above and below, and the rows above could never be scrolled to. */
  flex-wrap: nowrap;
  justify-content: flex-start;
}
/* Open, it is the thing being used: above the Upload and Jobs buttons (z 6),
   ending at the tab bar and scrolling the rest. At the 16px phone base its six
   52px rows ran under those buttons upright and past the map's bottom edge
   sideways, where only the first two could be reached (main already lost the
   last two there). Not while the map-type list is open: that list is drawn
   inside the bar, a scrolling bar would clip it, and the bar is then z 11 (the
   rule after .map-context-bar .searchable-select-panel). */
html.mobile #flightsPage .map-context-bar.open:not(:has(.searchable-select.open)) {
  z-index: 7;
  max-height: calc(100% - 0.625rem - var(--touch-min) - 0.25rem - var(--mobile-tab-bar-inset) - 0.5rem);
  overflow-y: auto;
}
html.mobile #flightsPage .map-context-bar > .searchable-select { width: 100%; }
html.mobile #flightsPage .map-context-bar select,
html.mobile #flightsPage .map-context-bar .searchable-select-trigger {
  width: 100%;
  min-width: 0;
  border-right: 0;
  border-bottom: 0.0625rem solid rgba(var(--rgb-slate), 0.16);
  /* Stacked, so the map type is the panel's top row rather than its left
     segment: its rounded corners move with it (the bar's inner radius, as on
     desktop). */
  border-radius: 0.4375rem 0.4375rem 0 0;
}
html.mobile #flightsPage .map-context-bar label {
  padding: 0.375rem 0.75rem;
}
/* The touch-target min-height on inputs balloons each toggle row - the whole
   label is the touch target here, so keep the boxes compact. */
html.mobile #flightsPage .map-context-bar input[type="checkbox"] {
  min-height: 0;
}
/* Not wrapped in a label, so no row carries a touch target for it. The
   map-context-bar boxes above are inside labels and keep the shared size. */
html.mobile .airspace-group-master {
  width: 1.5rem;
  height: 1.5rem;
}
html.mobile #flightsPage .map-context-bar label:last-child {
  padding-bottom: 0.5rem;
}
html.mobile #flightsPage .map-context-bar label:first-of-type {
  padding-top: 0.5rem;
}

/* The Flights pill occupies the map's bottom-left corner - shift the scale
   control to the free bottom-right corner so they don't stack. The map credit
   owns that corner's bottom edge, so the scale sits above it - the line or the
   folded "i" (--map-credit-h, the credit's own height, set by
   createMapCreditControl) - with room for the "i"'s touch target. */
html.mobile #flightsPage .maplibregl-ctrl-bottom-left {
  left: auto;
  right: 0;
  margin-bottom: calc(var(--mobile-tab-bar-inset) + var(--map-credit-h, 1.75rem) + 0.5rem);
}
/* The whole credit, opened to read, runs to two or three lines; a scale
   lifted above that lands on the locate button at the foot of the zoom column
   on a 667px phone, and the column sits too low on a shorter one for any lift
   to clear it. So the scale steps aside while the credit is open, as it did
   before the credit was compact, and is back as soon as the credit closes. */
html.mobile #flightsPage .maplibregl-map:has(.map-credit.is-expanded) .maplibregl-ctrl-bottom-left {
  visibility: hidden;
}
html.mobile #flightsPage .maplibregl-ctrl-bottom-left .maplibregl-ctrl-scale {
  margin: 0 0.625rem 0.625rem 0;
}

/* Floating action buttons over the map (bottom-left, above the tab bar). The
   map now runs under the bar, so the offset carries the tab-bar reserve. */
.mobile-map-fabs { display: none; }
html.mobile .mobile-map-fabs {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  position: absolute;
  left: 0.75rem;
  bottom: calc(0.75rem + var(--mobile-tab-bar-inset));
  z-index: 6;
}
.mobile-map-fab {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5rem 0.875rem;
  border-radius: 999px;
  border: 0.0625rem solid rgba(var(--rgb-green), 0.55);
  background: var(--map-float-bg);
  color: var(--fg-fab);
  font-size: 0.8125rem;
  font-weight: 700;
  box-shadow: var(--shadow-fab);
  backdrop-filter: var(--backdrop-bar);
  cursor: pointer;
}
.mobile-map-fab .mobile-map-fab-icon {
  display: inline-flex;
  width: 1.05rem;
  height: 1.05rem;
  color: var(--green);
}
.mobile-map-fab .mobile-map-fab-icon svg { width: 100%; height: 100%; }
/* Jobs is the page's primary control on a phone, so it is the filled one - in
   the green every other floating button here is outlined in, with the ink a
   picked day in the calendar uses on it. */
.mobile-map-fab.mobile-map-fab-primary {
  border-color: var(--green);
  background: var(--green);
  color: var(--on-green);
}
.mobile-map-fab.mobile-map-fab-primary .mobile-map-fab-icon { color: var(--on-green); }

/* ---- Mobile bottom-sheet drawers (shared: Logs filters/flights) ---- */
.mobile-drawer-overlay { display: none; }
html.mobile .mobile-drawer-overlay {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 82;
}
html.mobile .mobile-drawer-overlay[hidden] { display: none; }
/* Filters opens from inside the jobs sheet as a secondary sheet, so it stacks
   above it; closing it reveals the jobs sheet again. The jobs sheet in turn
   stacks above the Flights drawer (its Jobs button opens it), and above the
   Airspace layers sheet (84), which nothing opens alongside it. */
html.mobile #mobileFiltersDrawer { z-index: 86; }
html.mobile #mobileJobsDrawer { z-index: 85; }

/* THE SHORT VIEWPORT'S TAB BAR. Under html.viewport-short (see THE INSTALLED
   IPHONE APP'S SHORT VIEWPORT near the top of this file) the band below the page
   is iOS's own, painted with <html>'s background (--bg-deep) as it was at launch.
   The tab bar takes the same colour, so the bar and the band read as one bar
   running to the bottom of the screen. After a theme change the band keeps the
   old theme's colour until the app is next opened; iOS gives no way to repaint it. */
html.viewport-short.mobile .mobile-tab-bar {
  background: var(--bg-deep);
}

/* The jobs sheet's search sits in the sheet's header, outside the scrolling
   list, so it is there however far down the list you are. */
.mobile-jobs-search {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0.5rem 0 0.25rem;
  padding: 0.5rem 0.75rem;
  border: 0.0625rem solid var(--control-border-2);
  border-radius: 0.5rem;
  background: var(--select-bg);
  color: var(--fg-control-2);
  font-size: 1rem;
  outline: none;
}
/* The app's own focus for a text field - a green edge and halo - rather than
   the browser's ring. */
.mobile-jobs-search:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 0.1875rem rgba(var(--rgb-green), 0.12);
}
.mobile-jobs-list {
  display: flex;
  flex-direction: column;
  margin: 0 -0.875rem;
}
/* The list runs edge to edge (the negative margin above), so a message in
   place of the rows carries the rows' own inset - without it "Loading your
   jobs…" and every other message here sat hard against the screen edge. */
.mobile-jobs-empty {
  margin: 0;
  padding: 1rem 0.75rem;
  color: var(--muted);
  font-size: 0.875rem;
}
.mobile-jobs-retry {
  align-self: flex-start;
  margin: 0 0.75rem 1rem;
}
/* The log list's sheet is titled after the picked job, which can be long: it
   gives way before the Jobs and close buttons beside it do. */
.mobile-drawer-title {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mobile-drawer-titlerow-actions { flex: none; }
/* On a phone the Filters sheet carries the filters and nothing else: picking
   a job is the jobs sheet's work, and a second job list here - one where a tap
   meant something different - would be two answers to one question. The
   desktop column keeps both; openFlightsDrawer moves this card back into it. */
html.mobile #mobileFiltersDrawerBody #flightJobSearch,
html.mobile #mobileFiltersDrawerBody #flightJobFilter {
  display: none;
}
/* The client filter is a searchable select, whose trigger is a button - and
   `.flight-filters button { width: auto }` shrank it to its label, a stub
   between two full-width selects. In the sheet it takes the full width like
   the controls around it. */
html.mobile #mobileFiltersDrawerBody .flight-filters .searchable-select-trigger {
  width: 100%;
}
.mobile-drawer-titlerow-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.mobile-drawer-filters-button {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.375rem 0.75rem;
  border-radius: 999px;
  border: 0.0625rem solid rgba(var(--rgb-green), 0.55);
  background: var(--control-bg);
  color: var(--fg-on-green-tint);
  font-size: 0.8125rem;
  font-weight: 700;
}
.mobile-drawer-filters-button .mobile-map-fab-icon {
  width: 0.875rem;
  height: 0.875rem;
  display: inline-flex;
  color: var(--green);
}
.mobile-drawer-filters-button .mobile-map-fab-icon svg { width: 100%; height: 100%; }
.mobile-drawer-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim-3);
}
.mobile-drawer-panel {
  position: absolute;
  left: var(--safe-left);
  right: var(--safe-right);
  bottom: 0;
  height: 75dvh;
  max-height: 75dvh;
  display: flex;
  flex-direction: column;
  background: var(--sheet-bg);
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
  border-top: 0.0625rem solid rgba(var(--rgb-slate), 0.18);
  animation: mobileMoreSlideUp 0.24s ease;
}
.mobile-drawer-header {
  flex: none;
  padding: 0.5rem 0.875rem 0.25rem;
}
.mobile-drawer-titlerow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.mobile-drawer-titlerow > strong { font-size: 1rem; }
.mobile-drawer-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  min-height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 0.5rem;
  background: rgba(var(--rgb-slate), 0.12);
  color: var(--text);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
}
.mobile-drawer-close:hover { background: rgba(var(--rgb-slate), 0.2); }
.mobile-drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0.25rem 0.875rem calc(1rem + var(--safe-bottom));
}
/* Cards moved into a drawer lose the column's flex sizing; let them size to
   their content and never clip. */
html.mobile .mobile-drawer-body .filters-card,
html.mobile .mobile-drawer-body .flights-list-card {
  flex: 0 0 auto;
  height: auto;
  min-height: 0;
}
/* The drawer chrome already names the sheet, so hide the moved card's own
   heading/subtitle - otherwise "Filters"/"Flights" appears twice. The flights
   header keeps its Select all / Clear buttons, just drops the title. */
html.mobile .mobile-drawer-body .filters-card-header,
html.mobile .mobile-drawer-body .filters-card > .card-copy,
html.mobile .mobile-drawer-body .flights-list-header > strong { display: none; }

/* Queue lists flow at natural height on mobile - the page (or the drawer body)
   owns the scroll, so drop the desktop 22rem cap and its nested scrollbar. */
html.mobile .queue-list { max-height: none; }

/* ---- Flights sheet: controls that park, and come straight back ---- */
/* The wrapper is invisible to layout everywhere else, so the desktop card is
   untouched: its header and search box remain direct flex children of the
   card. */
.flights-list-controls { display: contents; }

/* Inside the sheet it becomes a real box, stuck to the top of the drawer's
   scroller. The log list runs to hundreds of rows on a working account, and a
   phone sheet is 75dvh - so the search box and Select all / Clear get out of
   the way while you read down it. They come back on ANY upward scroll (see
   app.js), not only at the top of the list: reaching for the search box must
   never mean flicking back through 282 rows.

   Opaque on purpose. #0d1b2a is the card's own top gradient stop composited
   over the drawer panel's #0b1725, so rows pass cleanly underneath instead of
   ghosting through. */
html.mobile .mobile-drawer-body .flights-list-controls {
  display: block;
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--surface-raised);
  padding-top: 0.25rem;
  transition: transform 0.16s ease;
}
/* Parked. The extra 0.25rem is not padding: `top: 0` on a sticky element
   resolves to its scrollport's PADDING edge, and .mobile-drawer-body has
   0.25rem of top padding - so its own height alone leaves a 3.4px sliver of
   this bar's background sitting in that strip. Clear the padding too and it
   goes entirely. */
html.mobile .mobile-drawer-body.controls-parked .flights-list-controls {
  transform: translateY(calc(-100% - 0.25rem));
}
/* A shadow only once it is actually overlapping rows, so the resting state at
   the top of the list looks no different from before. */
html.mobile .mobile-drawer-body .flights-list-controls::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 0.75rem;
  background: var(--scroll-fade);
  opacity: 0;
  transition: opacity 0.16s ease;
  pointer-events: none;
}
html.mobile .mobile-drawer-body.is-scrolled .flights-list-controls::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  html.mobile .mobile-drawer-body .flights-list-controls,
  html.mobile .mobile-drawer-body .flights-list-controls::after { transition: none; }
}

/* ---- Airspace page (full-screen map) ---- */
/* Same full-bleed treatment as the Logs map - see the note there. */
html.mobile #mapPage {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  overflow: hidden;
  margin: 0 -1rem var(--mobile-tab-bar-outset);
  padding: 0;
}
html.mobile #mapPage .map-page-layout {
  display: block;
  height: 100%;
}
html.mobile #airspaceMapShell {
  height: 100%;
  width: 100%;
}
/* The floating legend card is gone on a phone; the Layers button below opens
   the same controls as a sheet instead. Expanded, the card measured 204 x 647.3
   - 54% of the map - and still scrolled internally, so the pilot could not see
   the effect of what they were toggling; and the time bar, an absolutely
   positioned sibling that won on DOM order, was painted across its lower rows,
   which meant two zone toggles (a different two at every handset size) answered
   a tap by sliding a calendar over the screen. Both faults are geometry, and a
   sheet has neither: it opens on demand over the map, closes completely, and
   can never share space with the bar. Its 30.6px minimise button - the one
   control the pilot then depended on, and the only one the app's own touch
   floor exempts - goes with it. */
html.mobile #airspaceMapLegend { display: none; }

/* The rule that hid the Airspace page's subtitle on a phone is gone with the
   subtitle itself (2026-09-27). Which dataset the map is drawing is stated
   under the Chart control that sets it, inside the layers sheet - the one place
   a pilot can act on it. */

/* Layers sheet. It stacks above the Logs drawers' band (82) but below the
   Filters sheet (86); nothing opens the two together, the number just has to
   be a decision rather than a DOM accident. */
html.mobile #mobileLayersDrawer { z-index: 84; }
/* The legend body arrives here still carrying the card's compact styling. On a
   sheet there is room, so the rows take the full touch floor - this was the
   first place in the app to do it, back when the shared floor was still the
   37.4px 2.75rem and every legend row measured that. */
html.mobile #mobileLayersDrawerBody .airspace-toggle-row,
html.mobile #mobileLayersDrawerBody .airspace-toggle-group-summary {
  min-height: var(--touch-min);
  padding: 0.25rem 0.5rem;
  border-radius: 0.5rem;
  font-size: 0.9375rem;
}
html.mobile #mobileLayersDrawerBody .airspace-toggle-row:active,
html.mobile #mobileLayersDrawerBody .airspace-toggle-group-summary:active {
  background: rgba(var(--rgb-slate), 0.14);
}
html.mobile #mobileLayersDrawerBody .airspace-toggle-group-title { font-size: 0.9375rem; }
html.mobile #mobileLayersDrawerBody .airspace-toggle-list,
html.mobile #mobileLayersDrawerBody .airspace-toggle-group-items { gap: 0.125rem; }
/* A colour swatch is how a pilot matches a row to a polygon on the map; 10.2px
   is not enough at arm's length. The plain rows reserve the same width so both
   kinds of label stay in one column. */
html.mobile #mobileLayersDrawerBody .airspace-swatch { width: 1rem; height: 1rem; }
html.mobile #mobileLayersDrawerBody .airspace-toggle-plain > span {
  margin-left: calc(1rem + 0.4375rem);
}
/* ux:k1 - the hint under the Outline switch lines up with its label in the sheet. */
html.mobile #mobileLayersDrawerBody .airspace-toggle-hint {
  margin-left: calc(0.5rem + 1.25rem + 0.4375rem + 1rem + 0.4375rem);
  font-size: 0.75rem;
}
html.mobile #mobileLayersDrawerBody .airspace-chart-field { gap: 0.375rem; }
html.mobile #mobileLayersDrawerBody .airspace-chart-field select,
html.mobile #mobileLayersDrawerBody .airspace-chart-field .searchable-select-trigger {
  min-height: var(--touch-min);
  font-size: 0.9375rem;
}
html.mobile #mobileLayersDrawerBody .airspace-notam-status { font-size: 0.75rem; }
/* Landscape leaves 390px of screen for a 19-row list, so the zone groups run
   two-up - the width is the one thing landscape has. The panel keeps the
   shared 75dvh: at 844x390 that leaves about 97px of live backdrop, which is
   how every sheet in the app is dismissed, and taking the full height bought
   nothing - 805px of content still scrolls inside a 331px window either way. */
html.mobile:not(.is-portrait) #mobileLayersDrawerBody .airspace-toggle-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 1.25rem;
  align-items: start;
}

/* The Layers button sits clear of the time bar, which is the point of the
   sheet: the two never occupy the same pixels. --airspace-timebar-h is
   published by app.js from the bar's measured height (0 while it is hidden),
   so the gap holds however the freshness line wraps. Its foot is the bar's
   own (html.mobile .airspace-timebar), which rises while the map credit is
   open, so the button rises with it and stays 0.5rem clear. */
html.mobile #airspaceMobileFabs {
  bottom: calc(max(3rem, var(--map-credit-h, 0rem) + 1.125rem) + 0.5rem + var(--airspace-timebar-h, 0px) + var(--mobile-tab-bar-inset));
}
/* Same for the chart legend, which is flush into the bottom-right corner. */
html.mobile #mapPage .airspace-legend-panel {
  bottom: var(--mobile-tab-bar-inset);
}
html.mobile #mapPage .airspace-legend-panel:not(.collapsed) {
  max-height: calc(100% - 1.5rem - var(--mobile-tab-bar-inset));
}

/* ---- Billing page ---- */
html.mobile #financialsPage .financials-layout {
  height: auto;
  overflow: visible;
}
/* Summary strip: force two per row (desktop auto-fits to more). */
html.mobile .financials-summary {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* Body stacks: horizontal tab row on top, panels below. Gap comes from the
   tab row's opaque padding, not a transparent flex gap. */
html.mobile .side-body {
  flex-direction: column;
  gap: 0;
}
html.mobile .side-tabs {
  flex-direction: row;
  flex-wrap: wrap;
  width: auto;
  gap: 0.375rem;
  /* Freezes below the sticky page header once scrolled up to it (the summary
     strip above scrolls away normally). The sealed-sticky construction is
     documented on html.mobile .settings-tabs; this row differs from it - it
     wraps rather than scrolling, and without a negative top margin it travels
     to its pinned offset instead of starting there. The phone header has no
     bottom margin (NO BOTTOM MARGIN, ON EVERY PAGE): the page's
     scrollport starts at the header's bottom edge, so the row freezes flush with
     it. With a margin there, the row froze 5px under the header with a band of
     the shell showing through (measured 2026-11-03), and a pinned offset big
     enough to close the band is clipped by the scrollport instead. */
  position: sticky;
  top: -0.5rem;
  z-index: 15;
  background: var(--bg);
  margin: 0 -1rem;
  padding: 0.5rem 1rem 0.625rem;
}
html.mobile .side-tabs .side-tab {
  flex: 1 1 28%;
  width: auto;
  text-align: center;
  min-height: var(--touch-min);
  padding: 0.5rem 0.75rem;
  font-size: 0.9375rem;
}
/* Accent bar back on the bottom edge (like the settings/reports chips). */
html.mobile .side-tabs .side-tab.active::before {
  left: 0.5rem;
  right: 0.5rem;
  top: auto;
  bottom: 0;
  width: auto;
  height: 0.1875rem;
  border-radius: 0.5rem 0.5rem 0 0;
}
/* Panels flow at natural height; tables keep their horizontal scroll.
   .side-panels must drop its desktop flex-row layout: content-sized
   (flex-basis auto) panels let a wide quotes/invoices table push the panel
   past the right edge of the screen instead of scrolling inside its
   .table-wrap. */
html.mobile .side-panels {
  display: block;
  min-width: 0;
}
html.mobile .side-panel {
  display: block;
  flex: 0 0 auto;
  width: 100%;
}
html.mobile .billing-panel > .card > .table-wrap {
  overflow-y: visible;
}

/* ---- Reports page ---- */
html.mobile #reportsPage { height: auto; }
/* The desktop preview is the page's scroller; on mobile the page itself
   scrolls, so the preview must not become a second scrollport inside it. */
html.mobile .reports-preview {
  flex: none;
  min-height: auto;
  overflow-y: visible;
}
html.mobile .reports-layout {
  height: auto;
  overflow-y: visible;
}
/* The tabbed pages' sticky chip rows (html.mobile .settings-tabs and .side-tabs)
   sit at their pinned offset from the first pixel of scroll because the phone
   header has no bottom margin on any page (NO BOTTOM MARGIN, ON EVERY PAGE, with
   html.mobile .app-main > header). Billing's summary tiles are the first thing on
   its page, so its layout carries the top gap (THE PHONE PAGE'S TOP GAP): on the
   layout, not the page, so the side-tab row still freezes flush under the bar. */
html.mobile #financialsPage .financials-layout { padding-top: 0.875rem; }

/* ---- Documents page ---- */
/* The button min-height balloons the compact row Delete button and makes the
   viewer's Close (<button>) taller than Download (<a>, which the min-height
   rule never touched) - pin both viewer actions to one equal box and keep
   Delete small. */
/* Long-press opens the row context menus (see the long-press block in
   app.js): menu items get full touch height, and the rows that own menus
   suppress the OS text-selection callout a long press would otherwise
   trigger. */
html.mobile .map-context-menu button {
  min-height: var(--touch-min);
}
html.mobile .jobs-table td,
html.mobile #quotesTableBody td,
html.mobile #invoicesTableBody td,
html.mobile #flightLayerList .queue-item {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

html.mobile .hazard-actions-head button,
html.mobile .flights-list-header-actions .secondary-button,
html.mobile .flights-list-footer .secondary-button {
  /* Row-level compact chips share the documented 2rem touch exception (§5) -
     one chip height everywhere, not 2rem here and the full floor there. They
     keep the 2rem look and take a full-size hit box instead (see Touch
     targets); Delete is destructive and Select all / Clear are used at the
     aircraft, so neither could be left at a 27px target. */
  min-height: 2rem;
}
html.mobile .document-viewer-header-actions a,
html.mobile .document-viewer-header-actions .modal-close-button {
  height: var(--touch-min);
  min-height: var(--touch-min);
}
/* A phone has no room for a side column, so Upload document and New report
   move above the library instead of beside it - a row, not the desktop's
   stacked rail, so the two buttons cost one line rather than two (Josh,
   2026-09-29: "stack sensibly", not a squeezed side column).
   THE PAGE SCROLLS, NOT THE LISTS. On a phone .app-page is the one scroll
   container (see html.mobile .app-page), and every other page flows at its
   natural height inside it; the desktop's "split the card's height between the
   two lists, each scrolls in its half" has no room to work in here. The card's
   own title, blurb and type chips take about 190px before either heading, so
   a phone in landscape (390px tall) or a 360x640 one with the chip row had
   0-50px left for two lists and the reports were clipped to nothing again
   (review, 2026-09-29) - and a portrait phone still cut the third row off
   inside a 212px box. So nothing is boxed: the layout, the card, the two
   sections and the two lists all take `flex: 1 0 auto` - grow to fill the
   screen when they are short, so the page still reaches the tab bar instead of
   leaving a gap of background under a short list, and never shrink below
   their rows, so a long one just makes the page scroll. Each `min-height`
   goes back to `auto` for the same reason, and the lists' `overflow-y` to
   `visible` (a scroll container's automatic minimum height is 0, which is the
   clipping). */
html.mobile #documentsPage .documents-page-layout {
  flex: 1 0 auto;
  min-height: auto;
}
html.mobile .documents-content {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  min-height: auto;
  margin-bottom: 0;
}
html.mobile .document-section {
  flex: 1 0 auto;
  min-height: auto;
}
html.mobile #documentsPage #documentList,
html.mobile #documentsPage #reportList {
  flex: 1 0 auto;
  min-height: auto;
  overflow-y: visible;
}
/* With the lists no longer scroll boxes, nothing clips a row that is wider than
   the card, so a row has to fit it. `.queue-name` is nowrap, and the row's text
   cell (`1fr` = minmax(auto, 1fr)) will not shrink below it, so a scanned file's
   name with no spaces in it - or a long report title - pushed the Valid / Report
   pill out past the card's edge. Let the cell shrink and the name wrap. */
html.mobile #documentList .queue-item > :first-child,
html.mobile #reportList .queue-item > :first-child { min-width: 0; }
html.mobile #documentList .queue-name,
html.mobile #reportList .queue-name {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ---- The hazard register, occurrence reporting and My actions (Documents & Reporting tabs) ---- */
html.mobile .safety-layout { height: auto; }
html.mobile .safety-panel > .card { overflow-y: visible; }
/* Card header (copy + action button) wraps instead of squashing the button. */
html.mobile .incidents-header {
  flex-wrap: wrap;
}
html.mobile .incidents-header > button { flex: 0 0 auto; }

/* Airspace data-source note under the Map-display source selector. Dormant:
   the selector and its note are commented out of index.html while we ship NZ
   AIP only (see AIRSPACE_VECTOR_SOURCE_PICKER in app.js). Kept so restoring
   that markup needs no CSS work - do not prune it as dead. */
.airspace-source-note {
  margin-top: 0.4rem;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* Platform: NZ AIP / eVNC dataset update card */
.aip-dataset-status { margin: 0.75rem 0; font-size: 0.85rem; }
.aip-status-grid { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 0.75rem; }
.aip-status-key { color: var(--fg-muted-2); }
.aip-status-val { color: var(--fg-neutral); }
.aip-progress { margin-top: 0.75rem; }
.aip-progress-msg { font-size: 0.8125rem; color: var(--fg-label); margin-bottom: 0.375rem; }
.aip-progress-bar { height: 0.5rem; background: rgba(var(--rgb-slate), 0.2); border-radius: 999px; overflow: hidden; }
.aip-progress-fill { height: 100%; background: var(--progress-fill); transition: width 0.4s ease; }
.aip-upload-form { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem; align-items: flex-start; }
/* Upload and Cancel sit side by side. Buttons are content-width by default in
   this app, so this row only has to lay them out, never stretch them. */
.aip-upload-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* What the server still holds from an interrupted upload. Deliberately a note
   and not a warning: nothing is wrong, there is just less left to send. */
.aip-resume-note {
  margin: 0.5rem 0;
  padding: 0.5rem 0.625rem;
  border: 0.0625rem solid rgba(var(--rgb-emerald), 0.35);
  border-radius: 0.375rem;
  background: rgba(var(--rgb-emerald), 0.08);
  font-size: 0.8125rem;
  color: var(--fg-success-3);
}
.aip-resume-note[hidden] { display: none; }
/* The corner widget carrying a data pack's progress off the platform panel.
   Red only when the ingest actually failed - a failure is the one state that
   waits to be clicked rather than fading away on its own. */
.aip-data-widget { font: inherit; text-align: left; width: 100%; }
.aip-data-widget-failed { border-color: rgba(var(--rgb-red), 0.55); }
/* Sits under the upload form: a quieter path to the same job, so it gets the
   secondary treatment and its own hint rather than competing with Upload. */
.aip-reingest {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid rgba(var(--rgb-overlay), 0.08);
}

.aip-reingest .form-hint {
  margin-top: 0.375rem;
}

.aip-history { margin-top: 1rem; }
.aip-history-title { display: block; font-size: 0.8125rem; margin-bottom: 0.375rem; }
.aip-history-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.8125rem; color: var(--fg-muted-2); }

/* Airspace map: eVNC chart legend */
/* Flush into the map's bottom-right corner; tabs pick a section so each is
   large enough to read; the header bar collapses/expands it. */
.airspace-legend-panel {
  position: absolute; bottom: 0; right: 0; z-index: 6;
  width: min(760px, 52vw);
  display: flex; flex-direction: column;
  background: var(--legend-panel-bg); border: 1px solid rgba(var(--rgb-slate), 0.3);
  border-right: none; border-bottom: none;
  border-radius: 0.5rem 0 0 0; overflow: hidden;
  box-shadow: var(--shadow-sheet);
}
/* Fixed height when expanded so the panel doesn't jump as you switch tabs -
   the body scrolls instead. */
/* Capped to the map box as well as the viewport: at 85vh the top of the panel
   - where the tabs are - was cut off by the map edge on a laptop-height
   window. */
.airspace-legend-panel:not(.collapsed) {
  height: min(85vh, 54rem);
  max-height: calc(100% - 1.5rem);
}
/* Collapsed: just the header bar, narrowed to its label. */
.airspace-legend-panel.collapsed { width: auto; }
.airspace-legend-panel.collapsed .airspace-legend-tabs,
.airspace-legend-panel.collapsed .airspace-legend-panel-body { display: none; }
.airspace-legend-panel-header {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  width: 100%; text-align: left;
  padding: 0.5rem 0.75rem; border: none; border-bottom: 1px solid rgba(var(--rgb-slate), 0.2);
  background: var(--legend-header-bg); color: var(--fg-neutral);
  font-size: 0.8125rem; font-weight: 600; font-family: inherit; cursor: pointer;
}
.airspace-legend-panel.collapsed .airspace-legend-panel-header { border-bottom: none; }
/* The header stays up once the panel is open (UX U8): title and a collapse
   chevron, so an expanded panel that covers most of the map always has its own
   way back. It used to be hidden here, leaving click-off as the only exit. */
.airspace-legend-caret { color: var(--fg-muted-2); font-size: 0.9rem; line-height: 1; }
.airspace-legend-tabs {
  display: flex; flex-wrap: wrap; gap: 0.25rem;
  padding: 0.4375rem 0.5rem; border-bottom: 1px solid rgba(var(--rgb-slate), 0.2);
}
.airspace-legend-tab {
  font-size: 0.6875rem; padding: 0.25rem 0.5rem; border-radius: 0.3125rem;
  border: 1px solid rgba(var(--rgb-slate), 0.3); background: var(--legend-tab-bg);
  color: var(--fg-label); cursor: pointer; white-space: nowrap;
}
.airspace-legend-tab.active { background: var(--legend-tab-active); border-color: var(--legend-tab-active); color: var(--on-solid); }
.airspace-legend-panel-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 0.5rem; }
.airspace-legend-image { display: block; width: 100%; height: auto; background: var(--paper); border-radius: 0.25rem; }


/* The ctrl-drag selection box on the Logs map. Drawn into the map's own canvas
   container so it scrolls and resizes with the map, and marked
   pointer-events:none so the drag underneath it is never interrupted by the
   rectangle the drag is drawing. */
.map-box-select {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  border: 0.0625rem solid rgba(var(--rgb-blue), 0.9);
  background: rgba(var(--rgb-blue), 0.16);
  border-radius: 0.125rem;
}




/* ----- Site questionnaire (job preview) --------------------------------- */

.sq-recipients {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
}

.sq-recipient {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.sq-recipient-who { display: flex; flex-direction: column; min-width: 12rem; }
.sq-recipient-email { font-size: 0.8125rem; color: var(--muted); }
.sq-recipient-status { font-size: 0.8125rem; color: var(--muted); flex: 1 1 auto; }
.sq-recipient-actions { display: flex; gap: 0.75rem; }

.sq-toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }

.sq-section {
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  padding: 0.875rem 1rem;
  margin-bottom: 0.75rem;
  background: var(--surface-raised);
}

.sq-section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  margin-bottom: 0.5rem;
}
.sq-section-head h3 { margin: 0; font-size: 0.9375rem; }

/* What accepting this section actually does to the job. Said on the section
 * rather than in a help note, because "accepted" has to mean something the
 * operator can rely on and only some of them write anything. */
.sq-section-effect { font-size: 0.8125rem; color: var(--muted); }

.sq-answers {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) 1fr;
  gap: 0.25rem 1rem;
  margin: 0 0 0.75rem;
}
.sq-answers dt { color: var(--muted); font-size: 0.875rem; }
.sq-answers dd { margin: 0; font-size: 0.875rem; }

.sq-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.sq-photos figure { margin: 0; }
.sq-photos img {
  display: block;
  width: 100%;
  height: 7rem;
  object-fit: cover;
  border-radius: 0.375rem;
  border: 1px solid var(--line);
}
.sq-photos figcaption { font-size: 0.8125rem; color: var(--muted); margin-top: 0.25rem; }

.sq-decision { margin: 0 0 0.5rem; font-size: 0.8125rem; color: var(--muted); }
/* The spray flight types offered once a chemical or target is taken: a row of
 * buttons that wraps, with room between them and a full-size tap on a phone. */
.sq-flight-offer { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
html.mobile .sq-flight-offer .secondary-button { min-height: var(--touch-min); }
.sq-section-actions { display: flex; align-items: center; gap: 0.75rem; }

/* Site details, answers first (2026-11-07): "Use all answers" tops the answers, and the link admin is
   one row after them ("Link · sent 3 Nov to Tom Bell · opened") that unfolds to the recipient line, its
   actions and the toolbar. Full-width row by .btn-block in the markup; no box, a rule above it. */
.sq-use-all { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin-bottom: 0.75rem; }
.sq-links { margin-top: 0.5rem; border-top: 0.0625rem solid var(--line); }
.sq-links-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 2.25rem;
  padding: 0.375rem 0;
  border-radius: 0;
  background: transparent;
  color: var(--fg-label);
  font-size: 0.8125rem;
  font-weight: 650;
  text-align: left;
}
.sq-links-toggle:hover:not(:disabled) { color: var(--fg-strong); }
.sq-links-summary { min-width: 0; overflow-wrap: anywhere; }
.sq-links-toggle::after {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 0.125rem solid currentColor;
  border-bottom: 0.125rem solid currentColor;
  transform: translateY(-0.125rem) rotate(45deg);
}
.sq-links.is-open .sq-links-toggle::after { transform: translateY(0.125rem) rotate(-135deg); }
.sq-links-body { padding-top: 0.5rem; }
html.mobile .sq-links-toggle { min-height: var(--touch-min); font-size: 0.875rem; }
html.mobile .sq-use-all button { min-height: var(--touch-min); }

html.mobile .sq-answers { grid-template-columns: 1fr; }
html.mobile .sq-answers dd { margin-bottom: 0.375rem; }

/* A risk-checklist row the client reported on their site questionnaire. Said
 * on the row because where it came from changes how much weight it carries:
 * the landowner's account of their own block, not something the operator has
 * stood on and looked at. */
.risk-from-client {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.0625rem 0.375rem;
  border-radius: 0.25rem;
  border: 1px solid var(--line);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

/* What the client's area would mean for airspace, shown beside it in the
 * review so the question is answered before the area is accepted rather
 * than at go-live. */
.sq-airspace { margin: 0.5rem 0 0.75rem; font-size: 0.875rem; color: var(--muted); }
.sq-airspace-list { margin: 0; padding-left: 1.125rem; }
.sq-airspace-list li { margin-bottom: 0.25rem; }

/* Who a site-questionnaire contact is to the block. Several people can fill
 * in one form, and this is what says whose answer to weigh on a question
 * like who owns the land. */
.sq-recipient-role { font-size: 0.8125rem; color: var(--muted); }

/* An operator's own site-questionnaire questions, edited in Settings. One
 * small labelled card per question: wording, answer type, its choices when it
 * has any, and whether it has to be answered. The fields are .form-field, so
 * they take the app's input styling (the old unlabelled rows did not). */
.sq-question-card {
  display: grid;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
  padding: 0.875rem;
  border: 0.0625rem solid var(--line);
  border-radius: 0.5rem;
  background: rgba(var(--rgb-mist), 0.05);
}
.sq-question-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.sq-question-number {
  color: var(--fg-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.sq-question-card .filter-checkbox-label { margin: 0; }
.sq-question-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

/* ----- New job: how to start (#jobStartOverlay) ------------------------------
 * Two cards side by side on a desktop, one above the other on a phone (the
 * modal is a full sheet there). Each is a title and one line under it; the
 * questionnaire one stays marked while its form is open beneath. A bare button
 * is content-width and centred, so this states the layout it wants. */
.job-start-choices {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin: 0.75rem 0 1rem;
}
.job-start-choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  width: auto;
  padding: 0.875rem 1rem;
  text-align: left;
  white-space: normal;
  background: var(--surface-raised);
  border: 0.0625rem solid var(--line);
  color: var(--text);
  font-weight: 400;
}
.job-start-choice strong { font-size: 0.9375rem; }
.job-start-choice span { font-size: 0.8125rem; line-height: 1.4; color: var(--muted); }
.job-start-choice:hover:not(:disabled) { filter: none; border-color: rgba(var(--rgb-green), 0.55); }
.job-start-choice.selected {
  background: var(--green-soft);
  border-color: rgba(var(--rgb-green), 0.55);
}
/* Where the questionnaire goes, said before Send: the address wraps rather
 * than running off a phone. */
.job-start-sends {
  margin: 0.25rem 0 0.75rem;
  font-size: 0.875rem;
  color: var(--text);
  overflow-wrap: anywhere;
}
.job-start-out { margin: 0 0 0.75rem; font-size: 0.8125rem; color: var(--muted); }
.job-start-more { margin: 0 0 0.75rem; }
.job-start-more[open] > summary { margin-bottom: 0.25rem; }
/* The client picker's list opens inside the card and is cut to the room it has:
 * a card holding only the two choices and one field left it about four rows.
 * Kept tall enough, while the form is showing, for a real list of clients. */
.job-start-card:has(#jobStartForm:not([hidden])) { min-height: min(36rem, 82vh); }
.job-start-more > summary {
  cursor: pointer;
  padding: 0.375rem 0;
  font-size: 0.875rem;
  color: var(--muted);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.job-start-more > summary::-webkit-details-marker { display: none; }
.job-start-more > summary::before {
  content: "\25B8";
  font-size: 0.85em;
  transition: transform 0.15s ease;
}
.job-start-more[open] > summary::before { transform: rotate(90deg); }

html.mobile .job-start-choices { grid-template-columns: 1fr; }

/* A button that reads as a link: a secondary action sitting beside content,
 * not a control competing with the primary one. Used by the site
 * questionnaire's per-contact and per-section actions, where the default
 * button style made "Turn this link off" as loud as anything on the page. */
.link-button {
  margin: 0;
  min-height: 0;
  padding: 0;
  background: none;
  border: 0;
  width: auto;
  font: inherit;
  font-size: 0.875rem;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}
.link-button:hover { color: var(--text); }

/* ux:FX */
/* The minor fixes sweep: rules for what the package reviews and browser passes left
   open, each beside the comment that says which finding it closes. Colours come from
   tokens only. */

/* The Cancel job card hangs its reason menu past its own edge (overflow: visible), so
   on a phone with the keyboard up, or held sideways, the card could be taller than the
   screen with nothing to scroll and Cancel job off it. The overlay scrolls instead,
   and the card is centred when there is room (auto margins) and starts at the top when
   there is not. */
html.mobile #jobCancelOverlay {
  align-items: flex-start;
  overflow-y: auto;
}
html.mobile #jobCancelOverlay > .modal-card {
  margin: auto;
  max-height: none;
}

/* The overdue note's "cancel it" is part of its sentence. The phone's touch floor
   (html.mobile button) made it a 44px box that dropped to a line of its own and
   doubled the note; the Cancel job button and the Overdue pill are the thumb-sized
   ways to the same dialog. */
html.mobile .job-section-flags .settings-link {
  display: inline;
  min-height: 0;
  line-height: inherit;
}

/* A risk score segment is a <label>, which centres its content (the global radio-label
   rule), so the picked fill, the hover wash and the focus ring covered a band about
   18px of a 44px phone segment. The face fills the segment. */
.risk-seg { align-items: stretch; }

/* The dialog card takes focus when its dead space is clicked (trackModalOpen), so Tab
   goes on from inside the dialog. Not something to ring. */
.modal-card[tabindex="-1"]:focus { outline: none; }

/* A phone held sideways has about 390px of screen: the pinned New/Export band took 51 of
   them and left a scroll window of about a card and a half. Pinned in portrait only. */
html.mobile:not(.is-portrait) .billing-actions { position: static; }

/* The pinned tab row and New/Export band (portrait) sit over the top of the page's own
   scroller: a row reached with the arrow keys scrolled to the top edge hid under them.
   A focused row stops clear of both. */
html.mobile.is-portrait #financialsPage { scroll-padding-top: calc(var(--touch-min) * 2 + 1.25rem); }

/* A form field dresses its spans as uppercase labels (.form-field span). A line a field
   says about itself - a refused Remove under the evidence list, "Couldn't load files" -
   is a sentence, not a label: it was drawn in capitals at 10px. */
.form-field .save-status,
.form-field .list-state-text {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 500;
  font-size: 0.8125rem;
}
.form-field .list-state:not(.list-state-error) .list-state-text { color: inherit; }

/* A chip row that scrolls sideways fades its last 1.5rem; with 0.25rem of padding the
   last chip ("Other", "Paid") ended inside the fade and read dimmed even scrolled to
   the end. The padding is the fade's width, so the last chip scrolls clear of it. */
html.mobile .billing-chips { padding-inline-end: 1.5rem; }

/* A long file name with no break in it (a scan's name) made the Edit dialog scroll
   sideways. */
.document-current-file { overflow-wrap: anywhere; }

/* Plan's Sort menu has five entries and, on a phone, each is touch height: the panel's
   15rem cap cut Client in half and left the order in force below the fold (the status
   filter's panel had its cap lifted for the same reason). */
.all-jobs-card .searchable-select:has(> #jobsSortSelect) .searchable-select-list {
  max-height: none;
}

/* ux:DEC2 */
/* Screen decisions Josh made on 2026-10-14 (build/DEC2). Colours come from tokens only. */

/* Location: the one line that says DroneOps reads NOTAMs and does not file them. Quiet, like the
   evidence lines above it, and clear of the map below. */
.job-notam-filing {
  margin: 0 0 0.75rem;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* The map's own buttons were MapLibre's white in both themes, and rang in a darker ring of their own.
   They are on the app's map surfaces now (MAP BUTTONS: dark in the dark
   theme, light in the light), so they take the ring every control on those surfaces does, drawn inside
   the button's face (the group clips an outside ring); at the group's ends it follows the group's
   curve. MapLibre's own blue glow for a focused map button is off. */
.maplibregl-ctrl button:focus {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: var(--keyboard-ring-inset);
}
.maplibregl-ctrl.maplibregl-ctrl-group button:focus { box-shadow: none; }

/* A text field no rule above gave a focus mark of its own: the border turns the accent. Zero
   specificity, last in the file: any field with a rule of its own keeps it. */
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="hidden"]), textarea):focus-visible {
  border-color: var(--focus-ring);
}

/* A control flush with an edge that clips (a scrolling list, a tab column, the top of a page) had its
   ring cut off on that side: the rows and tabs and tiles draw it inside instead, over their own
   neutral face, which is 5:1 or better in both themes. The Plan list's rows are in it: the list's
   only Tab stop is a row, and the list's wrap clips the left edge of every row, the top of the first
   and the bottom of the last. The Flights column's Upload logs button keeps
   its outside ring and gets the 4px of room it needs. */
.settings-tab:focus,
.side-tab:focus,
.queue-item:focus,
.mobile-job-row:focus,
.job-row:focus,
.dashboard-occurrence:focus,
.charge-rule-row:focus,
.stat-action:focus,
[role="tabpanel"]:focus,
.maplibregl-canvas:focus {
  outline-offset: var(--keyboard-ring-inset);
}
/* On a computer it ends on the Flights column's bottom edge, under the card
   above it (whose own margin is the gap): the 1.25rem above and below it was
   height the Jobs list can use (Josh, 2026-11-02). The quarter rem it keeps
   at its sides and bottom is the keyboard ring's room - the column scrolls, so
   it clips at its own edges, and the ring is 0.21875rem out from the button. */
html:not(.mobile) #uploadLogButton { margin: 0 0.25rem 0.25rem; }
/* The side-tab panels (Billing, Safety, Documents) are tab stops of their own, and each is filled edge to edge by its
   card, which paints over an inset ring and sits flush with the clip outside it. The card wears it. */
.side-panel:focus { outline: none; }
.side-panel:focus > .card {
  outline: var(--keyboard-ring-width) solid var(--keyboard-ring);
  outline-offset: var(--keyboard-ring-inset);
}
/* The phone's Documents & Reporting tab row is pinned over the top of the page, two rows of tabs
   since the hazard register and occurrence reporting joined it: a control reached by keyboard stops
   clear of it (the Billing page has the same, above). */
html.mobile #documentsPage { scroll-padding-top: calc(var(--touch-min) * 2 + 1.625rem); }
/* The phone's pinned Billing band and its sideways chip row cut an outside ring off at the top (the
   tab row above is opaque) and at top and bottom (overflow-x makes the row clip both ways): 0.25rem
   of room each, the chip row's bottom one taken back from its margin. A tab or chip focused by keyboard
   at the end of a sideways strip is scrolled clear of the fade over the strip's end. */
html.mobile .billing-actions { padding-top: 0.25rem; }
html.mobile .billing-chips { padding-block: 0.25rem; margin-block: 0 0.25rem; }
html.mobile .settings-tabs,
html.mobile .billing-chips { scroll-padding-inline: 1.5rem; }

/* ===== The bell's two lists (PWA P3, 2026-10-25) ==========================
   "For you" - every member's own alerts - and, for admins, the compliance list
   as before, chosen by a small tab row inside the panel (shown only when there
   are two lists). An unread alert of one's own is marked by a green edge and a
   "New" tag. */
.alerts-tabs {
  display: flex;
  gap: 0.25rem;
  margin: 0 0 0.625rem;
  border-bottom: 0.0625rem solid var(--line);
}
.alerts-tab {
  position: relative;
  width: auto;
  min-height: 2.25rem;
  padding: 0.5rem 0.75rem;
  border: 0;
  border-radius: 0.5rem 0.5rem 0 0;
  color: var(--fg-nav);
  background: transparent;
  font-size: 0.8125rem;
  font-weight: 700;
}
.alerts-tab:hover:not(:disabled) {
  filter: none;
  background: rgba(var(--rgb-overlay), 0.055);
  color: var(--fg-strong);
}
.alerts-tab.active { color: var(--fg-strong); }
.alerts-tab.active::before {
  content: "";
  position: absolute;
  left: 0.375rem;
  right: 0.375rem;
  bottom: -0.0625rem;
  height: 0.1875rem;
  border-radius: 0.1875rem;
  background: var(--green);
}
.alerts-tab-count {
  display: inline-block;
  min-width: 1.25rem;
  margin-left: 0.375rem;
  padding: 0 0.375rem;
  border-radius: 0.625rem;
  background: var(--badge-bg);
  color: var(--on-solid);
  font-size: 0.8125rem;
  line-height: 1.25rem;
  text-align: center;
}
.alerts-mine-actions {
  display: flex;
  flex: none;
}
.alerts-read-all { font-size: 0.8125rem; font-weight: 500; }
.my-alert-row { align-items: flex-start; }
.my-alert-row.is-unread { border-left-color: var(--green); }
.my-alert-row.is-unread .alert-title { font-weight: 700; }
.my-alert-row[aria-busy="true"] { opacity: 0.6; cursor: progress; }
/* The time and the "New" tag at 0.8125rem: 11px at the 85% root, the smallest
   text the legibility pass allows (0.75rem and 0.6875rem were 10.2px and 9.35px,
   and "New" is the one word that says a row is unread). */
.my-alert-when {
  margin-top: 0.25rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.my-alert-unread {
  flex: none;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--pill-green-fg);
}
html.mobile .alerts-tab { min-height: var(--touch-min); }

/* ==== Job stages (step 2): marks on the job card's tabs and Flight-tab blocks (the Plan
   list shows no stage: Josh, 2026-11-05). The stage comes from the server (JobResponse.stage); these
   only draw it. Each state has its own shape - a tick, a dot, an amber "!", a dash - so
   colour is never the only difference, and the words are there for a screen reader
   (.job-stage-mark-text). */
.job-stage-mark {
  display: inline-block;
  min-width: 0.75rem;
  margin-right: 0.3125rem;
  font-size: 0.75rem;
  line-height: 1;
  text-align: center;
  vertical-align: 0.0625rem;
}
[data-stage-mark="done"] > .job-stage-mark::before {
  content: "\2713";
  color: rgb(var(--rgb-green));
  font-weight: 700;
}
[data-stage-mark="current"] > .job-stage-mark::before {
  content: "";
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: rgb(var(--rgb-green));
  box-shadow: 0 0 0 0.125rem rgba(var(--rgb-green), 0.25);
}
[data-stage-mark="attention"] > .job-stage-mark::before {
  content: "!";
  display: inline-block;
  width: 0.875rem;
  height: 0.875rem;
  border-radius: 50%;
  border: 0.0625rem solid rgb(var(--rgb-amber));
  background: rgba(var(--rgb-amber), 0.2);
  color: var(--pill-amber-fg);
  font-size: 0.625rem;
  font-weight: 800;
  line-height: 0.75rem;
  box-sizing: border-box;
}
[data-stage-mark="not_needed"] > .job-stage-mark::before {
  content: "\2013";
  color: var(--muted);
}
.job-preview-tab[data-stage-mark="not_needed"]:not(.active),
.job-billing-summary > strong[data-stage-mark="not_needed"] {
  color: var(--muted);
}

/* "No one needs telling", said: the line and its Undo. */
.job-notification-none { margin-top: 0.375rem; }
.job-notification-none .settings-link { margin-left: 0.5rem; }
