/*
 * SkyPane companion service — hand-written stylesheet: no build step,
 * no framework, no web/icon fonts, no external references (no url(),
 * no @font-face, no remote asset).
 *
 * Accent is reserved to selection, primary-action and wayfinding
 * affordances, plus one brand element. The list below is exhaustive; an
 * accent consumer not named here does not belong in this file:
 *   - links (a, .text-link, summary and its disclosure marker,
 *     .special-add__summary, button.look-sheet__reset)
 *   - the focus ring (every :focus-visible outline, the focused search
 *     input and filter-chip box, the sparkline hit target, the look
 *     frame/open button) and the native accent-color of input/select
 *   - primary actions (button[type="submit"], a.calendar-manage--primary,
 *     button.look-sheet__done, button.look-sheet__save, the skip link)
 *   - current location / selection (.sidebar-link--active,
 *     .mobile-nav__link--active, .tab-bar__link--active and its pill,
 *     the tab bar's space switch cell, the active theme option,
 *     the checked runway/look cards and look dot, the editing look frame)
 *   - the current Personalisation section link
 *     (.personalisation-nav__link[aria-current])
 *   - the current segment of the space switch
 *     (.space-switch__segment--current)
 *   - the current frame's tile outline in the frame tile strip
 *     (.frame-tile[aria-current]) and the Add a screen tile's text
 *     and dashed border (.frame-tile--add)
 *   - switch-on state (.switch[aria-checked="true"])
 *   - stat tiles (the top rule and icon of .stat-tile)
 *   - the special-look add button's dashed plus
 *   - the logo mark's flick (.logo-accent)
 *   - the map's pin and area circle (.place-pin, .place-circle)
 * Border is structural only (dividers, table lines, card edges)
 * and never substitutes for accent or status colour.
 *
 * Theming: :root plus a prefers-color-scheme block hold both palettes;
 * html[data-ui-theme] overrides either explicitly. Accent stays
 * perceptually separated from every status colour in both themes
 * (companion/test_contrast_check.py).
 */

:root {
  /* Spacing scale, multiples of 4. */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;

  /* Typography scale: exactly 4 sizes, 2 weights. */
  --font-label-size: 14px;
  --font-body-size: 16px;
  --font-heading-size: 22px;
  /* The page title scales with the viewport: 30-34px on a phone, 44-48px
   * from the desktop breakpoint (see the @media block below :root). */
  --font-page-title-size: clamp(1.875rem, 1.55rem + 1.4vw, 2.125rem);
  --weight-regular: 400;
  --weight-semibold: 600;

  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* A system font stack, deliberately: the zero-external-reference rule
   * forbids a web font. Leads with Iowan Old Style/Charter (bundled on
   * Apple platforms) rather than Georgia, so Mac/iPhone render the
   * intended faces instead of always falling through to Georgia. */
  --font-serif: "Iowan Old Style", Charter, "Palatino Linotype", Georgia, "Book Antiqua", serif;

  --radius-card: 20px;
  --radius-control: 8px;

  /* Theme-independent colour tokens: every one of these held the exact
   * same value in every theme block before it was a token, so (like
   * --radius-card, --space-* and --motion-* above) it is declared once
   * here rather than repeated in the dark-mode/explicit-theme blocks
   * below — :root's cascade already reaches an element carrying
   * html[data-ui-theme], and none of those blocks override it. */
  --color-image-backdrop: #ffffff;
  --color-scrim: rgba(0, 0, 0, 0.5);
  /* A dark tint painted OVER an image preview (a replace-zone icon/hint),
   * never over the page background — always dark regardless of theme,
   * which is why it is not built from --color-text (that inverts in
   * dark mode). */
  --color-overlay-icon: rgba(23, 25, 31, 0.55);
  --color-overlay-text: rgba(23, 25, 31, 0.7);
  /* A bare <button>'s idle wash/border: the color-mix() equivalent that
   * immediately follows each use is the real, theme-aware value: these
   * are the fallback for a browser without color-mix() support, so they
   * carry the light-mode-shaped value forever, same as before this was
   * a token. */
  --color-control-wash: rgba(23, 25, 31, 0.045);
  --color-control-border-subtle: rgba(23, 25, 31, 0.09);
  --shadow-control: inset 0 1px 0 rgba(255, 255, 255, .14), 0 1px 2px rgba(18, 21, 27, .14);
  --shadow-control-hover: inset 0 1px 0 rgba(255, 255, 255, .16), 0 2px 5px rgba(18, 21, 27, .18);
  --color-shadow-strong: rgba(18, 21, 27, 0.16);
  /* The toast's two elevations: barely lifted when docked in flow, and a
   * deep drop shadow when it floats over the page. */
  --shadow-toast-docked: 0 1px 2px rgba(10, 12, 18, 0.12);
  --shadow-toast-float: 0 22px 44px -16px rgba(10, 12, 18, 0.5), 0 4px 10px -2px rgba(10, 12, 18, 0.22);

  /* The app's entire motion budget: two durations, plus TWO documented
   * exceptions, enforced by companion/test_companion_app_04.py (no bare
   * literals, no other token).
   * --motion-fast (180ms) is for a REACTION the user is watching
   * confirm; --motion-slow (2s) is for an AMBIENT loop nobody watches
   * (e.g. a breathing status dot) — reaction speed would strobe.
   * Exception: --motion-toast-dwell (6s, success/info) and
   * --motion-toast-dwell-long (12s, pending) are not movement speeds but
   * how long a toast stays before it hides itself. Only the toast's
   * timer hairline animates on them, and toast.js reads the same tokens
   * for its timer, so the two always agree. Never reuse them for
   * anything else.
   * Exception: --motion-dot-pulse (3.2s) is the period of the soft halo
   * that expands and fades around Home's status dot. It is slower than
   * --motion-slow on purpose: the dot is read from across a room, where a
   * 2s loop would look anxious. Only the dot halo rule
   * (.home-state__dot--pulse::after) may spend it; reduced motion turns
   * that animation off outright. Never reuse it for anything else. */
  --motion-fast: 180ms;
  /* The sky-and-motion language's two further durations: the entrance
   * of a card (.reveal and the page sections) and the delay step between
   * siblings. Both are finite, short, and never block interaction. */
  --motion-enter: 400ms;
  --motion-stagger: 60ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --motion-slow: 2s;
  --motion-toast-dwell: 6s;
  --motion-toast-dwell-long: 12s;
  --motion-dot-pulse: 3.2s;
  /* Home's plane flies in once along its trail; only that animation spends
   * this duration. */
  --motion-flight: 1.2s;

  /* Frosted glass: theme-independent geometry. The blur is only applied
   * inside an @supports block at the end of this file; outside it every
   * surface keeps its solid --color-dominant/--color-secondary fill. */
  --glass-blur: 16px;
  --radius-glass: 22px;

  /* Light mode (default) */
  /* The sky behind the page: three stops of a vertical gradient painted by
   * a fixed pseudo-element on <html>. Light mode shows the sky of the
   * viewer's local time of day (html[data-sky], set by static/sky.js;
   * "day" without script). Every stop keeps text and accent readable
   * (companion/test_sky_contrast.py). */
  --sky-top: #D5E7F6;
  --sky-mid: #E3EDF5;
  --sky-bottom: #F4F1EA;
  /* Translucent surfaces painted over the sky: cards and panels, and the
   * bars (sidebar, tab bar, phone header). The composite over the worst
   * sky stop is what the contrast tests measure. The bars are nearly opaque
   * because page content scrolls beneath them: whatever passes behind a bar
   * (a white picture, a dark card) must not wash out its labels. */
  --glass-bg: rgba(255, 255, 255, 0.66);
  --glass-bar-bg: rgba(255, 255, 255, 0.94);
  --glass-border: rgba(255, 255, 255, 0.7);
  --shadow-glass: 0 10px 30px rgba(80, 60, 40, 0.12), 0 2px 6px rgba(80, 60, 40, 0.06);
  --color-canvas: #F7F4EF;
  --color-dominant: #FFFFFF;
  --color-secondary: #EEE8DE;
  --color-border: #DFD7C8;
  /* Deliberately hue-separated (~16°) from --color-status-warn's
   * golden amber (~32°), so an accent control and a warning dot never
   * read as the same signal. Meets WCAG AA text contrast; re-verify
   * both hue separation and contrast before changing this value
   * (companion/test_contrast_check.py pins both). */
  --color-accent: #B13F16;
  --color-accent-hover: #963610;
  --color-on-accent: #FFFFFF;
  --shadow-card: 0 1px 3px rgba(18, 21, 27, 0.06), 0 1px 2px rgba(18, 21, 27, 0.04);
  --shadow-card-hover: 0 4px 12px rgba(18, 21, 27, 0.08), 0 2px 4px rgba(18, 21, 27, 0.05);
  --color-text: #17191F;
  --color-status-ok: #16A34A;
  --color-status-warn: #D97706;
  /* Hue ~345 (crimson), not pure red: measured too close to
   * --color-accent (dE76 22.9/15.9°) at the old value, closer than the
   * accepted accent-vs-warn pair. Now dE76 29.3/30.5° from accent, and
   * its worst-case contrast on light surfaces is 5.16:1 (up from 3.96),
   * safe as text. Enforced by test_contrast_check.py's separation section. */
  --color-status-error: #BE123C;
  /* The switch thumb: white on light surfaces, a light grey on dark ones
   * so an unchecked thumb stays visible on its dark track. */
  --color-switch-thumb: #FFFFFF;
  /* Rarity inks for airlines and aircraft: each is text-safe (4.5:1 or
   * better) on the card and page surfaces of its theme. */
  --color-rarity-occasional: #0F766E;
  --color-rarity-rare: #6D28D9;
  /* The Display page's framed pictures: a wooden-dark bezel and the
   * lifted shadow of a popover over the page. */
  --color-frame-bezel: #2A2622;
  --color-frame-bezel-hi: #4A433C;
  --shadow-pop: 0 18px 48px rgba(18, 21, 27, 0.18), 0 4px 12px rgba(18, 21, 27, 0.08);

  /* The toast's inverse surface (dark slab on light pages, light slab on
   * dark pages) and its five tone inks, each chosen for contrast on that
   * surface rather than on the page. */
  --color-toast-bg: #1D2028;
  --color-toast-fg: #F4F1EC;
  --color-toast-success: #4ADE80;
  --color-toast-info: #C3C9D4;
  --color-toast-warning: #FBBF24;
  --color-toast-error: #FB7185;
  --color-toast-pending: #FF9A70;

  /* Without color-scheme, the browser paints native form-control chrome
   * (e.g. <input type="time">) using its own light/dark heuristic
   * instead of following the OS scheme this default case follows. */
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --sky-top: #0B1026;
    --sky-mid: #18203F;
    --sky-bottom: #2C2540;
    --glass-bg: rgba(255, 255, 255, 0.08);
    --glass-bar-bg: rgba(16, 21, 48, 0.9);
    --glass-border: rgba(255, 255, 255, 0.16);
    --shadow-glass: 0 10px 30px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.2);
    --color-canvas: #0C0F14;
    --color-dominant: #151922;
    --color-secondary: #1C222D;
    --color-border: #2A3040;
    --color-accent: #FF8A5C;
    --color-accent-hover: #FF9E77;
    --color-on-accent: #151922;
    --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
    --shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.2);
    --color-text: #F1F3F6;
    --color-status-ok: #4ADE80;
    --color-status-warn: #FBBF24;
    /* Same rose family as light mode's crimson, shifted lighter for
     * dark. Was too close to the dark accent (dE76 22.3/16.9°, worse
     * than the accent-vs-warn gap); now dE76 32.2/25.6°. */
    --color-status-error: #FB7185;
    --color-switch-thumb: #E6E9EE;
    --color-rarity-occasional: #5EEAD4;
    --color-rarity-rare: #C4B5FD;
    --color-frame-bezel: #050608;
    --color-frame-bezel-hi: #2B2F36;
    --shadow-pop: 0 18px 48px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.35);
    --color-toast-bg: #E9ECF1;
    --color-toast-fg: #151922;
    --color-toast-success: #166534;
    --color-toast-info: #4A505D;
    --color-toast-warning: #92400E;
    --color-toast-error: #BE123C;
    --color-toast-pending: #B13F16;
  }
}

/* Explicit theme overrides — take precedence over prefers-color-scheme. */
html[data-ui-theme="light"] {
  --sky-top: #D5E7F6;
  --sky-mid: #E3EDF5;
  --sky-bottom: #F4F1EA;
  --glass-bg: rgba(255, 255, 255, 0.66);
  --glass-bar-bg: rgba(255, 255, 255, 0.94);
  --glass-border: rgba(255, 255, 255, 0.7);
  --shadow-glass: 0 10px 30px rgba(80, 60, 40, 0.12), 0 2px 6px rgba(80, 60, 40, 0.06);
  --color-canvas: #F7F4EF;
  --color-dominant: #FFFFFF;
  --color-secondary: #EEE8DE;
  --color-border: #DFD7C8;
  --color-accent: #B13F16;
  --color-accent-hover: #963610;
  --color-on-accent: #FFFFFF;
  --shadow-card: 0 1px 3px rgba(18, 21, 27, 0.06), 0 1px 2px rgba(18, 21, 27, 0.04);
  --shadow-card-hover: 0 4px 12px rgba(18, 21, 27, 0.08), 0 2px 4px rgba(18, 21, 27, 0.05);
  --color-text: #17191F;
  --color-status-ok: #16A34A;
  --color-status-warn: #D97706;
  --color-status-error: #BE123C;
  --color-switch-thumb: #FFFFFF;
  --color-rarity-occasional: #0F766E;
  --color-rarity-rare: #6D28D9;
  --color-frame-bezel: #2A2622;
  --color-frame-bezel-hi: #4A433C;
  --shadow-pop: 0 18px 48px rgba(18, 21, 27, 0.18), 0 4px 12px rgba(18, 21, 27, 0.08);
  --color-toast-bg: #1D2028;
  --color-toast-fg: #F4F1EC;
  --color-toast-success: #4ADE80;
  --color-toast-info: #C3C9D4;
  --color-toast-warning: #FBBF24;
  --color-toast-error: #FB7185;
  --color-toast-pending: #FF9A70;
  color-scheme: light;
}

/* Light mode's four skies, chosen by html[data-sky] (static/sky.js, from the
 * viewer's local hour). They sit after the explicit light block and before
 * the explicit dark block on purpose: equal specificity, so the later block
 * wins, and an explicit Dark theme always shows the night sky. Auto with a
 * dark system is handled by the media block directly below. */
html[data-sky="dawn"] {
  --sky-top: #FCDFC6;
  --sky-mid: #F8E4D0;
  --sky-bottom: #F1ECE8;
}

html[data-sky="dusk"] {
  --sky-top: #F7DFDB;
  --sky-mid: #EDE1EA;
  --sky-bottom: #EFE8EF;
}

html[data-sky="night"] {
  --sky-top: #E1E4F3;
  --sky-mid: #E6E8F4;
  --sky-bottom: #EBE9F0;
}

/* Auto theme on a dark system: always the deep night sky, whatever the
 * hour. More specific than the html[data-sky] blocks above. */
@media (prefers-color-scheme: dark) {
  html[data-sky][data-ui-theme="auto"] {
    --sky-top: #0B1026;
    --sky-mid: #18203F;
    --sky-bottom: #2C2540;
  }
}

html[data-ui-theme="dark"] {
  --sky-top: #0B1026;
  --sky-mid: #18203F;
  --sky-bottom: #2C2540;
  --glass-bg: rgba(255, 255, 255, 0.08);
  --glass-bar-bg: rgba(16, 21, 48, 0.9);
  --glass-border: rgba(255, 255, 255, 0.16);
  --shadow-glass: 0 10px 30px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.2);
  --color-canvas: #0C0F14;
  --color-dominant: #151922;
  --color-secondary: #1C222D;
  --color-border: #2A3040;
  --color-accent: #FF8A5C;
  --color-accent-hover: #FF9E77;
  --color-on-accent: #151922;
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.2);
  --color-text: #F1F3F6;
  --color-status-ok: #4ADE80;
  --color-status-warn: #FBBF24;
  --color-status-error: #FB7185;
  --color-switch-thumb: #E6E9EE;
  --color-rarity-occasional: #5EEAD4;
  --color-rarity-rare: #C4B5FD;
  --color-frame-bezel: #050608;
  --color-frame-bezel-hi: #2B2F36;
  --shadow-pop: 0 18px 48px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.35);
  --color-toast-bg: #E9ECF1;
  --color-toast-fg: #151922;
  --color-toast-success: #166534;
  --color-toast-info: #4A505D;
  --color-toast-warning: #92400E;
  --color-toast-error: #BE123C;
  --color-toast-pending: #B13F16;
  color-scheme: dark;
}

/* A single global reduced-motion override every transition in this
 * file inherits for free — no later rule needs its own. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}

/* `*, *::before, *::after` matches ELEMENTS only — the ::view-transition
 * pseudo-element tree is a separate tree these selectors do not reach,
 * so a view transition needs its own reduced-motion opt-out (below),
 * wrapped so the transition is never set up at all rather than set up
 * and run at a near-zero duration. */

/* Baseline "limited" (2026-09-13): Chrome/Edge 126+, Safari/iOS 18.2+,
 * not Firefox — unsupported browsers just navigate instantly, no
 * fallback needed. Each view-transition-name below must be unique per
 * rendered document or the browser silently drops the whole transition
 * with no error; test_browser_ux.py counts the computed value on every
 * route to guard it. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* Base state hidden via display (not opacity/visibility), so the
 * hidden copy stays out of the tab order and a11y tree at mobile width
 * — never two navigation landmarks exposed at once. */
.dashboard-sidebar {
  view-transition-name: skypane-sidebar;
  display: none;
}

/* Every .preview-frame__image declaration site the file used to carry
 * separately, folded into one rule: the view-transition hook, the base
 * box shared with the other thumbnail selectors (now each with their
 * own single rule instead of a group site), the shimmer gradient
 * (background-image, a LONGHAND, deliberately declared after the
 * background SHORTHAND above it so it overrides only that one
 * sub-property rather than the shorthand's implicit background-image:
 * none — reproducing the exact two-rule cascade this used to take), and
 * the aspect-ratio/object-fit pairing that keeps a differently shaped
 * source safely letterboxed inside a guaranteed box. */
.preview-frame__image {
  view-transition-name: skypane-picture;
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-image-backdrop);
  background-image: linear-gradient(
    110deg,
    color-mix(in srgb, var(--color-text) 4%, transparent) 0 40%,
    color-mix(in srgb, var(--color-text) 9%, transparent) 50%,
    color-mix(in srgb, var(--color-text) 4%, transparent) 60% 100%
  );
  aspect-ratio: 3 / 4;
  object-fit: contain;
}

/* The app's only @keyframes block for an ambient pulse, shared by two
 * consumers rather than duplicated (test_companion_app.py fails if a
 * second near-identical block appears). Cycles opacity only, never
 * transform: scale() on a ~6px dot. Floor is 0.35, not 0: a dot that
 * vanishes outright reads as a rendering fault, not a heartbeat. */
@keyframes skypane-pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* The status dot's halo: from the dot's own size and a soft fill out to a
 * larger ring that has faded away; the last third rests so the loop
 * breathes rather than strobes. */
@keyframes skypane-dot-halo {
  0% {
    transform: scale(1);
    opacity: var(--dot-halo-alpha);
  }
  70%, 100% {
    transform: scale(var(--dot-halo-scale));
    opacity: 0;
  }
}

/* A one-shot 0->1 fade, distinct from the ambient pulse above (which
 * never reaches 0 and loops forever) — reusing it would start a
 * refreshed picture at a third of its weight instead of from nothing. */
@keyframes skypane-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* A background wash that drains away, not an opacity fade: a newly
 * arrived row was not literally absent a moment ago. Fades to a
 * transparent form of ITS OWN colour, not the transparent keyword —
 * the final frame must paint identically to the row's real background
 * (a hover tint, a card surface) so there is no snap at the end. */
@keyframes skypane-row-arrive {
  from {
    background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
  }
  to {
    background-color: color-mix(in srgb, var(--color-accent) 0%, transparent);
  }
}

/* An animation, not a transition: the bar goes from display: none to
 * displayed, which a transition cannot animate across without
 * @starting-style — newer than this component's audience. transform
 * and opacity only: this component has shipped a stranded-at-
 * intermediate-size bug before (a blocked save), and neither property
 * can strand a value the way an interpolated size can. */
@keyframes skypane-bar-arrive {
  from {
    opacity: 0;
    transform: translateY(var(--space-md));
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* A floating toast's entrance: a short drop and fade, on --motion-fast.
 * Under reduced motion the global override zeroes it, so the toast just
 * appears. */
@keyframes skypane-toast-in {
  from {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The auto-hide hairline draining over --motion-toast-dwell (or the long one). Under
 * reduced motion the global override drains it at once, so no countdown
 * moves; the toast still hides on toast.js's timer. */
@keyframes skypane-toast-dwell {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}

* {
  box-sizing: border-box;
}

/* The sky: a vertical gradient painted by a fixed, non-interactive
 * pseudo-element behind everything (never background-attachment: fixed,
 * which iOS Safari ignores). html keeps the opaque canvas colour as the
 * floor under it. The pseudo-element takes no space, so it can cause
 * neither layout shift nor horizontal scroll. */
html {
  background: var(--color-canvas);
}

html::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 45%, var(--sky-bottom) 100%);
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--font-body-size);
  font-weight: var(--weight-regular);
  line-height: 1.5;
  background: transparent;
  color: var(--color-text);
  /* No matter what layout quirk exists deep in the tree, the document
   * itself can never scroll horizontally once this is set (measured
   * 1425px client vs 1712px scroll width before this fix).
   * .data-table-wrap's own overflow-x: auto still gives a wide table its
   * own scoped horizontal scroll — do not remove either half. */
  overflow-x: hidden;
}

.mono {
  font-family: var(--font-mono);
  font-weight: var(--weight-regular);
}

/* Typography roles */
.text-label {
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
  line-height: 1.4;
}

.text-body {
  font-size: var(--font-body-size);
  font-weight: var(--weight-regular);
  line-height: 1.5;
}

/* Stays weight-regular on purpose: this is the middle rung of a
 * three-tier heading ladder (semibold .page-title above, semibold
 * nested-card-title below) — a regular middle tier is what keeps all
 * three reading as distinct levels instead of one weight repeated.
 * Carries the same family/letter-spacing/margin every heading-role
 * selector below shares, folded in here rather than left in a group
 * this class also had to join (h1, h2 and h3 keep their own copy since
 * a bare heading tag with no .text-heading class still needs it). */
.text-heading {
  font-family: var(--font-serif);
  font-size: var(--font-heading-size);
  font-weight: var(--weight-regular);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-sm);
}

/* Composes with .text-label (both classes always applied together)
 * rather than restating it. No margin here, deliberately: this role
 * relies on the UA default paragraph margin in its Settings context. */
.section-caption {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.field-error {
  color: var(--color-status-error);
  margin: var(--space-xs) 0 0;
}

/* Mobile-first: a column stack by default, with a @media (min-width:
 * 960px) override further down restoring the inline baseline row. No
 * margin of its own — the <h2> inside still carries the heading-rhythm
 * margin, so spacing below this row is unchanged from before this
 * wrapper existed. display: flex is a literal here on purpose:
 * test_status_pages.py indexes it directly. */
.section-intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 2px;
}

.section-intro > p {
  margin: 0;
}

/* Headings-only: body text, tables, form controls, nav links and
 * monospace content stay on --font-ui, never --font-serif. legend's own
 * identical family/weight/letter-spacing live on its own dedicated rule
 * further down (it is a <fieldset>'s accessible group name, the same
 * role as an <h2>) rather than here, so this selector list is not a
 * second declaration site for it; .text-heading carries its own copy
 * too, above, for the same reason. Any future heading role belongs in
 * this selector — stated once, in the same selector as the family, so a
 * bare heading tag with no size-giving class still gets the right
 * identity and the heading-rhythm margin. */
h1,
h2,
h3 {
  font-family: var(--font-serif);
  font-weight: var(--weight-regular);
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-sm);
}

/* The top rung of the three-tier heading ladder: semibold here,
 * regular on .text-heading, semibold again on the nested-card-title
 * tier — what makes all three read as distinct levels. */
.page-title {
  view-transition-name: skypane-title;
  font-family: var(--font-serif);
  font-size: var(--font-page-title-size);
  font-weight: var(--weight-semibold);
  line-height: 1.08;
  letter-spacing: -0.02em;
  margin: 0 0 var(--space-xs);
}

.page-header__purpose {
  margin: 0 0 var(--space-lg);
}

/* Block-level by element choice (a <p>), keeping .page-header's
 * children block-level end to end so the renderer never has to
 * synthesize an anonymous block box for a stranded inline child. */
.page-header__freshness {
  margin: 0 0 var(--space-xs);
}

/* Holds only the hidden refresh pill, so it reserves no space. */
.page-header__freshness--silent {
  margin: 0;
}

/* Establishes a containing block for the out-of-flow pill further down.
 * The pill used to be an inline-level child stranded between block
 * siblings, forcing an anonymous block box whose line box was consumed
 * on every render whether visible or not (measured: a spurious 28px
 * gap under <h1>). position: absolute removes it from flow entirely,
 * reserving no line box at all. */
.page-header {
  position: relative;
  margin-bottom: var(--space-lg);
}

/* The brand lockup: the logo mark beside the live site-title text. The
 * 28px mark sits inside the title's 28.8px line box, so the lockup is
 * exactly as tall as the bare title used to be and nothing shifts. The
 * mark is decorative (aria-hidden); the text is the accessible name. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
}

.icon.logo-mark,
.logo-mark {
  width: 28px;
  height: 28px;
  flex: none;
  color: var(--color-text);
}

/* The one accent element of the mark: the flick leaving the pane. Bound
 * here, not in the symbol, so the sprite and the login page's inline
 * copy follow the theme from the same rule. */
.logo-accent {
  stroke: var(--color-accent);
}

/* The login card's mark, centred above the heading. */
.login-card .logo-mark {
  display: block;
  width: 40px;
  height: 40px;
  margin: 0 auto var(--space-md);
}

.site-title {
  font-size: 24px;
  font-weight: var(--weight-semibold);
  font-family: var(--font-serif);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* Visible focus outline on every focusable element — never removed
 * without a replacement (accessibility floor). */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

a {
  color: var(--color-accent);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

a:hover {
  color: var(--color-accent-hover);
}

/* The phone app bar: a slim strip holding the logo only. The desktop
 * layout hides it (the sidebar carries the brand there). */
.site-header {
  display: flex;
  align-items: center;
  min-height: 52px;
  padding: var(--space-xs) var(--space-md);
  background: var(--color-secondary);
  margin-bottom: var(--space-sm);
  border-bottom: 1px solid var(--color-border);
}

/* The admin-only two-segment switch between the Console and the personal
 * space. The track is 60px high (8px padding around two 44px segments);
 * the current segment is filled, carries a check glyph and aria-current, so
 * it never relies on colour alone. The phone strip scrolls with the page;
 * the sidebar copy is the first element of the sidebar. */
.space-switch {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-xs);
  padding: var(--space-sm);
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.space-switch__segment {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 44px;
  min-width: 0;
  padding: 0 var(--space-sm);
  overflow: hidden;
  background: var(--color-dominant);
  color: var(--color-text);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--radius-control);
}

/* Glyphs keep their size; only the label gives way, with an ellipsis,
 * when a segment is narrower than its content. */
.space-switch__segment > svg {
  flex: none;
}

.space-switch__segment > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The sidebar's segments are too narrow for check, space glyph and label
 * together: the space glyph gives way there, the check (the non-colour cue
 * for the current space) and the label stay. */
.dashboard-sidebar .space-switch__segment {
  padding: 0 2px;
  gap: 2px;
  font-size: 13px;
}

.dashboard-sidebar .space-switch__check {
  width: 14px;
  height: 14px;
}

.dashboard-sidebar .space-switch__segment > svg:not(.space-switch__check) {
  display: none;
}

.space-switch__segment:hover {
  color: var(--color-accent-hover);
}

.space-switch__segment--current,
.space-switch__segment--current:hover {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

@media (prefers-reduced-motion: reduce) {
  /* The global override would leave a 0.01ms loop running; the halo is
   * switched off instead, so reduced motion shows no animation at all. */
  .home-state__dot.home-state__dot--pulse::after {
    animation: none;
  }
}

/* The `.js` progressive-enhancement gate, shared by every script-only
 * control in this file: hidden by default (display: none, never
 * visibility/opacity, which would leave a dead control focusable),
 * revealed under `.js` via a consumer's own --js-gate-display. The
 * reverse direction (render then hide) flashes a dead control on every
 * load and strands it if the script fails to run at all. */
.js-gate {
  display: none;
}

.js .js-gate {
  display: var(--js-gate-display, block);
}

/* 22x22 visual box with a ::before synthesizing a real 44x44 hit area
 * (22+11+11=44) — every value reused verbatim from .copy-btn, and a
 * harness asserts them equal so a future density pass cannot drift one
 * without the other. No focus rule here: the global focus-visible floor
 * already covers every native <button>. */
.control-hit-area {
  width: 22px;
  height: 22px;
  padding: 0;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: transparent;
}

.control-hit-area::before {
  content: "";
  position: absolute;
  inset: -11px;
}

.control-hit-area .icon {
  width: 14px;
  height: 14px;
}

/* Shared wrapper/track/handle naming convention for a continuous-value
 * control (the quiet-hours dial, the wake-interval slider). No
 * __readout class: a control's printed value reuses .time-value instead.
 * touch-action: none is load-bearing on both the wrapper and the handle
 * (it does not inherit): without it the browser's own panning gesture
 * claims the drag and pointermove never reaches value-controls.js. */
.value-control {
  position: relative;
  touch-action: none;
}

.value-control__track {
  position: relative;
}

.value-control__handle {
  position: absolute;
  touch-action: none;
  /* Every base-button transitioned property except transform: a
   * handle's position represents a live value and must track
   * --value-fraction instantly, never ease through a stale position. */
  transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease;
}

/* The quiet-hours dial: a server-drawn 24h ring; only the two drag
 * handles are gated behind .js. No fill/stroke-width declared below:
 * both ride as presentation attributes, and a CSS declaration of any
 * specificity would beat and flatten them. */
.quiet-dial {
  position: relative;
  /* Pinned by a harness against config_page.py's own QUIET_DIAL_SIZE
   * and QUIET_DIAL_RADIUS — the handle has to land on the stroke the
   * emitter drew, and the two numbers must agree across both files. */
  width: 176px;
  --quiet-dial-radius: 78px;
  margin: 0 auto var(--space-sm);
}

.quiet-dial__ring {
  display: block;
}

.quiet-dial__day {
  stroke: var(--color-border);
}

.quiet-dial__arc {
  stroke: var(--color-text);
}

/* .js gates this rule the instant ANY script has run, before
 * value-controls.js paints anything — at rest this rule and the
 * server's own presentation attributes describe the identical picture.
 * No pathLength: measured to reinterpret the dasharray as a second,
 * scaled coordinate system, painting a spurious second dash. */
.js .quiet-dial .quiet-dial__arc {
  transform-box: fill-box;
  transform-origin: center;
  transform: rotate(calc(-90deg + var(--quiet-start-fraction, 0) * 1turn));
  stroke-dasharray:
    calc(var(--quiet-sweep-fraction, 0) * var(--quiet-dial-radius) * 6.283185307)
    calc(var(--quiet-dial-radius) * 6.283185307);
}

.quiet-dial__hour {
  position: absolute;
  /* 30% is this file's existing CONTEXT strength (the airfield diagram
   * further down uses it too): these orient the drawing, they are not
   * part of its reading. A harness asserts they stay weaker than the arc. */
  color: color-mix(in srgb, var(--color-text) 30%, transparent);
}

/* Each label is placed by its own edge, then pulled back by half of
 * itself on the axis to centre — not its leading edge. */
.quiet-dial__hour--0 {
  top: 16%;
  left: 50%;
  transform: translateX(-50%);
}

.quiet-dial__hour--6 {
  top: 50%;
  right: 13%;
  transform: translateY(-50%);
}

.quiet-dial__hour--12 {
  bottom: 16%;
  left: 50%;
  transform: translateX(-50%);
}

.quiet-dial__hour--18 {
  top: 50%;
  left: 13%;
  transform: translateY(-50%);
}

.quiet-dial__readout {
  margin: 0 0 var(--space-md);
  text-align: center;
}

/* One layer per handle, each covering the ring exactly, so the pointer
 * angle is measured about the ring's own centre. pointer-events: none
 * here is what stops the upper of the two stacked layers from
 * swallowing every press meant for the ring or the other handle. */
.quiet-dial__handles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.quiet-dial__handle-track {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* A harness pins the source order of the two shared rules this element
 * also wears: both declare position at equal specificity, and the
 * absolute one must win. Polar placement without trigonometry: pull
 * back by half, rotate by the value's fraction, walk out to the ring's
 * stroke, then rotate back — carrying the button without spinning it. */
.quiet-dial__handle {
  top: 50%;
  left: 50%;
  transform:
    translate(-50%, -50%)
    rotate(calc(var(--value-fraction, 0) * 1turn))
    translateY(calc(-1 * var(--quiet-dial-radius)))
    rotate(calc(var(--value-fraction, 0) * -1turn));
  pointer-events: auto;
  background: var(--color-canvas);
  border: 1px solid var(--color-text);
  border-radius: 50%;
}

/* Gains 2px over the shared 44x44 hit-area register (22+12+12=46 vs
 * 22+11+11=44): this handle lands at a fractional pixel position from
 * its rotate/translate placement, and the browser's hit-region snapping
 * measured 43x43 with the shared inset — one pixel under the floor. */
.quiet-dial__handle::before {
  inset: -12px;
}

/* A native <input type="range">: the browser supplies the track, thumb,
 * drag and keyboard model; the global input/select rule already supplies
 * the 44px floor and styling. width: 100% is the one geometry
 * declaration — the range's intrinsic width is ~129px in this engine,
 * far too narrow for its 60..3600 band. */
.wake-slider {
  margin-top: var(--space-sm);
}

.wake-slider__input {
  width: 100%;
}

.mobile-nav__link {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: var(--color-text);
  text-decoration: none;
  font-size: var(--font-body-size);
  font-family: var(--font-ui);
  border-radius: var(--radius-control);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

.mobile-nav__link--active {
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  border-radius: var(--radius-control);
  font-weight: var(--weight-semibold);
}

/* :not()-scoped: an unscoped hover here would outrank
 * .mobile-nav__link--active (equal specificity, later in source) and
 * replace the active tint the moment the pointer crossed it. */
.mobile-nav__link:not(.mobile-nav__link--active):hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

.nav-notification {
  width: 8px;
  height: 8px;
  margin-left: var(--space-xs);
  margin-right: 0;
}

/* Screen-reader-only utility. Must never be given display: none or
 * visibility: hidden — either would remove the text from the
 * accessibility tree and defeat its only purpose. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* min-height/min-width: 0 clears the global input/select rule's own
 * 44px floor — a floor from another rule is not cleared by this
 * utility's own 1px sizing. Safe because the wrapping <label> (not this
 * input) is the real 44px+ activation target; wins on specificity
 * (class + type beats a bare type selector) independent of source order. */
input.visually-hidden,
select.visually-hidden {
  min-height: 0;
  min-width: 0;
  appearance: none;
  -webkit-appearance: none;
}

.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--color-accent);
  color: var(--color-on-accent);
  padding: var(--space-sm) var(--space-md);
  z-index: 10;
  border-radius: 0 0 var(--radius-control) 0;
}

.skip-link:focus {
  top: 0;
}

.theme-form {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
}

/* Doubled .theme-form .theme-option (0,2,0), not the bare class (0,1,0):
 * the later button[type="submit"] rule (0,1,1) would otherwise win on
 * background regardless of source order. box-shadow: none is
 * load-bearing too — it clears the submit rule's own inset highlight. */
.theme-form .theme-option {
  height: 28px;
  /* Without flex: 1 1 auto the three intrinsic-width buttons pack to
   * the left instead of filling the bordered container as one control. */
  flex: 1 1 0;
  min-width: 0;
  padding: 0 4px;
  white-space: nowrap;
  /* A label wider than its equal-width segment is clipped with an
   * ellipsis inside its own box; it can never paint over a neighbour. */
  overflow: hidden;
  text-overflow: ellipsis;
  background: transparent;
  border: none;
  box-shadow: none;
  border-radius: 6px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  font-size: 13px;
  font-weight: var(--weight-regular);
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

/* The Account page holds these controls in its own card, where a tap target
 * must reach 44px; the sidebar footer keeps the compact segments. */
section[aria-labelledby="account-preferences"] .theme-form .theme-option {
  height: 44px;
}

/* Preferences: one labelled row per setting. The segmented control is
 * sized to its labels (never stretched); on a narrow screen the text sits
 * above it, from 640px beside it. */
.pref-rows {
  display: flex;
  flex-direction: column;
}

.pref-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--color-border);
}

.pref-row:first-child {
  border-top: none;
  padding-top: 0;
}

.pref-row:last-child {
  padding-bottom: 0;
}

.pref-row__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.pref-row__label {
  font-weight: var(--weight-semibold);
}

.pref-row__hint {
  font-size: 13px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.pref-row .theme-form {
  flex: 0 0 auto;
  max-width: 100%;
}

.pref-row .theme-form .theme-option {
  flex: 0 1 auto;
  min-width: 44px;
  padding: 0 var(--space-md);
}

@media (min-width: 640px) {
  .pref-row {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
  }
}

.account-sign-out button,
.sidebar-footer .logout-form button {
  min-height: 44px;
}

.theme-form .theme-option--active {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
  font-weight: var(--weight-semibold);
}

/* :not()-scoped at the doubled specificity above, matching the nav
 * hover pattern: keeps the active segment's tint from being erased. */
.theme-form .theme-option:not(.theme-option--active):hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

/* button[type="submit"]:hover (0,2,1) beats .theme-option--active's
 * (0,2,0), so a just-clicked segment took the primary hover FILL while
 * keeping the active TEXT colour — unreadable until the pointer moved.
 * This positive-restore rule reaches (0,3,0) to win, reusing the
 * active rule's own wash values rather than inventing a new one. */
.theme-form .theme-option--active:hover {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
}

/* Matched declaration-for-declaration against .page-section below —
 * same surface, hairline, radius and hover reveal. The two rules must
 * be edited together if either changes. */
.theme-status {
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  box-shadow: none;
}

.theme-status:hover,
.theme-status:focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

.theme-status__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.theme-swatch {
  display: inline-flex;
  gap: var(--space-xs);
}

/* The fill is an inline style carrying a raw e-ink palette value — the
 * one colour in this file the stylesheet cannot reason about. The
 * hairline keeps a chip visible if its palette entry is near-white. */
.theme-swatch__chip {
  width: 16px;
  height: 16px;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  display: inline-block;
}

/* Each link is a 44px row, so the rows touch instead of adding a gap. */
.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.sidebar-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: var(--color-text);
  text-decoration: none;
  font-size: var(--font-label-size);
  letter-spacing: 0.02em;
  border-radius: var(--radius-control);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

.sidebar-link__icon {
  flex: none;
}

.sidebar-link--active {
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  border-radius: var(--radius-control);
  font-weight: var(--weight-semibold);
}

/* :not()-scoped, matching .mobile-nav__link's own hover rule: an
 * unscoped hover would erase the active tint on pointer-over. */
.sidebar-link:not(.sidebar-link--active):hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

/* The generic a{color}/a:hover{color} rule (0,1,1) outranks the
 * single-class .sidebar-link/.mobile-nav__link selectors (0,1,0) on
 * color, so hover silently shifted text to accent-hover regardless of
 * active state. These two rules restore per-state text colour at
 * matching (0,2,0) specificity. */
.sidebar-link:hover,
.mobile-nav__link:hover {
  color: var(--color-text);
}

.sidebar-link--active:hover,
.mobile-nav__link--active:hover {
  color: var(--color-accent);
}

/* Sides stay at --space-lg (24px), not the wider spec value: 64px of
 * side padding on a 375px phone would leave a 247px content column,
 * contradicting the mobile floor. The wider pair lands on
 * .dashboard-main below instead, at the >=960px level it actually names. */
.page-content {
  padding: var(--space-xl) var(--space-lg) var(--space-2xl);
}

/* Phones: the title sits close under the app bar. The gap from the bar to
 * the title is the bar's bottom margin (--space-sm, set on .site-header)
 * plus this top padding (--space-md): 24px in total instead of the 64px
 * the desktop-sized values gave. The breakpoint is the one that shows the
 * app bar and the tab bar. */
@media (max-width: 959.98px) {
  .page-content {
    padding-top: var(--space-md);
  }
}

/* Status dot: a 12px circle plus a Label-size text description */
.dot {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: var(--space-xs);
  vertical-align: middle;
}

.dot--ok {
  background: var(--color-status-ok);
}

.dot--warn {
  background: var(--color-status-warn);
}

.dot--error {
  background: var(--color-status-error);
}

.dot--off {
  background: color-mix(in srgb, var(--color-text) 30%, transparent);
}

/* An outlined ring for a frame without a live device (retired, waiting for
 * pairing): shape, not colour, tells it apart from the filled dots. */
.dot--hollow {
  background: transparent;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--color-text) 50%, transparent);
}

/* Names the MOTION, not the component (a dot in this rule, a <time> in
 * relative-time.js), so future consumers reuse this class instead of
 * writing a new one. Under reduced motion the global override already
 * zeroes animation-duration; companion/test_companion_app.py pins the
 * file's reduce-block count at two, so no per-rule block is added. */
.is-breathing {
  animation: skypane-pulse var(--motion-slow) ease-in-out infinite;
}

/* Added only when the picture's src genuinely changed (freshness.js
 * compares it first) — a fade on every swap would flash the page every
 * refresh to say nothing. Names the MOTION, not the component, like
 * .is-breathing above. Reduced motion already zeroes this via the
 * global override; the file's pinned reduce-block count stays at two. */
.is-fading-in {
  animation: skypane-fade-in var(--motion-fast) ease-out;
}

/* One class, both renderings: Flights renders every flight as both a
 * desktop <tr> and a phone <li>, and the diff marks whichever carries
 * the identity, so one rule serves both. Names the MOTION, like
 * .is-breathing/.is-fading-in above. */
.is-new-row {
  animation: skypane-row-arrive var(--motion-slow) ease-out;
}

.dot-label {
  font-size: var(--font-label-size);
  vertical-align: middle;
}

/* Toasts: every alert in the app, one component on the inverse surface
 * (--color-toast-bg/-fg, which flip with the theme). A tone sets one
 * ink, --toast-tone, used by the glyph disc, the action and the timer
 * hairline; the tone is never carried by that colour alone (each tone
 * has its own glyph and a spoken prefix). Flash toasts float only under
 * `@media (scripting: enabled)` below; everything else here is the
 * in-flow form, which is also what a no-script visitor sees. Text and
 * action contrast on this surface are pinned by test_contrast_check.py. */
.toast {
  --toast-tone: var(--color-toast-info);
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 48px;
  margin: 0;
  padding: 2px 2px 2px 10px;
  border-radius: 14px;
  overflow: hidden;
  background: var(--color-toast-bg);
  color: var(--color-toast-fg);
  box-shadow: var(--shadow-toast-docked);
  font-size: var(--font-label-size);
  line-height: 1.4;
}

.toast--success {
  --toast-tone: var(--color-toast-success);
}

.toast--warning {
  --toast-tone: var(--color-toast-warning);
}

.toast--error {
  --toast-tone: var(--color-toast-error);
}

.toast--pending {
  --toast-tone: var(--color-toast-pending);
}

/* A docked toast is a persistent state: in flow, full width, no timer
 * and no dismiss. Its right padding replaces the missing 44px control. */
.toast--docked {
  margin-bottom: var(--space-lg);
  padding-right: var(--space-md);
}

.toast__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--toast-tone);
  color: var(--color-toast-bg);
}

.toast__icon .toast__glyph {
  width: 18px;
  height: 18px;
}

.toast__text {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: 6px 0;
  overflow-wrap: anywhere;
}

/* The section form (Health's source outage) keeps its <h2> for the page
 * outline but reads as the toast's title. */
.toast__title {
  margin: 0;
  font-size: inherit;
  font-family: inherit;
  font-weight: var(--weight-semibold);
  line-height: inherit;
  letter-spacing: normal;
}

.toast__detail {
  margin: 0;
  color: color-mix(in srgb, var(--color-toast-fg) 76%, transparent);
}

/* The visible middle dot between title and detail; silent for assistive
 * technology, which reads the visually-hidden dash instead. */
.toast__title + .visually-hidden + .toast__detail::before {
  content: "\00a0\00b7\00a0" / "";
  color: color-mix(in srgb, var(--color-toast-fg) 45%, transparent);
}

.toast--section .toast__text {
  display: flex;
  flex-direction: column;
}

.toast__action-form {
  display: flex;
  flex: none;
  margin: 0;
}

/* (0,2,0) beats the base button and button[type="submit"] rules, so an
 * Undo button and a link action look the same: tone-coloured text
 * behind a hairline divider, with a 44px target. */
.toast .toast__action {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  height: auto;
  min-height: 44px;
  padding: 0 12px 0 16px;
  border: 0;
  border-radius: 12px;
  background: transparent;
  box-shadow: none;
  color: var(--toast-tone);
  font: inherit;
  font-weight: var(--weight-semibold);
  letter-spacing: normal;
  text-decoration: none;
  white-space: nowrap;
}

.toast .toast__action::before {
  content: "";
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 1px;
  background: color-mix(in srgb, var(--color-toast-fg) 20%, transparent);
}

.toast .toast__action:hover {
  background: color-mix(in srgb, var(--color-toast-fg) 8%, transparent);
  color: var(--toast-tone);
}

.toast .toast__action-glyph {
  width: 16px;
  height: 16px;
}

.toast .toast__dismiss {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: transparent;
  box-shadow: none;
  color: color-mix(in srgb, var(--color-toast-fg) 72%, transparent);
}

.toast .toast__dismiss:hover {
  background: color-mix(in srgb, var(--color-toast-fg) 10%, transparent);
  color: var(--color-toast-fg);
}

.toast .toast__action:focus-visible,
.toast .toast__dismiss:focus-visible,
.toast a:focus-visible {
  outline: 2px solid var(--toast-tone);
  outline-offset: -2px;
}

/* The auto-hide hairline, drawn only once toast.js has armed the timer
 * (data-toast-armed), so a no-script toast never shows a countdown that
 * cannot happen. Paused together with the timer. */
.toast__timer {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: var(--toast-tone);
  transform-origin: left center;
}

.toast[data-toast-armed] .toast__timer {
  display: block;
  animation: skypane-toast-dwell var(--motion-toast-dwell) linear forwards;
}

.toast[data-toast-armed][data-toast-autohide="long"] .toast__timer {
  animation: skypane-toast-dwell var(--motion-toast-dwell-long) linear forwards;
}

.toast.is-paused .toast__timer {
  animation-play-state: paused;
}

.toast__pills {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-left: var(--space-sm);
  vertical-align: middle;
}

.toast__pill {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  background: color-mix(in srgb, var(--color-toast-fg) 12%, transparent);
  color: color-mix(in srgb, var(--color-toast-fg) 88%, transparent);
}

/* The flash region: in flow under the page title by default. */
.toast-region--flash {
  margin: 0 0 var(--space-lg);
}

/* Phone width: the action drops under the text and the dismiss control
 * stays top right, so a long message never squeezes the title. */
@media (max-width: 699.98px) {
  .toast {
    display: grid;
    grid-template-columns: 28px minmax(0, 1fr) 44px;
    column-gap: 12px;
    padding: var(--space-sm) 2px var(--space-sm) 12px;
  }

  /* Each cell pins itself to the top (align-self, not the toast's own
   * align-items), so a multi-line message never re-centres the glyph. */
  .toast__icon {
    align-self: start;
    margin-top: var(--space-xs);
  }

  .toast__text {
    grid-column: 2;
    grid-row: 1;
    align-self: start;
    padding: var(--space-xs) 0 2px;
  }

  .toast--docked .toast__text {
    grid-column: 2 / 4;
  }

  .toast .toast__dismiss {
    grid-column: 3;
    grid-row: 1;
    align-self: start;
  }

  .toast__title,
  .toast__detail {
    display: block;
  }

  .toast__pills {
    display: flex;
    margin: var(--space-xs) 0 0;
  }

  .toast__title + .visually-hidden + .toast__detail::before {
    content: none;
  }

  .toast > .toast__action,
  .toast > .toast__action-form {
    grid-column: 2;
    grid-row: 2;
    justify-self: start;
  }

  .toast .toast__action {
    min-height: 44px;
    margin: -8px 0 0 -10px;
    padding: 0 10px;
  }

  .toast .toast__action::before {
    content: none;
  }
}

/* With scripts running (a media query, not the .js class, so the toast
 * floats from the very first paint and never shifts the page): the flash
 * toast and the quick-switch failure toast float, out of flow, top
 * centre over the content column; on a phone, above the tab bar within
 * thumb reach. The region itself lets clicks through; only the toast
 * takes them. */
@media (scripting: enabled) {
  .toast-region--flash,
  .toast-region--live {
    position: fixed;
    top: var(--space-sm);
    left: 0;
    right: 0;
    z-index: 40;
    display: flex;
    justify-content: center;
    margin: 0;
    padding: 0 var(--space-md);
    pointer-events: none;
  }

  .toast-region--flash .toast,
  .toast-region--live .toast {
    width: max-content;
    max-width: min(680px, 100%);
    border-radius: 16px;
    box-shadow: var(--shadow-toast-float);
    pointer-events: auto;
    animation: skypane-toast-in var(--motion-fast) ease-out;
  }
}

@media (scripting: enabled) and (min-width: 960px) {
  .toast-region--flash,
  .toast-region--live {
    left: calc(240px + var(--space-md) + var(--space-xl));
  }
}

@media (scripting: enabled) and (max-width: 959.98px) {
  .toast-region--flash,
  .toast-region--live {
    top: auto;
    bottom: var(--space-sm);
  }

  .has-tab-bar .toast-region--flash,
  .has-tab-bar .toast-region--live {
    bottom: calc(56px + env(safe-area-inset-bottom, 0px) + var(--space-sm));
  }

  .toast-region--flash .toast,
  .toast-region--live .toast {
    width: 100%;
  }
}

/* Neutral-tinted, not severity-tinted: a row/badge pill outside the
 * toast family. Diverges from .airline-card__chip by one declaration
 * (min-width: 0): this pill lives in a wrapping flex row that must
 * survive a 375px viewport. */
.banner__pill {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  flex: none;
  min-width: 0;
}

/* [hidden]-vs-author-display collision (recurring in this file): an
 * author display: inline-flex always beats the UA [hidden] rule
 * regardless of source order. display: none, not visibility: hidden
 * (unlike .refresh-pill[hidden] below), since this badge must take no
 * space at all when hidden, in-flow. */
.banner__pill[hidden] {
  display: none;
}

/* The frame tile strip above the page title (two or more frames). One
 * whole-tile link per frame: a CSS-drawn frame (dark bezel, white screen), the
 * name, the status dot and the last-seen word. Accent only marks the current
 * tile's outline and the Add a screen tile. Below 700px the strip scrolls
 * sideways; from 700px it wraps. */
.frame-tiles {
  box-sizing: border-box;
  display: flex;
  gap: var(--space-sm);
  margin: 0 0 var(--space-md);
  padding: 4px;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x proximity;
}

.frame-tiles > li {
  display: flex;
  flex: none;
  width: 132px;
  scroll-snap-align: start;
}

.frame-tile {
  box-sizing: border-box;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 44px;
  padding: var(--space-sm);
  background: var(--color-dominant);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  text-align: center;
  text-decoration: none;
}

.frame-tile:hover {
  background: var(--color-control-wash);
}

.frame-tile[aria-current="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 8%, var(--color-dominant));
}

.frame-tile__dot {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
}

.frame-tile__pic {
  display: block;
  box-sizing: border-box;
  flex: none;
  width: 44px;
  height: 58px;
  padding: 3px;
  background: var(--color-frame-bezel);
  border: 1px solid var(--color-frame-bezel-hi);
  border-radius: 5px;
}

.frame-tile__pic i {
  display: block;
  height: 100%;
  background: var(--color-image-backdrop);
  border-radius: 1px;
}

.frame-tile__name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.frame-tile__seen,
.frame-tile__note {
  font-size: var(--font-label-size);
  overflow-wrap: anywhere;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.frame-tile--add {
  justify-content: center;
  background: transparent;
  border-style: dashed;
  color: var(--color-accent);
  font-weight: var(--weight-semibold);
}

.frame-tile__plus {
  font-size: 24px;
  line-height: 1;
}

@media (min-width: 700px) {
  .frame-tiles {
    flex-wrap: wrap;
    overflow-x: visible;
  }
}

/* The collapsible wrapper of the strip. From 700px it adds nothing: the
 * summary row is hidden and the strip looks as above. On a phone it is a
 * marked-off surface (secondary background, hairline border, inset shadow and
 * a soft gradient, all from tokens) whose summary row names the active frame
 * and opens or closes the tiles, which shrink to the picture and the name. */
@media (min-width: 700px) {
  .frame-fold__summary {
    display: none;
  }
}

@media (max-width: 699.98px) {
  .frame-fold {
    margin: 0 0 var(--space-md);
    background:
      linear-gradient(
        color-mix(in srgb, var(--color-text) 5%, transparent),
        transparent var(--space-lg)),
      var(--color-secondary);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    box-shadow: inset 0 2px 6px color-mix(in srgb, var(--color-text) 12%, transparent);
  }

  .frame-fold__summary {
    padding: 0 var(--space-md);
    color: var(--color-text);
    font-weight: var(--weight-semibold);
  }

  .frame-fold__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .frame-fold .frame-tiles {
    margin: 0;
    padding: var(--space-xs) var(--space-sm) var(--space-sm);
  }

  .frame-fold .frame-tiles > li {
    width: 84px;
  }

  .frame-fold .frame-tile {
    gap: var(--space-xs);
    padding: var(--space-xs);
  }

  .frame-fold .frame-tile__pic {
    width: 32px;
    height: 42px;
    padding: 2px;
  }

  .frame-fold .frame-tile__pic--system {
    width: 32px;
    height: 32px;
  }

  .frame-fold .frame-tile__dot {
    top: var(--space-xs);
    right: var(--space-xs);
  }

  .frame-fold .frame-tile__name {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--font-label-size);
  }

  .frame-fold .frame-tile__seen,
  .frame-fold .frame-tile__note {
    display: none;
  }

  .frame-fold .frame-tile--add {
    min-height: 44px;
  }
}

/* The System tile of the Console Fleet strip: a round badge instead of the
 * frame picture. */
.frame-tile__pic--system {
  width: 44px;
  height: 44px;
  border-radius: 50%;
}

.frame-tile__pic--system i {
  border-radius: 50%;
}

/* The blocks of the selected frame (or the System view): stacked on a phone,
 * two columns of block cards from 1000px. The embedded page content brings its
 * own cards and forms; a block only frames them. */
.fleet-blocks {
  box-sizing: border-box;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
  margin: 0 0 var(--space-lg);
}

.fleet-block {
  box-sizing: border-box;
  min-width: 0;
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.fleet-block > .text-heading {
  margin: 0 0 var(--space-sm);
}

.fleet-block__fact {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-md);
  margin: 0 0 var(--space-sm);
}

@media (min-width: 1000px) {
  .fleet-blocks {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: flex-start;
  }

  .fleet-block--wide {
    grid-column: 1 / -1;
  }
}

/* The strip an administrator sees on a personal page of someone else's frame:
 * who owns it and the way back to Fleet. Wraps rather than overflows. */
.frame-notice {
  box-sizing: border-box;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-md);
  min-height: 44px;
  margin: 0 0 var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-secondary);
  color: var(--color-text);
  border-radius: var(--radius-card);
  font-size: var(--font-label-size);
  overflow-wrap: anywhere;
}

.frame-notice__text {
  min-width: 0;
}

.frame-notice__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--font-label-size);
}

.refresh-pill {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  gap: 4px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: var(--weight-regular);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* [hidden]-vs-author-display collision again, at a second call site.
 * This one hides by VISIBILITY, not display: the pill's line box stays
 * reserved, so revealing it on a recurring refresh timer shifts nothing
 * in the header. visibility: hidden removes it from the a11y tree
 * exactly as display: none does. */
.refresh-pill[hidden] {
  visibility: hidden;
}

.refresh-pill .icon {
  width: 14px;
  height: 14px;
}

/* Offsets derived, not guessed: the <h1> line box is 30px at 1.2
 * line-height = 36px tall; the pill is 20px; (36-20)/2 = 8, centring
 * it against the title's own line. Scoped to .page-header, never the
 * bare .refresh-pill, since this positioning is specific to sitting
 * beside a title, not a property of the pill itself. */
.page-header .refresh-pill {
  position: absolute;
  top: 8px;
  right: 0;
}

/* Forms and fieldsets */
fieldset {
  border: 1px solid var(--color-border);
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
}

/* The one legend rule (folded together with what used to be a second,
 * separate bare `legend` rule near the top of the file): family/weight/
 * letter-spacing match the shared heading identity h1/h2/h3/.text-heading
 * carry, plus legend's own size and padding. No margin: legend never
 * joined the heading-rhythm margin group above, and folding these two
 * rules into one must not add it now. */
legend {
  font-family: var(--font-serif);
  font-weight: var(--weight-regular);
  letter-spacing: -0.01em;
  font-size: var(--font-heading-size);
  padding: 0 var(--space-xs);
}

label {
  display: block;
  font-size: var(--font-label-size);
  margin-bottom: var(--space-sm);
}

input,
select {
  min-height: 44px;
  min-width: 44px;
  padding: var(--space-sm);
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  color: var(--color-text);
  font-size: var(--font-body-size);
  font-family: var(--font-ui);
  /* Every native radio/checkbox site-wide renders with the terracotta
   * accent instead of the browser default blue. */
  accent-color: var(--color-accent);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

/* Console forms (Locations and the other fleet forms): a text input or
 * select never grows past its card, whatever its longest option or value. */
.fleet-form input,
.fleet-form select,
.fleet-confirm__form input,
.fleet-confirm__form select {
  max-width: 100%;
  box-sizing: border-box;
}

.fleet-form > p,
.fleet-confirm__form > p {
  min-width: 0;
}

/* Touch-target floor register: WCAG 2.5.8 (AA, 24px) is met everywhere
 * in this file, including every control that trades away the 2.5.5
 * (AAA, 44px) floor for density here — a developer-accepted trade, not
 * an oversight. .copy-btn needs no trade: a ::before pseudo-element
 * synthesizes a real 44x44 hit area instead. */
button {
  height: 30px;
  padding: 0 12px;
  font-size: 13px;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
  font-family: var(--font-ui);
  border-radius: 6px;
  cursor: pointer;
  background: var(--color-control-wash); /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid var(--color-control-border-subtle); /* fallback for browsers without color-mix() */
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  color: var(--color-text);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease, transform .15s ease;
}

button[type="submit"] {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-color: transparent;
  box-shadow: var(--shadow-control);
}

/* Not a bare .theme-option:hover: .theme-form .theme-option (0,2,0)
 * is what keeps theme buttons out of the accent fill, and a lower-
 * specificity hover rule here would lose to it and repaint them. */
button[type="submit"]:hover {
  background: var(--color-accent-hover);
  border-color: transparent;
  box-shadow: var(--shadow-control-hover);
  transform: translateY(-1px);
}

/* Excludes .value-control__handle: that class is positioned via
 * transform from --value-fraction, and CSS transform REPLACES wholesale
 * rather than composing — a transform declared here would silently
 * delete that position instead of nudging it. */
button:not(.value-control__handle):active {
  transform: translateY(1px);
}

/* .logout-form button and button[type="submit"] are equal (0,1,1)
 * specificity, so source order (placed after) is what actually wins
 * here. box-shadow: none clears the inset highlight a submit button
 * would otherwise inherit from the primary rule. */
.logout-form button {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  box-shadow: none;
  color: var(--color-text);
}

.dirty-bar {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  margin-top: var(--space-md);
  box-shadow: var(--shadow-card-hover), 0 12px 32px var(--color-shadow-strong);
  /* Reuses the existing skypane-bar-arrive @keyframes rather than a new
   * block, keeping the file's pinned @keyframes count unchanged. Fires
   * when display changes from none, the only way this element is ever
   * revealed. The global reduced-motion override already zeroes it. */
  animation: skypane-bar-arrive var(--motion-fast) ease-out;
}

/* [hidden]-vs-author-display collision, the same trap fixed elsewhere
 * in this file: an author display: flex always beats the UA
 * [hidden] { display: none } rule regardless of source order. */
.dirty-bar[hidden] {
  display: none;
}

.dirty-bar__cancel {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  box-shadow: none;
  color: var(--color-text);
}

/* .frame-strip__cell button and button[type="submit"] are equal
 * (0,1,1) specificity; placed after it in source order to win, the
 * same precedent .logout-form button above documents. */
.frame-strip__cell button {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  box-shadow: none;
  color: var(--color-text);
}

/* button:hover (0,1,1) must sit after .logout-form button (also
 * (0,1,1)) in source order, or the quietest control gets no hover
 * feedback. .frame-strip__cell button (0,1,1), placed before this
 * rule, inherits the same hover wash for free. */
button:hover {
  background: color-mix(in srgb, var(--color-text) 7.5%, transparent);
  border-color: color-mix(in srgb, var(--color-text) 14%, transparent);
}

/* 28x28 visual box with a real 44x44 hit area synthesized by the
 * ::before pseudo-element's negative inset below — no accessibility
 * trade-off. The pattern to reuse for any future icon-only button. */
.copy-btn {
  width: 22px;
  height: 22px;
  padding: 0;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: transparent;
}

.copy-btn .icon {
  width: 14px;
  height: 14px;
}

/* The invisible 44x44 hit area: 22 (visual box) + 11 on each side. */
.copy-btn::before {
  content: "";
  position: absolute;
  inset: -11px;
}

.copy-btn:hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
}

.copy-btn__label {
  display: none;
}

.copy-btn--copied .copy-btn__label {
  display: inline;
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: var(--space-xs);
  white-space: nowrap;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-status-ok);
}

/* Disabled/cooldown state: reduced opacity, not-allowed cursor. */
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.runway-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.quiet-preset-row {
  display: flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  overflow: hidden;
  margin-bottom: var(--space-md);
}

.quiet-preset-row button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  border: none;
  border-radius: 0;
}

.quiet-preset-row button:not(:first-child) {
  border-left: 1px solid var(--color-border);
}

/* Narrow-viewport fallback required by arithmetic already on record:
 * two 144px time inputs (.config-form input[type="time"]'s own
 * min-width) plus a gap need more width than this card's own 280px
 * inner content at the 360px contract floor. */
.quiet-dial-summary {
  margin-top: var(--space-md);
  text-align: center;
}

.quiet-times-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm) var(--space-lg);
}

@media (max-width: 479.98px) {
  .quiet-times-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

.runway-card {
  display: block;
  position: relative;
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: none;
  padding: var(--space-md);
  flex: 1 1 0;
  min-width: 0;
  cursor: pointer;
  /* Declared on the base rule, not inside @supports: a transition
   * animates however the state is reached — through :has() where
   * supported, through the --selected fallback where not.
   * test_config_page.py pins the file at exactly one feature-query block. */
  transition: transform var(--motion-fast) ease,
    border-color var(--motion-fast) ease,
    box-shadow var(--motion-fast) ease,
    background-color var(--motion-fast) ease;
}

/* :not(.runway-card--selected) makes this structurally incapable of
 * matching a selected card, independent of source order or specificity. */
.runway-card:not(.runway-card--selected):hover,
.runway-card:not(.runway-card--selected):focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

/* box-shadow: inset ... 2px, not a thicker border: in a flex: 1 1 0
 * row, border-box sizing does not stop a thicker border growing the
 * card's own distributed outer width (measured: 98.67px vs siblings'
 * 96.66px). An inset box-shadow paints inside the padding box instead
 * and contributes to no box at all, so selection is geometrically free. */
/* transform: scale, not a width/padding/border change: a transform
 * paints at a different size without participating in layout, so the
 * old 2px-wider-selected-card defect (measured 98.67px vs 96.66px) is
 * structurally impossible here. test_browser_ux.py measures offsetWidth
 * specifically, not getBoundingClientRect(), to assert that. */
.runway-card--selected {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 2px var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  transform: scale(1.02);
}

.runway-card__number {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
}

.runway-card__image {
  display: block;
  max-width: 100%;
  height: auto;
  margin-top: var(--space-sm);
}

/* The check glyph's markup exists on every card, shown only on the
 * selected one via the server-rendered --selected modifier. */
.runway-card__check {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  color: var(--color-accent);
  display: none;
}

.runway-card--selected .runway-card__check {
  display: inline-flex;
}

/* Re-keys selection from --selected (server-computed, never updates on
 * a client click) to live :has(input:checked) state, which is (0,2,1)
 * and beats the (0,1,0) fallback. A positive restore rule further down
 * re-wins the existing (0,3,0) hover guard at (0,3,1). A browser
 * without :has() skips this whole block and keeps --selected only. */
@supports selector(:has(*)) {

  .runway-card:has(input:checked) {
    border-color: var(--color-accent);
    box-shadow: inset 0 0 0 2px var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
    transform: scale(1.02);
  }

  .runway-card:has(input:checked) .runway-card__check {
    display: inline-flex;
  }

  .runway-card:has(input:checked):hover,
  .runway-card:has(input:checked):focus-within {
    border-color: var(--color-accent);
    box-shadow: inset 0 0 0 2px var(--color-accent);
  }

  .runway-card--selected:not(:has(input:checked)) {
    border: 1px dashed color-mix(in srgb, var(--color-text) 70%, transparent);
    box-shadow: none;
    background: transparent;
    transform: none;
  }

  .runway-card--selected:not(:has(input:checked)) .runway-card__check {
    display: none;
  }

  /* Top-right here (unlike the chip's bottom-right): .runway-card has
   * no preview band occupying that corner. */
  .runway-card--selected:not(:has(input:checked))::after {
    content: attr(data-current-label);
    position: absolute;
    top: var(--space-sm);
    right: var(--space-sm);
    font-size: 12px;
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: color-mix(in srgb, var(--color-text) 70%, transparent);
  }

  /* The real focus target is the visually-hidden native radio; its own
   * outline is off-screen and unseeable, so keyboard focus on a
   * selected card was invisible without this. :focus-visible on the
   * radio, not :focus-within on the card, since a mouse click on the
   * label also focuses the radio and a ring on every click would be noise. */
  .runway-card:has(input:focus-visible) {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  /* :has() proves the bar exists in the DOM with no script needed, so a
   * scripts-blocked visitor still gets clearance. >=960px: the bar
   * measures 64px (never wraps at this width) plus --space-lg (24px)
   * above the viewport foot = 88px, measured via a real browser. */
  .dashboard-main:has(.dirty-bar) {
    padding-bottom: calc(var(--space-2xl) + 88px);
  }

  /* Measured via a real browser, scripts enabled, scrolled to the
   * document bottom, worst realistic case: 134px bar + 56px tab bar +
   * 16px anchor + 16px cushion = 222px, rounded up to 224px on the 8px
   * grid. .has-tab-bar keeps this from matching a hypothetical future
   * page that renders the bar without the tab bar. */
  .has-tab-bar .page-content:has(.dirty-bar) {
    padding-bottom: calc(
      var(--space-2xl) + 176px + env(safe-area-inset-bottom, 0px));
  }

  /* The Display look card's live selection states. */
  .look-cell:has(input:checked),
  .look-option:has(input:checked) {
    border-color: var(--color-accent);
    box-shadow: inset 0 0 0 2px var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  }

  /* The saved look, once another one is picked: a quiet dashed ring,
   * accent-free, so the page shows both what is saved and what is new. */
  .look-cell--saved:not(:has(input:checked)),
  .look-option--saved:not(:has(input:checked)) {
    border: 1px dashed color-mix(in srgb, var(--color-text) 70%, transparent);
  }

  .look-cell:has(input:focus-visible),
  .look-option:has(input:focus-visible),
  .look-dot:has(input:focus-visible),
  .look-seg__option:has(input:focus-visible) {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  .look-dot:has(input:checked) {
    box-shadow: 0 0 0 2px var(--color-dominant), 0 0 0 4px var(--color-accent);
  }

  .look-dot:has(input:checked)::after {
    content: "";
    position: absolute;
    inset: 0;
    width: 10px;
    height: 10px;
    margin: auto;
    border-radius: 50%;
    background: var(--color-image-backdrop);
    box-shadow: 0 0 0 1px var(--color-border);
  }

  .look-seg__option:has(input:checked) {
    background: var(--color-dominant);
    color: var(--color-text);
    box-shadow: 0 0 0 1px var(--color-border), var(--shadow-card);
  }

  .look-seg__option:has(input:disabled) {
    opacity: 0.45;
    text-decoration: line-through;
    cursor: not-allowed;
  }
}

/* Detection page. Summary of the saved setting: a label-voice caption over
 * an emphasis value, one row per fact. */
.detection-summary {
  display: grid;
  gap: var(--space-md);
  margin: 0;
}

.detection-summary > div {
  display: grid;
  gap: var(--space-xs);
}

.detection-summary dt {
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.detection-summary dd {
  margin: 0;
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.detection-summary__detail {
  display: block;
  font-weight: var(--weight-regular);
  margin-top: var(--space-xs);
}

.detection-tag {
  font-family: var(--font-mono);
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
}

/* Links on this page are 44px tall: inline-flex so the box, not the text
 * line, is the hit target. */
.detection-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Runway cards: the .runway-row flex row becomes a grid here, since an
 * airport can list many runways and flex would crush them on a phone. */
.detection-runways {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-sm);
}

@media (min-width: 600px) {
  .detection-runways {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  }
}

.detection-runways .runway-card__detail {
  display: block;
  font-variant-numeric: tabular-nums;
}

/* summary's own idiom reused verbatim: a centred flex row with a 44px
 * min-height. This relocates the touch-target floor to the label (a
 * native checkbox's label is already a click target), the same pattern
 * .copy-btn's synthesized ::before hit area uses — not a new opt-out
 * of the base button rule's floor register. */
.settings-checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 44px;
}

/* Undoes the global input/select rule declaration by declaration: both
 * 44px minimums are cleared to zero — load-bearing, since a plain
 * width/height cannot shrink an element below another rule's minimum
 * unless that minimum is itself cleared, or the 16px sizing is inert. */
.settings-checkbox input[type="checkbox"] {
  width: 16px;
  height: 16px;
  min-height: 0;
  min-width: 0;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  flex: none;
}

/* Data table: quiet uppercase label header, one row surface, hover-only
 * row cue. Wrapped in .data-table-wrap, which scrolls horizontally
 * rather than letting a wide table overflow the viewport on phones. */
/* Four-layer background-attachment trick: two opaque "cover" gradients
 * scroll WITH the content (local) while two shadow gradients stay fixed
 * to the viewport (scroll). When nothing is scrollable the covers sit
 * exactly over the shadows and occlude them by geometry alone. */
.data-table-wrap {
  overflow-x: auto;
  max-width: 100%;
  background-image:
    linear-gradient(to right, var(--color-canvas), transparent 24px),
    linear-gradient(to left, var(--color-canvas), transparent 24px),
    radial-gradient(farthest-side at 0 50%,
      color-mix(in srgb, var(--color-text) 15%, transparent), transparent),
    radial-gradient(farthest-side at 100% 50%,
      color-mix(in srgb, var(--color-text) 15%, transparent), transparent);
  background-repeat: no-repeat;
  background-size: 24px 100%, 24px 100%, 12px 100%, 12px 100%;
  background-position: left, right, left, right;
  background-attachment: local, local, scroll, scroll;
}

/* The one place .data-table-wrap sits inside a card surface instead of
 * the page background — the cover gradients must match that surface or
 * they read as a visible coloured band. */
.page-section .data-table-wrap,
.battery-trend-section .data-table-wrap {
  background-image:
    linear-gradient(to right, var(--color-dominant), transparent 24px),
    linear-gradient(to left, var(--color-dominant), transparent 24px),
    radial-gradient(farthest-side at 0 50%,
      color-mix(in srgb, var(--color-text) 15%, transparent), transparent),
    radial-gradient(farthest-side at 100% 50%,
      color-mix(in srgb, var(--color-text) 15%, transparent), transparent);
}

.data-table {
  width: 100%;
  min-width: max-content;
  border-collapse: collapse;
}

/* Opts the Resolution-statistics table out of the no-crop floor above.
 * Equal specificity with the base rule, so source order decides — this
 * rule must stay declared after it. A stylesheet guard in
 * test_status_pages.py pins that ordering. */
.data-table--prose {
  min-width: 0;
}

.data-table--prose th:first-child,
.data-table--prose td:first-child {
  white-space: nowrap;
}

/* Releasing the no-crop floor is safe here (and was rejected for the
 * registry table) for a column-specific reason: this table's only
 * columns are a single-token integer and a timestamp whose one break
 * point falls on a real word boundary in both languages (measured). */
table.data-table--readings {
  min-width: 0;
}

/* Real top padding, matching the bottom value: with zero top padding
 * the sticky header's opaque background began exactly at the glyph
 * tops, clipping ascenders in real Safari (not reproduced in Chromium)
 * — an engine-specific ascent-metric difference, not a WebKit-only bug. */
.data-table th {
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  text-align: left;
  padding: 10px var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.data-table td {
  padding: 13px var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

/* Scoped to Flights' own table only. Tokens only (no literal px), so
 * this rule tracks --space-sm/--space-md automatically if either token
 * ever changes. */
table.data-table--flights td,
table.data-table--flights th {
  padding: var(--space-sm) var(--space-sm);
}

/* .data-table's own min-width: max-content sizes every column to its
 * widest UNWRAPPED line, so inline primary+secondary spans blow through
 * the column budget. Making them block-level (stacked) makes each
 * column's max-content width the WIDER of its two lines instead of
 * their concatenation. Scoped to this table only. */
table.data-table--flights .cell-primary,
table.data-table--flights .cell-secondary {
  display: block;
}

table.data-table--flights .cell-inline-sep {
  display: none;
}

table.data-table--registry .cell-primary,
table.data-table--registry .cell-secondary {
  display: block;
}

table.data-table--registry .cell-inline-sep {
  display: none;
}

/* A version identifier beside its icon keeps the icon inline with it and
 * wraps as a unit, never mid-token. */
.data-cards .cell-primary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

/* The Update page. Two cards: a call-to-action card (up to date, or the
 * one newer release to install, or the schedule in flight) and a
 * "Versions" card of compact one-line rows, each a native <details>
 * holding every note, the absolute dates and the action, so the page
 * works with scripts blocked. Status colour only marks state (ok, warn,
 * error); the accent fill is the global primary button and is spent only
 * on installing a newer release. */
/* The hardware type switch reuses the segmented control (.theme-form).
 * Type labels are long, so a segment may wrap to a second line instead
 * of clipping, and the control sits one card gap above the cards. The
 * doubled selector (0,3,0) outranks .theme-form .theme-option's fixed
 * height and nowrap. */
.update-type-chooser {
  margin-bottom: var(--space-lg);
}

.update-type-chooser .theme-option {
  height: auto;
  min-height: 28px;
  padding: var(--space-xs) var(--space-sm);
  white-space: normal;
  overflow-wrap: anywhere;
}

.update-cta__top {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}

.update-cta__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}

.update-cta--ok .update-cta__icon {
  color: var(--color-status-ok);
  background: color-mix(in srgb, var(--color-status-ok) 14%, transparent);
}

.update-cta--warn .update-cta__icon {
  color: var(--color-status-warn);
  background: color-mix(in srgb, var(--color-status-warn) 14%, transparent);
}

.update-cta--error .update-cta__icon {
  color: var(--color-status-error);
  background: color-mix(in srgb, var(--color-status-error) 14%, transparent);
}

.update-cta__icon .icon {
  width: 22px;
  height: 22px;
}

.update-cta__text {
  min-width: 0;
  flex: 1;
}

/* The card's headline is the nested-card-title voice (16px sans
 * semibold), the same rung .page-section--nested > h2 uses. */
.update-cta__title {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-ui);
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  letter-spacing: normal;
  line-height: 1.3;
}

.update-cta__detail {
  margin: 0;
  font-size: var(--font-label-size);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

/* A relative age never breaks mid-phrase ("il y a / 1 h"). */
.update-cta time,
.update-row__meta time {
  white-space: nowrap;
}

.update-cta__detail + .update-cta__detail {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.update-cta__note {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.update-cta__on {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
  font-size: var(--font-label-size);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.update-cta .mono {
  white-space: nowrap;
}

/* Install is a safety-relevant action: it keeps the full touch target
 * at every width. Specificity (0,2,1) beats both the global button and
 * the quiet secondary button's own padding and font size. */
.update-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}

.update-actions form {
  margin: 0;
}

.update-actions button[type="submit"] {
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  font-size: var(--font-label-size);
  white-space: nowrap;
}

.update-cta .update-actions {
  margin-top: var(--space-md);
}

.update-cta .update-actions form,
.update-cta .update-actions button[type="submit"] {
  width: 100%;
}

.update-versions__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
}

.update-versions__head h2 {
  margin: 0;
}

.update-versions__count {
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.update-rows {
  list-style: none;
  margin: var(--space-sm) 0 0;
  padding: 0;
}

.update-rows > li + li {
  border-top: 1px solid var(--color-border);
}

/* The one-line row. Phone: version and age on the first line, the first
 * note on its own ellipsised line below, the chevron spanning both.
 * Replaces the global summary's flex layout and leading chevron: the row
 * owns a trailing chevron instead. */
.update-row > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 20px;
  grid-template-areas: "v d c" "s s c";
  column-gap: var(--space-sm);
  row-gap: 2px;
  align-items: center;
  min-height: 60px;
  padding: var(--space-sm) 0;
  color: var(--color-text);
  list-style: none;
}

.update-row > summary::before {
  content: none;
}

.update-row > summary::-webkit-details-marker {
  display: none;
}

.update-row__version {
  grid-area: v;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  min-width: 0;
}

.update-row__version .mono {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.update-row__when {
  grid-area: d;
  font-size: var(--font-label-size);
  white-space: nowrap;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.update-row__note {
  grid-area: s;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.update-row__note:empty,
.update-row[open] .update-row__note {
  display: none;
}

.update-row__chevron {
  grid-area: c;
  justify-self: end;
  transform: rotate(90deg);
  transition: transform var(--motion-fast) ease;
}

.update-row[open] > summary .update-row__chevron {
  transform: rotate(-90deg);
}

.update-row__body {
  display: grid;
  gap: var(--space-sm);
  padding: 0 0 var(--space-md);
}

/* Notes are untrusted CI text: overflow-wrap: anywhere lets one long
 * unbroken token wrap instead of widening the card past the viewport. */
.update-row__notes {
  margin: 0;
  padding: 0 0 0 var(--space-md);
  font-size: var(--font-label-size);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.update-row__notes li {
  margin: 0 0 var(--space-xs);
}

.update-row__meta {
  margin: 0;
  font-size: var(--font-label-size);
  line-height: 1.5;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The running release: a status-ok wash and rail bleeding to the card's
 * own edges (the card pads 16px, 24px from 960px). */
.update-row--running {
  margin-inline: calc(-1 * var(--space-md));
  padding-inline: var(--space-md);
  background: color-mix(in srgb, var(--color-status-ok) 7%, transparent);
  box-shadow: inset 3px 0 0 var(--color-status-ok);
}

@media (min-width: 960px) {
  .update-cta .update-actions form {
    width: auto;
  }

  .update-cta .update-actions button[type="submit"] {
    width: auto;
    min-width: 200px;
  }

  .update-row > summary {
    grid-template-columns: 232px minmax(0, 1fr) 140px 20px;
    grid-template-areas: "v s d c";
    column-gap: var(--space-md);
    min-height: 56px;
  }

  .update-row__when {
    text-align: right;
  }

  .update-row__body {
    padding-left: 248px;
    padding-right: var(--space-lg);
  }

  .update-row--running {
    margin-inline: calc(-1 * var(--space-lg));
    padding-inline: var(--space-lg);
  }
}

.flight-day-row th {
  background: var(--color-canvas);
  text-align: left;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* 70% muted strength, matching .section-caption/.data-table th. color,
 * not opacity: .data-table td above declares its own border-bottom, and
 * opacity would fade that hairline along with the text. Pinned in
 * test_contrast_check.py's existing muted-on-card-surface pairs. */
.data-table td.desc {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* A clean bottom edge: the last row carries no trailing separator line. */
.data-table tbody tr:last-child td {
  border-bottom: none;
}

.data-table tbody tr:hover {
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}

/* opacity on --color-text gives a muted read in both themes with no
 * per-theme value to keep in sync — no --color-text-muted token exists
 * in this file. 13px/0.75 (up from 12px/0.7): the smaller pair measured
 * at the edge of comfortable contrast on the dark surface. */
.cell-primary {
  font-family: var(--font-mono);
  font-weight: var(--weight-semibold);
}

.cell-secondary {
  font-family: var(--font-mono);
  font-size: 13px;
  opacity: 0.75;
}

.cell-inline-sep {
  opacity: 0.7;
  margin: 0 var(--space-xs);
}

/* .history-cards ~ .data-table-wrap (a sibling combinator) scopes this
 * toggle to History's own pair only — Airlines/Health also reuse
 * .data-table-wrap but never render a sibling .history-cards. */
.history-cards {
  display: grid;
  gap: var(--space-sm);
  list-style: none;
  margin: 0;
  padding: 0;
}

.history-cards ~ .data-table-wrap {
  display: none;
}

/* The phone boarding-pass card. No padding on the card itself: each
 * band carries its own, so the stub's tear line can run edge to edge
 * and its notches can sit on the card's border. */
.history-card {
  position: relative;
  background: var(--color-dominant);
  /* Same hairline-at-rest contract as .runway-card above. */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

/* A slim day header between cards, in the label voice with a trailing
 * hairline. Not a filter row: it carries no data-filter-text. */
.history-cards__day {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.history-cards__day:first-child {
  margin-top: 0;
}

/* [hidden]-vs-author-display collision: the filter hides a day header
 * with no visible row under it. */
.history-cards__day[hidden] {
  display: none;
}

.history-cards__day::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--color-border);
}

/* Identity on the left in a shrinkable track, the 44px picture action
 * pinned to its own content width on the right. */
.history-card__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) 0 var(--space-md);
}

.history-card__id {
  min-width: 0;
  padding-top: var(--space-xs);
}

.history-card__callsign {
  display: block;
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.03em;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.history-card__note {
  font-family: var(--font-ui);
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
  letter-spacing: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* One line, ellipsised: a long operator name never pushes the card
 * taller or the picture action out of its track. */
.history-card__airline {
  display: block;
  font-size: var(--font-label-size);
  line-height: 1.4;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.history-card__airline--unknown {
  font-style: italic;
}

/* An icon-only link with a real 44px box, not a padded glyph. */
.history-card__picture {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
  color: var(--color-text);
}

.history-card__picture:hover {
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}

/* origin | track | destination: the codes span both grid rows and pin
 * to the top, and the first row is exactly one code tall, so the
 * dashed track runs through the codes' middle while the direction
 * label tucks under the plane beside them rather than on a row of its
 * own. */
.history-card__route {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: var(--font-heading-size) auto;
  grid-template-areas:
    "from track to"
    "from dir to";
  column-gap: var(--space-sm);
  padding: var(--space-xs) var(--space-md) var(--space-sm);
}

.history-card__code {
  font-size: var(--font-heading-size);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  line-height: 1;
}

.history-card__code--from {
  grid-area: from;
  align-self: start;
}

.history-card__code--to {
  grid-area: to;
  align-self: start;
}

.history-card__code--home {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.history-card__code--none {
  line-height: var(--font-heading-size);
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
  letter-spacing: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.history-card__track {
  grid-area: track;
  align-self: center;
  position: relative;
  display: flex;
  justify-content: center;
  min-width: 0;
}

/* The dashed track behind the plane, through the glyph's middle. */
.history-card__track::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  border-top: 1px dashed color-mix(in srgb, var(--color-text) 30%, transparent);
}

/* The dominant-surface pad masks the dashes under the glyph. */
.icon.history-card__plane {
  position: relative;
  width: 16px;
  height: 16px;
  padding: 0 var(--space-xs);
  box-sizing: content-box;
  background: var(--color-dominant);
  color: var(--color-text);
}

.history-card__dir {
  grid-area: dir;
  justify-self: center;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1.2;
  white-space: nowrap;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The stub below the tear line: artwork plate and time on one row.
 * The two notches are half-discs in the page colour sitting on the
 * card's side borders, so they follow the canvas in both themes. */
.history-card__stub {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border-top: 1px dashed var(--color-border);
}

.history-card__stub::before {
  content: "";
  position: absolute;
  top: -8px;
  width: 14px;
  height: 14px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--history-card-notch, var(--color-canvas));
  left: -8px;
  clip-path: inset(0 0 0 50%);
}

.history-card__stub::after {
  content: "";
  position: absolute;
  top: -8px;
  width: 14px;
  height: 14px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--history-card-notch, var(--color-canvas));
  right: -8px;
  clip-path: inset(0 50% 0 0);
}

/* A fixed 40px-tall plate; the artwork keeps its aspect inside it on
 * the image backdrop, the same backing every artwork thumbnail uses. */
.history-card__art {
  display: block;
  box-sizing: border-box;
  width: 100%;
  max-width: 200px;
  height: 40px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
}

img.history-card__art {
  background: var(--color-image-backdrop);
  object-fit: contain;
}

.history-card__art--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-sm);
  border-style: dashed;
  font-size: 12px;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.history-card__when {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.3;
  white-space: nowrap;
}

/* The row-level picture action on the card and in the table: the
 * secondary-button look it inherits from .calendar-disconnect-btn, with
 * the 44px tap floor and no wrapping inside the narrow picture column. */
a.calendar-disconnect-btn.flight-picture-link {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}

/* Renders INSIDE an existing .page-section card, unlike .history-card
 * which sits on the page background — so .data-card carries no fill,
 * radius or shadow, only the row-hairline .data-table td already uses.
 * .data-card__desc repeats the 70% muted literal rather than joining
 * .data-table td.desc's list: test_status_pages.py pins that exact string. */
.data-cards {
  list-style: none;
  margin: 0;
  padding: 0;
}

.data-cards ~ .data-table-wrap {
  display: none;
}

/* A list with no desktop table sibling stays a list at every width; the
 * extra class outranks the >=960px rule that hides .data-cards. */
.data-cards.data-cards--standalone {
  display: block;
}

.data-card {
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border);
}

.data-card:last-child {
  border-bottom: none;
}

.data-card__primary {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  justify-content: space-between;
}

.data-card__value {
  margin-left: auto;
}

/* The identifying text may shrink and wrap so a long second line cannot
 * push the status pill past the card edge on a narrow phone. */
.data-card__primary > :first-child {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Mirrors .data-table th's own label tier by VALUE, not selector — a
 * harness check pins these two font-size declarations string-equal, so
 * they must always move together. */
.data-card__label {
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.data-card__secondary {
  margin-top: var(--space-xs);
  display: flex;
  gap: var(--space-sm);
}

.data-card__desc {
  margin: var(--space-xs) 0 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.data-card__details {
  margin-top: var(--space-sm);
}

/* auto 1fr sizes the label column to the longest label once, so every
 * value shares one left edge. Scoped inside .resolve-context so this
 * file still declares no bare dl/dt/dd rule. */
.resolve-context {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-xs) var(--space-sm);
  align-items: baseline;
  margin: 0 0 var(--space-md);
}

/* [hidden]-vs-author-display collision, the same trap the other
 * [hidden] guards in this file fix: an author display declaration
 * always beats the UA [hidden] rule regardless of source order. */
.resolve-context[hidden] {
  display: none;
}

.resolve-context dt,
.resolve-context dd {
  margin: 0;
}

.resolve-name-field {
  margin-bottom: var(--space-md);
}

.data-card__action {
  margin-top: var(--space-xs);
}

/* The shared filter bar (Flights, Airlines, Health's prefix list),
 * driven by list-filter.js; inert without JS — the full unfiltered list
 * stays completely usable. A search field with an inline clear, an
 * optional segmented chip group carrying live counts, and a quiet
 * count. Reuses existing tokens only. */
.filter-bar {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
  margin-bottom: var(--space-md);
  flex-wrap: wrap;
}

.filter-bar__search {
  position: relative;
  flex: 1 1 280px;
  max-width: 560px;
  min-width: 0;
}

/* 16px text: iOS Safari zooms the page on focus for anything smaller.
 * 48px tall clears the 44px target floor. height wins over the global
 * input rule's min-height because 48 > 44. */
.filter-bar__search input {
  width: 100%;
  height: 48px;
  padding: 0 52px 0 46px;
  font-size: var(--font-body-size);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  -webkit-appearance: none;
  appearance: none;
}

/* The native cancel glyph would double up with the inline clear. */
.filter-bar__search input:placeholder-shown {
  padding-right: var(--space-md);
}

.filter-bar__search input::-webkit-search-cancel-button,
.filter-bar__search input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

.filter-bar__search input:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.filter-bar__search .icon {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}

/* Reset of every base-button property that would box the glyph; the
 * 44px square is the hit target, the glyph inside it is 14px. (0,2,0)
 * outranks button, button[type="submit"] and the hover/active rules. */
.filter-bar .filter-bar__clear {
  position: absolute;
  right: 2px;
  top: 2px;
  width: 44px;
  height: 44px;
  min-width: 0;
  min-height: 0;
  padding: 0;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 50%;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  cursor: pointer;
}

.filter-bar .filter-bar__clear:hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
  color: var(--color-text);
}

/* Shown only while the field holds text; no script involved. */
.filter-bar__search input:placeholder-shown ~ .filter-bar__clear {
  display: none;
}

.filter-bar__meta {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  white-space: nowrap;
  margin-left: auto;
}

.filter-bar__count {
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  white-space: nowrap;
}

/* With chips the segments carry the counts, so the count sentence stays
 * only for assistive technology (role="status"): the visually-hidden
 * recipe, never display: none. */
.filter-bar--chips .filter-bar__meta {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Segmented chips. The group is hidden by .js-gate without a script
 * (they cannot filter anything then); --js-gate-display lets the gate
 * reveal it as a flex row. */
.filter-bar__chips {
  --js-gate-display: flex;
  flex: 1 1 200px;
  min-width: 0;
  gap: 2px;
  margin: 0;
  padding: 3px;
  border: none;
  border-radius: var(--radius-card);
  background: var(--color-secondary);
  box-shadow: inset 0 0 0 1px var(--color-border);
}

/* min-content keeps a label whole: a segment grows to its text instead of
 * clipping it, which would push the visible text off the segment's centre. */
.filter-chip {
  position: relative;
  flex: 1 1 0;
  min-width: min-content;
  cursor: pointer;
}

.filter-chip__box {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 0 var(--space-sm);
  border-radius: var(--radius-control);
  transition: background var(--motion-fast), box-shadow var(--motion-fast);
}

.filter-chip__count {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.filter-chip__text {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1.3;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.filter-chip:hover .filter-chip__box {
  background: color-mix(in srgb, var(--color-dominant) 55%, transparent);
}

.filter-chip input:checked + .filter-chip__box {
  background: var(--color-dominant);
  box-shadow: inset 0 0 0 1px var(--color-border), var(--shadow-card);
}

.filter-chip input:checked + .filter-chip__box .filter-chip__count,
.filter-chip input:checked + .filter-chip__box .filter-chip__text {
  color: var(--color-text);
}

.filter-chip input:focus-visible + .filter-chip__box {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.filter-chip.is-zero .filter-chip__count {
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

.filter-chip--current .filter-chip__box {
  background: var(--color-dominant);
  box-shadow: inset 0 0 0 1px var(--color-border), var(--shadow-card);
}

.filter-chip--current .filter-chip__text {
  color: var(--color-text);
}

/* Phone, scripts on: the search collapses to a 48px icon button beside
 * the chips and opens over the whole row on focus or while it holds
 * text (list-filter.js sets data-filter-active), pushing the chips to
 * the next line. Without a script the plain full-width field applies. */
@media (max-width: 959.98px) {
  .js .filter-bar--chips .filter-bar__search {
    flex: 0 0 48px;
    order: 2;
  }

  .js .filter-bar--chips .filter-bar__search input {
    padding: 0;
    cursor: pointer;
  }

  .js .filter-bar--chips .filter-bar__search input::placeholder {
    color: transparent;
  }

  .js .filter-bar--chips .filter-bar__search:focus-within,
  .js .filter-bar--chips[data-filter-active] .filter-bar__search {
    flex: 1 0 100%;
    order: 0;
    max-width: none;
    /* Declared on the open state only: a transition runs with the
     * destination state's property, so opening animates while the
     * collapse the script's own .js class causes at load does not. */
    transition: flex-basis var(--motion-fast) ease;
  }

  .js .filter-bar--chips .filter-bar__search:focus-within input,
  .js .filter-bar--chips[data-filter-active] .filter-bar__search input {
    padding: 0 52px 0 46px;
    cursor: text;
  }

  .js .filter-bar--chips .filter-bar__search:focus-within input::placeholder,
  .js .filter-bar--chips[data-filter-active] .filter-bar__search input::placeholder {
    color: color-mix(in srgb, var(--color-text) 55%, transparent);
  }
}

@media (min-width: 960px) {
  .filter-bar--chips {
    flex-wrap: nowrap;
    gap: var(--space-md);
  }

  .filter-bar--chips .filter-bar__chips {
    flex: 0 0 auto;
  }

  .filter-bar--chips .filter-chip {
    flex: 0 0 auto;
  }

  .filter-bar--chips .filter-chip__box {
    flex-direction: row-reverse;
    gap: var(--space-sm);
    min-height: 44px;
    padding: 0 var(--space-md);
  }

  .filter-bar--chips .filter-bar__search {
    flex: 0 1 380px;
    margin-left: auto;
    order: 2;
  }
}

/* The empty state: a soft card with a search glyph in a halo and its
 * own Clear action. Sets no display, so [hidden] still hides it. */
.empty-state--filter {
  padding: var(--space-lg) var(--space-md);
  border-radius: var(--radius-card);
  background: var(--color-secondary);
}

.empty-state--filter p {
  margin: 0 auto var(--space-xs);
  max-width: 36ch;
}

.empty-state--filter button {
  margin-top: var(--space-sm);
}

.empty-state__halo {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-sm);
  border-radius: 50%;
  background: var(--color-dominant);
  box-shadow: var(--shadow-card);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Empty state */
.empty-state {
  padding: var(--space-lg);
  text-align: center;
}

.empty-state--compact {
  padding: 0;
  text-align: left;
}

/* A compact empty state that sits directly in a page column rather than inside a
 * card or row keeps the card's inner padding so it does not touch the viewport edge. */
.status-page > .empty-state--compact,
.page-content > .empty-state--compact {
  padding: var(--space-md);
}

.empty-state--compact .empty-state__heading {
  margin: 0 0 var(--space-xs);
  font-weight: var(--weight-semibold);
}

.empty-state--compact .empty-state__body {
  margin: 0;
}

/* Native <details>/<summary>, first use in this file. Both marker rules
 * below are required: Safari supports only the -webkit- form on
 * <summary> specifically, unlike <li>. */
summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  color: var(--color-accent);
}

summary::marker {
  color: var(--color-accent);
}

summary::-webkit-details-marker {
  color: var(--color-accent);
}

/* content: "" plus two rotated borders, not a text glyph: a flex
 * container generates no native ::marker at all, and a glyph's
 * rendering varies by font. Covered by the global prefers-reduced-motion
 * block already; no per-rule block needed. */
summary::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-right: var(--space-sm);
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .15s ease;
}

/* Child combinator, not descendant: a nested <details> must never
 * rotate its parent's own marker. (0,1,2) against the (0,0,2) base. */
details[open] > summary::before {
  transform: rotate(45deg);
}

.readings-disclosure {
  margin-top: var(--space-md);
}

/* min-width: 0 must be first and is load-bearing: a grid item's default
 * min size is its content's intrinsic width, and a .data-table inside
 * (min-width: max-content) would otherwise stretch this tile's whole
 * grid track and force horizontal scroll on the page. */
/* Hairline-at-rest, shadow-on-hover; the two states are mutually
 * exclusive, never additive. Status modifiers below use the doubled
 * (0,2,0) form: a single-class (0,1,0) modifier ties the hover rule's
 * own (0,2,0) border-color: transparent shorthand and loses to it. */
.stat-tile {
  min-width: 0;
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-top: 3px solid var(--color-accent);
  border-radius: var(--radius-card);
  box-shadow: none;
  transition: box-shadow .15s, transform .15s;
}

/* :focus-within alongside :hover: several tiles hold focusable children
 * (chart hit targets) and need the same lift for keyboard users.
 * border-inline-color/border-block-end-color (not the four-edge
 * shorthand) spare the top rail, so a status/accent modifier survives
 * hover. :not(.frame-strip) excludes the control strip, which must not
 * lift just because a button inside it is hovered. */
.stat-tile:not(.frame-strip):hover, .stat-tile:not(.frame-strip):focus-within {
  border-inline-color: transparent;
  border-block-end-color: transparent;
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-1px);
}

.stat-tile.stat-tile--ok {
  border-top-color: var(--color-status-ok);
}

.stat-tile.stat-tile--warn {
  border-top-color: var(--color-status-warn);
}

.stat-tile.stat-tile--error {
  border-top-color: var(--color-status-error);
}

.stat-tile.stat-tile--accent {
  border-top-color: var(--color-accent);
}

.stat-tile__caption {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0 0 var(--space-sm);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* A display: none sprite container still resolves <use> references —
 * this hides the definitions without disabling them. */
.icon-defs {
  display: none;
}

.icon {
  width: 20px;
  height: 20px;
  flex: none;
  vertical-align: middle;
}

.stat-tile__icon {
  color: var(--color-accent);
}

.stat-tile--ok .stat-tile__icon {
  color: var(--color-status-ok);
}

.stat-tile--warn .stat-tile__icon {
  color: var(--color-status-warn);
}

.stat-tile--error .stat-tile__icon {
  color: var(--color-status-error);
}

.stat-tile__value {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
}

/* Not named .battery-readout__detail: that substring collides with
 * BATTERY_READOUT_ID and would false-positive existing "no battery
 * readout" regression guards, since this tile renders unconditionally. */
.stat-tile__meta {
  margin: var(--space-xs) 0 0;
}

/* font-weight: inherit is required: .mono targets the <span> element
 * directly and beats an equal-specificity class regardless of source
 * order, leaving Emphasis weight inert otherwise. Must stay first in
 * this list — a harness guard locates the rule by that literal. */
.stat-tile__value .mono,
.battery-readout .mono {
  font-weight: inherit;
}

/* 70% muted strength, matching .section-caption/.data-table th. This
 * pair (light #5D5E62 on #FFFFFF, dark #AFB2B6 on #151922) is pinned in
 * companion/test_contrast_check.py's live_pairs. */
.battery-readout__detail {
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* No bare `svg { width:100%; height:auto }` rule here: that element
 * selector (0,1,1) would beat .icon's own sizing (0,1,0) regardless of
 * source order and stretch every icon to fill its tile (a real defect
 * found on real-device testing). */

/* No hover lift/transform here, unlike .stat-tile: this card holds an
 * interactive chart, and lifting the container while the pointer aims
 * at a small hit target would be actively unhelpful. */
.battery-trend-section {
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: none;
  min-width: 0;
  margin-bottom: var(--space-2xl);
}

.battery-trend-section:hover,
.battery-trend-section:focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

/* (0,2,1) specificity, deliberately: out-specifies any bare
 * .sparkline__canvas class rule, since the canvas size must be declared
 * HERE ONLY. No viewBox: every point battery_sparkline_svg() emits is a
 * PERCENTAGE OF THIS HEIGHT, so it must never vary inside a media query. */
.battery-trend-section svg:not(.icon) {
  width: 100%;
  height: 160px;
  overflow: visible;
  display: block;
}

/* margin-bottom is the section-to-section gap AFTER this whole grid,
 * distinct from the gap above (between this grid's own tiles) and from
 * .page-section's own --space-lg card-to-card gap. A harness check in
 * test_status_pages.py pins both roles as a pair. */
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
  /* align-items: stretch here is unrelated to the OTHER cross-axis
   * declaration in .dashboard-shell below: the sticky sidebar there
   * depends on THAT one staying non-stretched. Do not conflate them. */
  align-items: stretch;
}

.page-section {
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: none;
}

.page-section:hover,
.page-section:focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

/* Doubled selector (0,2,0), not a single-class modifier (0,1,0): the
 * base rule's own border shorthand resets border-top-color, so (0,1,0)
 * would tie and lose to source order. Must stay declared AFTER both
 * hover rules above, which are also (0,2,0) and clear the same property. */
.battery-trend-section.battery-trend-section--ok,
.page-section.page-section--ok {
  border-top: 3px solid var(--color-status-ok);
}

.battery-trend-section.battery-trend-section--warn,
.page-section.page-section--warn {
  border-top: 3px solid var(--color-status-warn);
}

.battery-trend-section.battery-trend-section--error,
.page-section.page-section--error {
  border-top: 3px solid var(--color-status-error);
}

.page-section--nested > h2,
.battery-trend-section > h2,
.theme-status--nested > h2 {
  font-family: var(--font-ui);
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  letter-spacing: normal;
  margin-bottom: var(--space-md);
}

/* Both the child combinator and the .text-body qualifier are
 * load-bearing: the combinator excludes nested .empty-state paragraphs,
 * the qualifier excludes .battery-readout's own <p>, which already owns
 * its own margin. test_status_pages.py pins the allowlist this depends on. */
.page-section--nested > p.text-body,
.battery-trend-section > p.text-body {
  margin: 0 0 var(--space-sm);
}

/* The unidentified-airlines strip: an auto-fill grid with a 200px column
 * floor, collapsing to fewer columns automatically with no extra
 * breakpoints. */
.illustration-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-lg);
}

/* Below 960px: minmax(0, 1fr) (never bare 1fr) lets a fixed two-column
 * template shrink below the placeholder's intrinsic width — the
 * auto-fill default collapses to ONE column inside a 342px content
 * column, ballooning page height. */
@media (max-width: 959.98px) {
  .illustration-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Hairline-at-rest, shadow-on-hover — do not restore a resting shadow. */
.airline-card {
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: none;
  padding: var(--space-sm);
}

a.airline-card {
  display: block;
  color: inherit;
  text-decoration: none;
}

.airline-card:hover,
.airline-card:focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

/* The dashed frame standing in for a missing picture: on a gap card, and
 * on an airline row whose artwork is missing. Its own standalone rule
 * body — do not merge it into a shared selector list. */
.airline-card__placeholder {
  aspect-ratio: 450 / 132;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-canvas);
  margin-bottom: var(--space-sm);
  display: block;
}

/* Emphasis role, not a new type role. */
.airline-card__name {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-xs);
}

.airline-card__chip {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Interactive battery-trend chart hit targets and readout line. */

/* auto first column: the browser measures the real rendered width of
 * the widest Y-axis label and reserves exactly that, replacing a
 * Python-side width estimate. */
.sparkline {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-sm);
  row-gap: var(--space-xs);
}

.sparkline__y {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-end;
}

.sparkline__x {
  grid-column: 2;
  display: flex;
  justify-content: space-between;
}

/* 0.14 fill-opacity is a measured floor: the lowest opacity that
 * visibly separates the area from the card in both themes (dark mode is
 * the binding case). currentColor keeps it in step with the line's own
 * colour; no gradient, since the emitter never emits a url() reference. */
.sparkline-area {
  fill: currentColor;
  fill-opacity: 0.14;
}

/* One <line> per point pair, not a <polyline>: percentages are not
 * permitted inside a polyline's points list. */
.sparkline-line {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
}

.sparkline-axis {
  fill: var(--color-border);
}

/* Never receives pointer events, or it would steal them from the
 * larger .sparkline-hit target painted at the same coordinates. */
.sparkline-dot {
  fill: currentColor;
  pointer-events: none;
}

/* r is a presentation attribute from a Python-side radius constant; a
 * CSS declaration of any specificity would beat and flatten it. No
 * pointer-events here either — it's inherited from the sibling
 * .sparkline-dot; this mark simply paints on top of it (measured). */
.sparkline-mark {
  fill: currentColor;
  pointer-events: none;
}

.sparkline-threshold {
  fill: var(--color-status-warn);
}

/* grid-column: 1 / -1 is load-bearing, not tidy: without it the label
 * auto-places into the auto-sized Y-axis column, which then grows to
 * fit a sentence — measured: the canvas dropped from 230px to ~109px
 * inside the same 278px grid, with no other visible signal. */
.sparkline__legend {
  grid-column: 1 / -1;
}

.sparkline-legend {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 10px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.sparkline-swatch {
  width: 12px;
  height: 1px;
  background: var(--color-status-warn);
}

/* A transparent SVG fill is still hit-testable, which is why this
 * enlarged circle works as the real tap/click target. */
.sparkline-hit {
  fill: transparent;
  cursor: pointer;
}

.sparkline-hit:hover,
.sparkline-hit--active {
  fill: var(--color-text);
  fill-opacity: 0.15;
}

.sparkline-hit:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* HTML <span>, not SVG text: color sets the ink, not fill. 10px matches
 * this file's sub-scale exception tier. line-height: 1.2 is required,
 * not decorative: health_page.py's vertical-inset math assumes this
 * exact, non-inherited line-height. */
.sparkline-axis-label {
  font-size: 10px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Shared drawing vocabulary for this file's server-rendered SVG charts
 * (battery ring, area chart, day timeline, regularity grid), emitted
 * from companion/draw.py's own class constants. test_companion_app.py
 * asserts every class draw.py can emit resolves to a selector here. */

/* minmax(0, 1fr) on the canvas column is load-bearing: a plain 1fr
 * takes its min-content width as a floor, which would grow a
 * horizontal scrollbar at the 360px minimum viewport. */
.drawing {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-sm);
  row-gap: var(--space-xs);
}

.drawing__y {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-end;
}

.drawing__x {
  grid-column: 2;
  display: flex;
  justify-content: space-between;
}

/* An <svg> with no size attribute or CSS rule renders at the SVG
 * default 300x150. Never vary --drawing-canvas-height inside a media
 * query: every percentage y-coordinate a drawing emits is relative to
 * this height, so a responsive height would silently move every point. */
.drawing__canvas {
  display: block;
  width: 100%;
  height: var(--drawing-canvas-height, 160px);
  overflow: visible;
}

/* Deliberately NO width/height: draw.unit_canvas() emits intrinsic
 * width/height attributes in CSS pixels; width: 100% here would
 * override them and letterbox the aspect-locked shape. */
.drawing__figure {
  display: block;
}

/* Filled rects, not stroked lines: a rect can pair a percentage
 * position with an absolute size, which a stroked line cannot, and
 * needs no stroke-centring math. */
.drawing-axis {
  fill: var(--color-border);
}

/* fill: none is explicit, not tidy: without it an unfilled shape falls
 * back to the SVG default fill (black), correct in one theme only. */
.drawing-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.drawing-mark {
  fill: currentColor;
}

.drawing-axis-label {
  font-size: 10px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.drawing--ok {
  color: var(--color-status-ok);
}

.drawing--warn {
  color: var(--color-status-warn);
}

.drawing--error {
  color: var(--color-status-error);
}

/* Three tokens for three meanings: the frame is the day itself, the
 * shaded span is a HONOURED window (structural ink, never a status
 * colour), a mark is something that HAPPENED. 40% opacity on the frame
 * is measured to stay distinguishable from the span in both themes. */
.drawing-band {
  fill: color-mix(in srgb, var(--color-border) 40%, transparent);
}

.drawing-band-span {
  fill: var(--color-border);
}

.drawing-band-mark {
  fill: currentColor;
}

/* A fourth state (--none), beyond the ok/warn/error trio: it carries
 * the ABSENCE of a measured interval, never a verdict, so it uses the
 * structural --color-border token rather than borrow a verdict colour.
 * test_browser_ux.py asserts all four stay pairwise distinguishable. */
.drawing-cell {
  fill: currentColor;
}

.drawing-cell--on-cadence {
  color: var(--color-status-ok);
}

.drawing-cell--late {
  color: var(--color-status-warn);
}

.drawing-cell--missing {
  color: var(--color-status-error);
}

.drawing-cell--none {
  color: var(--color-border);
}

/* Neither rule declares stroke-width, r, cx or cy: draw.ring_gauge()
 * emits them as presentation attributes scaled per call site, and a
 * CSS declaration of any specificity beats a presentation attribute —
 * adding one here would flatten both sizes to one thickness. */

/* --color-border, deliberately NOT currentColor: the track must stay
 * structural even when the value arc's status modifier turns amber or
 * rose, or the ring would read as entirely full in a warning state. */
.drawing-ring-track {
  fill: none;
  stroke: var(--color-border);
}

/* stroke-linecap: butt is explicit even though it's the SVG initial
 * value: a round cap adds half a stroke-width of ink at each arc end,
 * so at the small size a 5% reading would draw as ~12% of the circle. */
.drawing-ring-value {
  fill: none;
  stroke: currentColor;
  stroke-linecap: butt;
}

/* The arc gauge (draw.arc_gauge()). Round caps on both strokes: the
 * emitter shortens the value dash by one stroke width to pay for them.
 * The track is a faint wash of the text colour rather than the border
 * token, which disappears on the tinted header in the dark theme. Like
 * the ring, no stroke-width here: it is a per-size presentation
 * attribute. */
.drawing-arc-track {
  fill: none;
  stroke: color-mix(in srgb, var(--color-text) 10%, transparent);
  stroke-linecap: round;
}

.drawing-arc-value {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
}

/* width/height: auto restores draw.unit_canvas()'s own intrinsic
 * attributes over the chart's own svg:not(.icon) sizing rule.
 * :not(.icon) filters nothing here — it exists only to add a class so
 * this selector outranks that rule on specificity, not source order. */
.battery-trend-section svg.drawing__figure:not(.icon) {
  width: auto;
  height: auto;
}

/* Fixed-width reveal text (4-digit mV + ISO timestamp, monospace) means
 * a reveal can never change where the line wraps, so min-height in em
 * prevents any layout jump. The semibold weight here must stay declared
 * after .mono in source order to win by source order, not specificity
 * — test_status_pages.py's cross-file guard pins that. */
/* align-items: center keeps the aspect-locked figure at its own size:
 * a flex row's default stretch would letterbox it to the row's full
 * height. */
.battery-readout-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}

.battery-readout-row > .battery-readout {
  margin-bottom: 0;
}

.battery-readout {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  min-height: 1.4em;
  margin: 0 0 var(--space-sm);
}

/* Percentage / Voltage control above the battery chart: a two-button
 * group of native buttons. The selected one is marked by weight, a
 * stronger border and a tinted wash, never by colour alone. Shipped
 * [hidden] and revealed by battery-trend.js, so the explicit [hidden]
 * rule is required: this display would otherwise beat the UA rule. */
.battery-unit {
  display: inline-flex;
  gap: var(--space-xs);
  margin: 0 0 var(--space-sm);
}

.battery-unit[hidden],
.battery-chart__view[hidden] {
  display: none;
}

.battery-unit__option {
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  font-weight: var(--weight-regular);
}

.battery-unit__option[aria-pressed="true"] {
  font-weight: var(--weight-semibold);
  border-color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 12%, transparent);
}

/* The raw-readings action is secondary: muted, regular weight and
 * label-sized, still a full-height native summary. */
.readings-disclosure > summary {
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Health rows: one hairline card, a group band per section, one native
 * <details> row per subsystem. The card is one width at every viewport
 * (no max-width cap), and nothing on it lifts: the rows are read-only, so
 * the only pointer response is a faint wash on the summary, which is the
 * one control a row has. Colour comes from the three status tokens and
 * never carries the state alone (each state has its own icon shape and a
 * visually hidden word). The grid is on the 8px scale: a 32px icon, a
 * 16px gutter, a 208px name column, a 64px row. */
.health-rows {
  /* The wide five-column row applies when the card itself is wide, not the
   * viewport: a half-width card on a desktop page keeps the stacked row. */
  container: health-rows / inline-size;
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  margin-bottom: var(--space-lg);
}

/* The h2 keeps the page outline's two sections and the #screen /
 * #server-data link targets; this rule gives it the label voice and takes
 * it off the shared serif heading rule. */
.health-rows__group {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-secondary);
  border-bottom: 1px solid var(--color-border);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  line-height: 1.3;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.health-rows__group:first-child {
  border-radius: calc(var(--radius-control) - 1px) calc(var(--radius-control) - 1px) 0 0;
}

.health-row {
  --row-tone: color-mix(in srgb, var(--color-text) 50%, transparent);
  border-bottom: 1px solid var(--color-border);
}

.health-row:last-child {
  border-bottom: 0;
}

.health-row[data-state="ok"] {
  --row-tone: var(--color-status-ok);
}

.health-row[data-state="warn"] {
  --row-tone: var(--color-status-warn);
}

.health-row[data-state="error"] {
  --row-tone: var(--color-status-error);
}

/* A phone row stacks name over verdict over value beside the icon; the
 * chevron is the shared summary marker, moved to the trailing edge. */
.health-row__summary {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) 16px;
  grid-template-areas:
    "icon name chevron"
    "icon verdict chevron"
    "icon value chevron";
  column-gap: var(--space-md);
  row-gap: var(--space-xs);
  align-items: center;
  min-height: 64px;
  padding: var(--space-md);
  color: var(--color-text);
  font-weight: var(--weight-regular);
}

.health-row__summary::before {
  grid-area: chevron;
  justify-self: end;
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
}

.health-row__summary:hover {
  background: color-mix(in srgb, var(--color-text) 3.5%, transparent);
}

.health-row__summary:focus-visible {
  outline-offset: -2px;
}

.health-row:last-child:not([open]) > .health-row__summary {
  border-radius: 0 0 calc(var(--radius-control) - 1px) calc(var(--radius-control) - 1px);
}

.health-row__icon {
  grid-area: icon;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--row-tone) 16%, transparent);
  color: var(--row-tone);
}

.health-row__name {
  grid-area: name;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  line-height: 1.3;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.health-row__verdict {
  grid-area: verdict;
  min-width: 0;
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  line-height: 1.3;
}

.health-row__value {
  grid-area: value;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  min-width: 0;
  font-size: var(--font-label-size);
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.health-row__ring {
  display: inline-grid;
  flex: none;
  color: var(--color-text);
}

.health-row__body {
  display: grid;
  gap: var(--space-md);
  min-width: 0;
  padding: 0 var(--space-md) var(--space-md);
}

/* min-width: 0 on every direct child: a grid item's automatic minimum is
 * its content's, and a table or chart inside would otherwise stretch the
 * row past the card. */
.health-row__body > * {
  min-width: 0;
  margin: 0;
}

.health-row__facts {
  display: grid;
  gap: var(--space-xs);
}

.health-row__facts > div {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.health-row__facts dt {
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.health-row__facts dd {
  margin: 0;
}

.health-row__note {
  margin: 0;
  font-size: var(--font-label-size);
  line-height: 1.4;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.health-row__outcomes {
  display: grid;
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.health-row__outcome {
  display: flex;
  align-items: center;
  margin: 0;
}

.health-row__outcome .mono {
  margin-left: var(--space-sm);
  font-weight: var(--weight-semibold);
}

/* The battery chart's wrapper keeps its class (the chart rules and
 * battery-trend.js key off it) but is not a card here: the row is the
 * container. Declared after the card's own hover rule at equal
 * specificity, so it wins by position. */
.battery-trend-section.battery-trend-section--embedded,
.battery-trend-section.battery-trend-section--embedded:hover,
.battery-trend-section.battery-trend-section--embedded:focus-within {
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* One elevation rule on this page: a hairline at rest and nothing else.
 * The unresolved-prefix card keeps its filter and links, but the
 * hover reveal that suits a browsable card is noise on a status page,
 * and it made neighbouring cards look inconsistent. */
.status-page .page-section:hover,
.status-page .page-section:focus-within {
  border-color: var(--color-border);
  box-shadow: none;
}

@container health-rows (min-width: 600px) {
  .health-row__summary {
    grid-template-columns: 32px 208px minmax(0, 1fr) auto 16px;
    grid-template-areas: "icon name verdict value chevron";
    padding: 0 var(--space-lg);
  }

  .health-rows__group {
    padding: var(--space-sm) var(--space-lg);
  }

  .health-row__value {
    justify-content: flex-end;
    text-align: right;
  }

  /* Indented to the name column, so the evidence reads as belonging to
   * the row's text and not to its icon. */
  .health-row__body {
    padding: 0 var(--space-lg) var(--space-lg)
      calc(var(--space-lg) + 32px + var(--space-md));
  }

  .health-row__facts > div {
    grid-template-columns: 160px minmax(0, 1fr);
    column-gap: var(--space-md);
    align-items: baseline;
  }
}

/* Desktop dashboard shell, this file's second and last breakpoint.
 * layout.py renders both nav copies (mobile dropdown + sidebar) into
 * every response since a stdlib server has no viewport info; display:
 * none here removes the losing copy from flow, tab order and a11y tree. */
@media (min-width: 960px) {
  .dashboard-shell {
    display: grid;
    grid-template-columns: calc(240px + var(--space-md)) minmax(0, 1fr);
    column-gap: var(--space-xl);
    align-items: start;
  }

  /* position: sticky needs .dashboard-shell's align-items: start above
   * — a stretched grid item is already sized to the full row height and
   * cannot visibly stick. */
  .dashboard-sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    min-height: calc(100vh - var(--space-md) * 2);
    margin: var(--space-md) 0 var(--space-md) var(--space-md);
    padding: var(--space-lg) var(--space-md);
    background: var(--color-secondary);
    border: 1px solid var(--color-border);
    border-radius: calc(var(--radius-glass) + 4px);
    box-shadow: var(--shadow-glass);
    position: sticky;
    top: var(--space-md);
  }

  .sidebar-footer {
    margin-top: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
  }

  .site-header {
    display: none;
  }

  .dashboard-main {
    min-width: 0;
    width: 100%;
    max-width: min(1440px, 100%);
    margin: 0 auto;
    padding: var(--space-2xl) var(--space-3xl);
  }

  .dirty-bar {
    position: fixed;
    bottom: var(--space-lg);
    left: calc(240px + var(--space-md) + var(--space-xl) + var(--space-md));
    right: var(--space-md);
    width: fit-content;
    max-width: calc(min(1440px, 100%) - var(--space-md) * 2);
    margin: 0 auto;
    z-index: 30;
  }

  .history-cards {
    display: none;
  }

  .history-cards ~ .data-table-wrap {
    display: block;
  }

  .data-cards {
    display: none;
  }

  .data-cards ~ .data-table-wrap {
    display: block;
  }

  /* opacity: 0 + pointer-events: none, never visibility: hidden or
   * display: none: those remove the element from the tab order and a11y
   * tree, so a keyboard user could never focus it to trigger the hover-
   * reveal state below. [data-copy-value] excludes the always-visible
   * View-panel trigger, which shares .copy-btn but not this attribute. */
  .data-table tbody tr [data-copy-value] {
    opacity: 0;
    pointer-events: none;
    transition: opacity .15s;
  }

  .data-table tbody tr:hover [data-copy-value],
  .data-table tbody tr:focus-within [data-copy-value] {
    opacity: 1;
    pointer-events: auto;
  }

  /* --space-lg (24px) is within this file's target 20-24px range and
   * already on the token scale — no new spacing literal. Widening
   * .battery-trend-section's padding here is safe: the sparkline's
   * height is fixed elsewhere in this file; only its container narrows. */
  .page-section,
  .theme-status,
  .battery-trend-section {
    padding: var(--space-lg);
  }

  /* 60ch bounds the caption/value to a readable line length rather than
   * letting the tile stretch the grid's full ~1040px column. */
  .stat-tile__caption,
  .stat-tile__value {
    max-width: 60ch;
  }

  .section-intro {
    flex-direction: row;
    align-items: baseline;
    gap: var(--space-sm);
  }
}

.login-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--color-dominant);
  /* Hairline-at-rest, shadow-on-hover — deliberate, not an oversight:
   * every other card in this file follows the same contract. */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: none;
  padding: var(--space-xl);
}

.login-card:hover,
.login-card:focus-within {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
}

/* The Sign in link carries the declarations of a.calendar-manage--primary:
 * it is the page's one primary action when Auth0 is configured. A link,
 * not a button, so the redirect is not subject to the CSP form-action. */
.login-card a.login-signin {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 100%;
  min-height: 44px;
  margin-top: var(--space-md);
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  background: var(--color-accent);
  color: var(--color-on-accent);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
}

.login-card a.login-signin:hover {
  background: var(--color-accent-hover);
}

/* A standalone text link inside the card keeps a 44px hit height. */
.login-card .login-card__link {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: var(--space-md);
}

.login-card .toast--docked {
  margin-top: var(--space-md);
}

/* The wordmark link of a plain user's shell inherits the brand row's
 * layout and the page text colour. */
.brand__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 44px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* The legacy-access badge sits on its own line above Sign out. */
.logout-form .banner__pill {
  display: flex;
  width: fit-content;
  margin-bottom: var(--space-sm);
}

/* A long email wraps instead of widening the Account card. */
.account-identity .data-card__value {
  min-width: 0;
  overflow-wrap: anywhere;
  text-align: right;
}

/* box-shadow at rest is this file's one hairline-at-rest exception: a
 * <dialog> is a floating modal, not a page-flow card. color is set
 * explicitly because <dialog> promoted via showModal() does not reliably
 * inherit color from its ancestors in any tested browser. */
.lightbox {
  background: var(--color-dominant);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
  max-width: 480px;
  width: calc(100% - var(--space-lg) * 2);
  box-shadow: var(--shadow-card-hover);
  /* Fades/zooms in via @starting-style (Baseline 2024: Chrome 117,
   * Firefox 129, Safari 17.5); older browsers just open instantly. The
   * close is deliberately instant — an animated exit would keep a
   * closed dialog hit-testable and in the tab order meanwhile. */
  opacity: 0;
  transform: scale(0.96);
  transition: opacity var(--motion-fast) ease-out,
    transform var(--motion-fast) ease-out;
}

.lightbox[open] {
  opacity: 1;
  transform: none;
}

@starting-style {
  .lightbox[open] {
    opacity: 0;
    transform: scale(0.96);
  }
}

.lightbox__image:not([hidden]) {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-control);
  margin-bottom: var(--space-sm);
}

.lightbox__caption {
  margin: 0 0 var(--space-xs);
}

.lightbox__note {
  margin: var(--space-sm) 0 0;
}

.lightbox__note:empty {
  display: none;
}

.lightbox__heading:empty {
  display: none;
}

/* The sheet's heading takes the dialog's initial focus (tabindex -1, set by
   panel-lookup.js); it is not a control, so it carries no ring. */
.lightbox__heading:focus {
  outline: none;
}

.lightbox__manual-note:empty {
  display: none;
}

.lightbox::backdrop {
  background: var(--color-scrim);
}

.lightbox__actions button {
  min-height: 44px;
}

.lightbox__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

/* Equal (0,1,0) specificity to .lightbox, so this must (and does) come
 * later in source order to win. The 450px cap is pinned to
 * illustration_normalize.ILLUSTRATION_TARGET_WIDTH by a harness check;
 * do not bump it "to look bigger" without updating that constant. */
.lightbox--wide {
  max-width: 450px;
}

/* width: auto, not the base rule's width: 100%: combined with the
 * max-height cap below, 100% + height: auto would squash the image's
 * aspect ratio once it hits the height cap. */
.lightbox--wide .lightbox__image {
  width: auto;
  max-width: 100%;
  max-height: 60vh;
  margin-left: auto;
  margin-right: auto;
}

.lightbox__replace:not([hidden]),
.lightbox__resolve-name:not([hidden]),
.lightbox__delete:not([hidden]) {
  display: block;
  padding-top: var(--space-md);
  min-width: 0;
}

.lightbox__replace-zone:not([hidden]),
.resolve-upload-zone:not([hidden]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-lg) var(--space-md);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-control);
  text-align: center;
  min-width: 0;
  background: var(--color-secondary); /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--color-secondary) 55%, var(--color-dominant));
}

.lightbox__replace-zone .lightbox__replace-icon,
.resolve-upload-zone .lightbox__replace-icon {
  color: var(--color-overlay-icon); /* fallback for browsers without color-mix() */
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

/* margin-bottom: 0 is load-bearing: the global label rule's own
 * margin-bottom: var(--space-sm) would otherwise stack on this zone's
 * flex gap. */
.lightbox__replace-zone label,
.resolve-upload-zone label {
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  color: var(--color-text);
  margin-bottom: 0;
}

.lightbox__replace-hint {
  margin: 0;
  font-size: 13px;
  color: var(--color-overlay-text); /* fallback for browsers without color-mix() */
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* min-height: 44px stays inherited from the global input rule — do not
 * zero it, this file's touch-target floor register keeps it. */
.lightbox__replace-zone input[type="file"],
.resolve-upload-zone input[type="file"] {
  background: transparent;
  border: 0;
  padding: 0;
  min-width: 0;
  max-width: 100%;
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--color-overlay-text); /* fallback for browsers without color-mix() */
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

  /* 30px meets this file's button-density register; WCAG 2.5.8 (AA,
   * 24px) still holds. Resting fill mixes toward --color-text rather
   * than a flat --color-secondary because --color-dominant is darker
   * than --color-secondary in dark mode — the inverse of light mode. */
.lightbox__replace-zone input[type="file"]::file-selector-button,
.resolve-upload-zone input[type="file"]::file-selector-button {
  height: 30px;
  padding: 0 12px;
  margin-right: var(--space-sm);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
  border-radius: var(--radius-control);
  background: var(--color-secondary); /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--color-text) 4%, var(--color-secondary));
  border: 1px solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}

.lightbox__replace-zone input[type="file"]::file-selector-button:hover,
.resolve-upload-zone input[type="file"]::file-selector-button:hover {
  background: var(--color-secondary); /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--color-text) 7%, var(--color-secondary));
}

/* Drag-and-drop affordance over the upload zones above. No :hover rule
 * anywhere: the drag state is carried by [data-upload-drop-active],
 * which panel-lookup.js sets/removes — a hover-only state is invisible
 * on a phone. */
.upload-drop {
  --js-gate-display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border-radius: var(--radius-control);
}

.upload-drop[data-upload-drop-active] {
  background: var(--color-secondary);
}

/* Restated against tokens rather than reusing .lightbox__replace-hint,
 * so test_status_pages.py's replace-zone contract still matches exactly
 * one .lightbox__replace-hint element per zone. */
.upload-drop__note {
  margin: 0;
  font-size: 13px;
  color: var(--color-text); /* fallback for browsers without color-mix() */
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* aspect-ratio reads --upload-preview-ratio (written inline by the
 * server from illustration_normalize's target dimensions) with
 * deliberately NO fallback value — a fallback would mask the inline
 * property's own disappearance instead of surfacing it. */
.upload-drop__preview {
  position: relative;
  margin: 0;
  width: 100%;
  aspect-ratio: var(--upload-preview-ratio);
  overflow: hidden;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-dominant);
}

.upload-drop[data-upload-drop-active] .upload-drop__preview {
  border-style: solid;
  border-color: var(--color-text);
}

/* object-fit: contain shows the whole chosen image: the server may crop
 * differently and this preview is not the authority. */
.upload-drop__image:not([hidden]) {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* The hint lives inside the frame, centred, until a chosen image
 * fills it. */
.upload-drop__preview .upload-drop__note {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm);
  text-align: center;
}

.upload-drop__image:not([hidden]) ~ .upload-drop__note {
  display: none;
}

.upload-drop__message {
  margin: 0;
  font-size: 13px;
  font-weight: var(--weight-semibold);
  color: var(--color-status-error);
}

.upload-drop__message:empty {
  display: none;
}

/* Bare button at (0,0,1) — not :not()-qualified, which would raise
 * specificity and override every class-scoped control that sizes
 * itself (theme segments, copy-btn, file-selector-button). Source
 * order (placed last) wins the cascade instead; a harness check pins it. */
@media (max-width: 959.98px) {
  button {
    height: 36px;
    font-size: 14px;
  }
}

/* The airport Search button must be type="submit" for the no-script GET,
 * which would paint it with the primary accent fill; this quiet override
 * (equal specificity to button[type="submit"], placed after it) keeps the
 * accent for the page's Save. min-height outranks the 30px and 36px
 * button heights at every width. */
button.detection-search {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  box-shadow: none;
  color: var(--color-text);
  min-height: 44px;
}

button.detection-search:hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
  border-color: color-mix(in srgb, var(--color-text) 14%, transparent);
  box-shadow: none;
}

/* Location and detection page. Without the map script it is one column in
 * the order the markup has: the panel (the mode switch, the runway choice or
 * the radius, the airport list, the fields, Save). With the map running
 * (html.map-ready) the map fills the page and everything else floats on it:
 * the one search field at the top, then on a phone the mode chips and a
 * compact bottom sheet, from 960px a card at the left holding the mode
 * switch and the sheet. The part of the map the script keeps clear when it
 * centres a place is the sheet (the card on a wide screen). */
.place-page {
  margin-bottom: var(--space-lg);
}

.place-stage {
  display: grid;
  gap: var(--space-md);
}

.place-stage > * {
  min-width: 0;
}

.place-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.place-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.place-sheet {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.place-sheet > *,
.place-card > * {
  margin: 0;
  min-width: 0;
}

.place-search__row button {
  white-space: nowrap;
}

/* The fallback explanation stays until place-map.js reports the map is
 * running; a failed script therefore falls back as well. */
.place-fallback {
  margin: 0;
}

.map-ready .place-fallback {
  display: none;
}

/* The mode switch: two segments in one track, each label centred. */
.place-modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xs);
  padding: var(--space-xs);
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.place-mode {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--space-md);
  border-radius: var(--radius-control);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  overflow-wrap: anywhere;
}

.place-mode__text {
  display: block;
  text-align: center;
}

.place-mode:has(input:checked) {
  background: var(--color-dominant);
  color: var(--color-accent);
  box-shadow: var(--shadow-card);
}

.place-mode:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.place-search {
  --js-gate-display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.place-search__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.place-search__row input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
}

.place-search .section-caption {
  margin: 0;
}

.place-locate {
  --js-gate-display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
}

/* The chosen mode decides which block the sheet shows. */
.place-stage:has(#place-mode-area:checked) .place-panel__runway,
.place-stage:not(:has(#place-mode-area:checked)) .place-panel__area {
  display: none;
}

/* The runway cards sit three to a row, as compact as the sheet needs. */
.place-sheet .detection-runways {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-sm);
}

.place-sheet .runway-card {
  padding: var(--space-sm);
  text-align: center;
}

.place-sheet .runway-card .runway-card__detail {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.place-sheet .runway-card .banner__pill {
  display: block;
  width: fit-content;
  margin: var(--space-xs) auto 0;
}

.place-panel__follow {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.place-details__close {
  align-items: center;
  min-height: 44px;
  margin-top: var(--space-sm);
}

/* The map is running: it fills the stage and everything else floats on it. */
.map-ready .place-page {
  margin: 0;
}

.map-ready .place-stage {
  position: relative;
  display: block;
  flex: 1 1 auto;
  min-height: 24rem;
  overflow: hidden;
  background: var(--color-secondary);
  --place-gutter: calc(var(--space-sm) + var(--space-xs));
  --place-search-bottom: calc(var(--place-gutter) + 54px);
}

.map-ready .place-map {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: auto;
  height: auto;
  min-height: 0;
  border: 0;
  border-radius: 0;
}

/* The one search field floats at the top of the map. Its label is for
 * assistive technology only. */
.map-ready .place-float {
  position: absolute;
  z-index: 3;
  top: var(--place-gutter);
  left: var(--place-gutter);
  right: var(--place-gutter);
}

.map-ready .place-search__row {
  padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card-hover);
}

.map-ready .place-search__row input[type="search"] {
  border: 0;
  background: transparent;
  box-shadow: none;
}

.map-ready .place-search__go {
  flex: none;
  width: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.map-ready .place-search__go .icon {
  display: block;
}

.map-ready .place-search label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The privacy line floats under the search field while it is in use, out of
 * the flow so nothing moves when the field loses focus; it gives way to the
 * results. */
.map-ready .place-search {
  position: relative;
}

.map-ready .place-search .section-caption {
  display: none;
}

.map-ready .place-results:has(.place-results__note) {
  pointer-events: none;
}

.map-ready .place-search:focus-within .section-caption {
  display: block;
  position: absolute;
  top: calc(100% + var(--space-xs));
  left: 0;
  right: 0;
  z-index: 3;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-card-hover);
}

.map-ready .place-float:has(.place-results:not([hidden])) .place-search .section-caption {
  display: none;
}

/* On a phone the chips sit under the search field: a list of results starts
 * below them. A one-line note lets taps through. */
.map-ready .place-results {
  margin: calc(44px + var(--space-md)) 0 0;
  max-height: 40dvh;
  overflow-y: auto;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-card-hover);
}

/* The overlay carries the card, the credits button and the locate button
 * above the map. It lets taps through; its parts take them. */
.map-ready .place-panel {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  gap: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
  padding-top: calc(var(--place-search-bottom) + var(--space-sm));
  pointer-events: none;
}

.map-ready .place-panel > *,
.map-ready .place-card > * {
  pointer-events: auto;
}

.map-ready .place-card {
  display: contents;
}

/* The mode chips: two pills under the search field on a phone. */
.map-ready .place-modes {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  display: flex;
  gap: var(--space-sm);
  margin-left: var(--place-gutter);
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}

.map-ready .place-mode {
  padding: 0 var(--space-md);
  border-radius: 22px;
  background: var(--color-dominant);
  box-shadow: var(--shadow-card-hover);
}

.map-ready .place-mode:has(input:checked) {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.map-ready .place-info {
  position: relative;
  grid-column: 2;
  grid-row: 1;
  display: block;
  margin-right: var(--place-gutter);
}

.map-ready .place-info__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-dominant) 88%, transparent);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
  color: color-mix(in srgb, var(--color-text) 80%, transparent);
}

.map-ready .place-info__button .icon {
  width: 18px;
  height: 18px;
}

.map-ready .place-credits {
  position: absolute;
  z-index: 4;
  top: calc(100% + var(--space-sm));
  right: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: min(20rem, calc(100vw - 2 * var(--place-gutter)));
  box-sizing: border-box;
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
}

.map-ready .place-credits[hidden] {
  display: none;
}

.map-ready .place-credits p {
  margin: 0;
}

.map-ready .place-credits a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.map-ready .place-credits__attribution {
  font-weight: var(--weight-semibold);
}

.map-ready .place-tools {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: end;
  align-self: end;
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  margin: 0 var(--place-gutter) var(--place-gutter) var(--place-gutter);
  pointer-events: none;
}

.map-ready .place-message {
  max-width: 16rem;
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-card-hover);
  color: var(--color-text);
  pointer-events: auto;
}

.map-ready .place-message[hidden] {
  display: none;
}

/* The sheet: a compact bottom sheet that never covers more than about 40% of
 * the viewport; it scrolls inside when it must. */
.map-ready .place-sheet {
  grid-column: 1 / -1;
  grid-row: 3;
  box-sizing: border-box;
  max-height: 40dvh;
  overflow-y: auto;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--color-dominant);
  border-radius: 20px 20px 0 0;
  box-shadow: var(--shadow-card-hover);
  pointer-events: auto;
}

.map-ready .place-hint,
.map-ready .place-picker,
.map-ready .place-radius > .section-caption,
.map-ready .place-sheet .place-radius label,
.map-ready .place-panel__area .place-subtitle {
  display: none;
}

.map-ready .place-sheet .place-chosen p {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm);
  margin: 0;
}

.map-ready .place-sheet .place-chosen__name {
  font-family: var(--font-serif);
  font-size: var(--font-body-size);
  font-weight: var(--weight-regular);
}

/* Save and Cancel live in the sheet; the count line is for assistive
 * technology only. */
.map-ready .place-sheet .dirty-bar {
  position: sticky;
  bottom: 0;
  flex-direction: row-reverse;
  justify-content: flex-start;
  flex-wrap: nowrap;
  gap: var(--space-md);
  width: auto;
  max-width: none;
  margin: 0;
  padding: var(--space-sm) 0 0;
  background: var(--color-dominant);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  z-index: auto;
}

.map-ready .place-sheet .dirty-bar[hidden] {
  display: none;
}

.map-ready .place-sheet .dirty-bar [data-dirty-count] {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.map-ready .place-sheet .dirty-bar button {
  min-height: 44px;
  padding: 0 var(--space-lg);
}

.map-ready .place-sheet .dirty-bar__cancel {
  background: none;
  border-color: transparent;
  color: var(--color-accent);
  text-decoration: underline;
}

/* The page title is the page's heading for assistive technology only: the
 * map carries the page. The flash banner stays in the flow. */
.map-ready .page-header {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The details (coordinates, label, time zone) are a card over the map,
 * shown when the Quiet hours link targets them or a save was refused. */
.map-ready .place-details {
  display: none;
}

.map-ready .place-details:target,
.map-ready .place-details:has([aria-invalid="true"]) {
  display: block;
  position: absolute;
  z-index: 5;
  top: var(--place-gutter);
  left: var(--place-gutter);
  right: var(--place-gutter);
  max-height: calc(100% - 40dvh - 2 * var(--place-gutter));
  overflow-y: auto;
  box-sizing: border-box;
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  pointer-events: auto;
}

.map-ready .place-details > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-weight: var(--weight-semibold);
  list-style: none;
  pointer-events: none;
}

.map-ready .place-details > summary::-webkit-details-marker {
  display: none;
}

.map-ready .place-details__close {
  display: inline-flex;
}

/* The page fills the area between the app bar and the tab bar on a phone,
 * the whole content area from 960px. The page's own padding goes. */
.map-ready .page-content:has(.place-stage) {
  display: flex;
  flex-direction: column;
  max-width: none;
  height: calc(100dvh - 52px - 57px - env(safe-area-inset-bottom, 0px));
  padding: 0;
}

.map-ready .page-content:has(.place-stage) > * {
  flex: none;
}

.map-ready .page-content:has(.place-stage) > .place-page {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

.map-ready .site-header:has(~ .page-content .place-stage) {
  margin-bottom: 0;
}

/* No zoom buttons on a phone (pinch zooms); the keyboard and the wide screen
 * have them. */
@media (max-width: 959.98px) {
  .map-ready .place-map .leaflet-control-zoom {
    display: none;
  }
}

@media (min-width: 960px) {
  /* The shell's column gap is taken back so the map meets the sidebar. */
  .map-ready .page-content:has(.place-stage) {
    width: calc(100% + var(--space-xl));
    height: 100dvh;
    margin: 0 0 0 calc(-1 * var(--space-xl));
  }

  .map-ready .place-stage {
    min-height: 34rem;
    --place-card-w: 360px;
    --place-search-bottom: calc(var(--space-md) + 54px);
  }

  .map-ready .place-float {
    right: auto;
    top: var(--space-md);
    left: var(--space-md);
    width: var(--place-card-w);
  }

  .map-ready .place-panel {
    grid-template-columns: calc(var(--place-card-w) + 2 * var(--space-md)) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto auto;
  }

  /* The mode switch and the sheet are one card at the left. */
  .map-ready .place-card {
    display: flex;
    flex-direction: column;
    grid-column: 1;
    grid-row: 1;
    gap: var(--space-md);
    box-sizing: border-box;
    margin: 0 var(--space-md);
    max-height: calc(100dvh - var(--place-search-bottom) - 2 * var(--space-md));
    overflow-y: auto;
    pointer-events: auto;
    box-shadow: var(--shadow-card-hover);
  }

  .map-ready .place-modes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    justify-self: stretch;
    margin: 0;
    padding: var(--space-xs);
    background: var(--color-secondary);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
  }

  .map-ready .place-mode {
    border-radius: var(--radius-control);
    box-shadow: none;
    background: transparent;
  }

  .map-ready .place-mode:has(input:checked) {
    border-radius: var(--radius-control);
    background: var(--color-dominant);
    color: var(--color-accent);
    box-shadow: var(--shadow-card);
  }

  .map-ready .place-sheet {
    max-height: none;
    overflow: visible;
    padding: 0;
    background: none;
    border-radius: 0;
    box-shadow: none;
  }

  .map-ready .place-sheet .dirty-bar {
    justify-content: flex-start;
  }

  .map-ready .place-info {
    grid-column: 2;
    grid-row: 3;
    justify-self: end;
    margin: 0 var(--space-md) var(--space-sm) 0;
  }

  .map-ready .place-credits {
    top: auto;
    bottom: calc(100% + var(--space-sm));
  }

  .map-ready .place-tools {
    grid-column: 2;
    grid-row: 4;
    margin: 0 var(--space-md) var(--space-md) 0;
  }

  .map-ready .place-details:target,
  .map-ready .place-details:has([aria-invalid="true"]) {
    left: calc(var(--place-card-w) + 3 * var(--space-md));
    right: auto;
    width: min(28rem, calc(100% - var(--place-card-w) - 4 * var(--space-md)));
    top: var(--space-md);
    max-height: calc(100% - 2 * var(--space-md));
  }

  .map-ready .place-results {
    margin-top: var(--space-xs);
  }
}

/* The map. Leaflet's own stylesheet comes first on this page; the rules
 * below only size it and put its parts on the tokens. Tiles are shown as
 * served in both themes (no filter, no inversion). */
.place-map {
  width: 100%;
  max-width: 100%;
  height: min(60vh, 26rem);
  min-height: 17.5rem;
  box-sizing: border-box;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-secondary);
  font-family: var(--font-ui);
  font-size: var(--font-label-size);
  overflow: hidden;
}

.js .place-map[hidden] {
  display: none;
}

/* A map that did not start leaves the plain form: nothing script-only shows. */
html:not(.map-ready) .place-search,
html:not(.map-ready) .place-info,
html:not(.map-ready) .place-tools,
html:not(.map-ready) .place-details__close {
  display: none;
}

.place-map__notice {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-secondary);
  color: var(--color-text);
}

.place-map__notice[hidden] {
  display: none;
}

.place-map.place-map .leaflet-bar a,
.place-map.place-map .leaflet-bar a:hover {
  width: 44px;
  height: 44px;
  line-height: 44px;
  box-sizing: border-box;
  background: var(--color-dominant);
  color: var(--color-text);
  border-bottom-color: var(--color-border);
  font-size: var(--font-heading-size);
}

.place-map .leaflet-bar {
  border: 1px solid var(--color-border);
  box-shadow: none;
}

.place-map.leaflet-container:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The pin: the sprite glyph in the accent colour, tip at the bottom of its
 * 44 px hit box. */
.place-pin {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--color-accent);
}

.place-pin__glyph {
  width: 36px;
  height: 36px;
}

.place-circle {
  stroke: var(--color-accent);
  stroke-width: 2px;
  fill: var(--color-accent);
  fill-opacity: 0.12;
}

/* An airport dot: a 12 px mark inside a 44 px invisible hit box. */
.place-dot {
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
}

.place-dot__mark {
  width: 12px;
  height: 12px;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--color-dominant);
  background: color-mix(in srgb, var(--color-text) 60%, transparent);
}

.place-status {
  margin: var(--space-sm) 0 0;
}

.js .place-locate[hidden],
.place-results[hidden],
.place-nearest[hidden] {
  display: none;
}

.place-results {
  margin-bottom: var(--space-md);
}

.place-results__note {
  margin: 0;
  overflow-wrap: anywhere;
}

.place-results__list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
}

.place-results__list li + li {
  border-top: 1px solid var(--color-border);
}

button.place-result {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-xs);
  width: 100%;
  height: auto;
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-body-size);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: var(--color-text);
  text-align: left;
}

button.place-result:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, var(--color-dominant));
  box-shadow: none;
}

.place-result__name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.place-result__rest {
  overflow-wrap: anywhere;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.place-nearest {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

.place-nearest__text {
  margin: 0;
}

/* The airport list scrolls inside its card; each row is one link. */
.place-airports {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  max-height: 24rem;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
}

.place-airport + .place-airport {
  border-top: 1px solid var(--color-border);
}

.place-airport__link {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text);
  text-decoration: none;
}

.place-airport__link:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, var(--color-dominant));
}

.place-airport__name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.place-airport__meta {
  overflow-wrap: anywhere;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.place-airport__meta[data-current-label]::after {
  content: " · " attr(data-current-label);
}

.place-subtitle {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
}

.place-chosen__name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.place-radius,
.place-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.place-panel__area .place-radius {
  margin-top: 0;
}

.place-radius__row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.place-radius__row input[type="number"] {
  flex: 0 0 6rem;
  width: 6rem;
}

.place-radius__range {
  --js-gate-display: block;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  accent-color: var(--color-accent);
}

.place-radius .section-caption,
.place-fields .section-caption {
  margin: 0;
}

.place-fields input,
.place-fields select {
  width: 100%;
  max-width: 28rem;
  box-sizing: border-box;
}

.place-details > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
}

/* The locate button is a round button on the map. */
.map-ready .place-locate {
  width: 48px;
  height: 48px;
  min-height: 48px;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card-hover);
  color: var(--color-accent);
  pointer-events: auto;
}

.map-ready .place-locate__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.rule-add-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.rule-add-form__field {
  display: flex;
  flex-direction: column;
}

/* align-self: flex-start opts this button out of the column container's
 * default align-items: stretch (which otherwise stretches it full-
 * width); width: auto states the resulting intrinsic size explicitly.
 * Accent fill is kept — this rule is about geometry only. */
.rule-add-form > button[type="submit"] {
  align-self: flex-start;
  width: auto;
}

.field-inline-value {
  margin-left: var(--space-xs);
  white-space: nowrap;
}

.field-inline-value[hidden] {
  display: none;
}

/* Home page audit/refactor: grouped nav, page-header caption, Home
 * widgets, and misc phone-layout fixes appended after the earlier
 * rules above (so they win on position wherever specificity ties). */

/* 1. Grouped navigation ------------------------------------------------ */
.nav-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

.nav-group__label {
  display: block;
  padding: 0 var(--space-sm);
  margin-bottom: var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* 2. Page-header screen caption --------------------------------------- */
.page-header__screen {
  margin: 0 0 var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* 3. Home page --------------------------------------------------------- */
.home-section {
  margin-bottom: var(--space-lg);
}

/* Stays one column at every width: splitting into desktop columns would
 * put the day band in a narrower column than its canvas needs (measured:
 * an 880px container leaves 288px per column against a 310px floor),
 * silently invalidating draw.py's DAY_BAND_MIN_MARK_SPACING_PERCENT. */
.home-overview {
  display: grid;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}

/* .home-overview > * is doubled to (0,2,0) deliberately: at (0,1,0) it
 * tied .frame-strip's own margin-bottom rule and lost to source order,
 * leaving inconsistent gaps (measured in Chromium). */
.home-overview.home-overview > * {
  margin-bottom: 0;
}

.home-section > .text-heading {
  margin-top: 0;
}

.home-status-grid {
  margin-bottom: var(--space-sm);
  align-items: stretch;
}

.stat-tile__gauge {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.widget-verdict {
  margin: 0 0 var(--space-xs);
  font-weight: var(--weight-semibold);
}

.widget-detail {
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.home-columns {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
}

.home-columns > .home-section {
  margin-bottom: 0;
}

/* .home-columns.home-picture-row is (0,2,0), outranking the generic
 * .home-columns >=960px split above at every width, so this row keeps
 * its own 3:2 ratio instead of the ambient 1fr/1fr. */
.home-columns.home-picture-row {
  grid-template-columns: 1fr;
}

@media (min-width: 1024px) {
  .home-columns.home-picture-row {
    grid-template-columns: 3fr 2fr;
    /* flex-start, not the bare "start" keyword: test_status_pages.py
     * pins this file to exactly one remaining bare-keyword declaration
     * elsewhere. Grid treats flex-start as an alias, same effect. */
    align-items: flex-start;
  }
}

/* The latest frame is the Home page's anchor.  The image remains bounded by
 * the existing preview rules; this grid only gives it the reading priority
 * the owner selected, with the recent list immediately alongside on desktop
 * and directly after it on a phone. */
.home-current-frame {
  min-width: 0;
}

.home-current-frame .preview-frame__image {
  width: 100%;
}

/* The docked attention toast only; the container is empty (and takes no
 * space) while every health state is fine. */
.home-facts {
  margin: 0 0 var(--space-lg);
}

.home-facts:empty {
  display: none;
}

.home-facts > .toast--docked {
  margin-bottom: 0;
}

/* The amber or red notification: the explanation is the toast title, the
 * known figures and the tip stack under it, each on its own line. */
.home-notice__line {
  display: block;
  margin-top: var(--space-xs);
  color: color-mix(in srgb, var(--color-toast-fg) 76%, transparent);
}

.home-notice__tip {
  display: block;
  margin-top: var(--space-xs);
}

/* The status header at the top of Home: one hero card in three zones, the
 * frame's state, the battery dial and a recessed panel holding the two
 * switches. Mobile first: the zones stack, separated by hairlines. The
 * arrangement follows the card's own width (a container query), not the
 * viewport's, because the desktop sidebar makes the column narrower at
 * 1024px than at 840px: from 520px of card the dial moves beside the
 * state with the switches below, from 600px the switches sit side by
 * side, and from 840px of card (a wide window, sidebar included) the
 * three zones become columns. Surface, border and
 * radius are the shared tokens, so both themes follow the palette. */
.home-state {
  --home-tone: var(--color-status-ok);
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  margin-bottom: var(--space-lg);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

.home-state--warn {
  --home-tone: var(--color-status-warn);
}

.home-state--error {
  --home-tone: var(--color-status-error);
}

.home-state--off {
  --home-tone: color-mix(in srgb, var(--color-text) 45%, transparent);
}

/* The state tint: a faint, static glow from the top-left corner in the
 * dot's colour. Subtle on purpose (13%, fading out before mid-card), the
 * wording and the dot carry the meaning; nothing moves, so reduced
 * motion needs nothing more. */
.home-state::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(
    90% 130% at 0% 0%,
    color-mix(in srgb, var(--home-tone) 13%, transparent),
    transparent 62%);
}

.home-state__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.home-state__status {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  min-width: 0;
  padding: var(--space-lg);
}

.home-state__status > p {
  margin: 0;
}

/* The micro-label voice the quick-action labels already use (12px
 * uppercase, the sub-scale exception tier). */
.home-state__eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.home-state__eyebrow .icon {
  width: 16px;
  height: 16px;
}

/* The state title takes the page-title size in the serif face: the
 * header's one display line, so it reads from across the room. */
.home-state__headline {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  font-family: var(--font-serif);
  font-size: var(--font-page-title-size);
  font-weight: var(--weight-semibold);
  line-height: 1.15;
  letter-spacing: -0.015em;
}

/* A solid dot inside a soft halo of the same status colour, so the state
 * reads at a glance without relying on colour alone: the words beside it
 * carry the meaning. */
.home-state__dot {
  position: relative;
  --dot-halo-scale: 2.8;
  --dot-halo-alpha: 0.5;
  flex: none;
  width: 14px;
  height: 14px;
  /* Centred on the first line, so a title that wraps on a phone keeps
   * its dot beside the first word. */
  margin: calc((1.15em - 14px) / 2) var(--space-xs) 0;
  background: var(--home-tone);
  box-shadow:
    0 0 0 5px color-mix(in srgb, var(--home-tone) 20%, transparent),
    0 0 18px 4px color-mix(in srgb, var(--home-tone) 30%, transparent),
    0 0 34px 10px color-mix(in srgb, var(--home-tone) 14%, transparent);
}

/* The halo: a ring of the dot's own tone that expands and fades, drawn on
 * a pseudo-element so only a transform and opacity move. Green when the
 * screen is on, amber when overdue or the battery holds the frame; a
 * resting frame (quiet hours, screen off) carries no pulse class. The
 * amber ring is gentler (smaller, fainter) through two custom properties,
 * never a second duration. */
.home-state--warn .home-state__dot,
.home-state--error .home-state__dot {
  --dot-halo-scale: 2.2;
  --dot-halo-alpha: 0.35;
}

.home-state__dot.dot--hollow {
  background: transparent;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--color-text) 50%, transparent);
}

.home-state__dot--pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--home-tone);
  pointer-events: none;
  animation: skypane-dot-halo var(--motion-dot-pulse) ease-out infinite;
}

.home-state__detail {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.home-state__next {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  color: var(--color-text);
}

.home-state__next .icon {
  width: 18px;
  height: 18px;
  /* Centres the 18px glyph on the first 24px line of the text. */
  margin-top: 3px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* An overdue line reads in the warning treatment through its weight and
 * the warning sign; the amber tint appears only once the delay is long
 * enough to act on. */
.home-state__next--overdue {
  font-weight: var(--weight-semibold);
}

.home-state--warn .home-state__next--overdue .icon {
  color: var(--color-status-warn);
}

.home-state__next .time-value {
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.home-state__countdown {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The next-update line and its info button share one row. The row is the
 * positioning context of the tooltip, so the bubble wraps within the
 * status zone instead of the button's own width. */
.home-state__next-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  align-self: stretch;
  gap: var(--space-xs);
}

/* A 44px tap target around an 18px glyph; the negative margins hand the
 * extra height back so the row keeps the line's own height. */
.info-tip__button {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: -10px -10px -10px -8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  cursor: help;
}

.info-tip__button:hover,
.info-tip:focus-within .info-tip__button {
  color: var(--color-text);
}

/* Shown on hover and on focus (a tap focuses the button) with no script.
 * Hidden with visibility, not display, so the text stays available to
 * aria-describedby. */
.info-tip__bubble {
  position: absolute;
  top: calc(100% + var(--space-xs));
  left: 0;
  z-index: 3;
  box-sizing: border-box;
  width: max-content;
  max-width: min(36ch, 100%);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-dominant);
  box-shadow: var(--shadow-toast-float);
  color: var(--color-text);
  font-weight: var(--weight-regular);
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--motion-fast) ease-out;
}

.info-tip:hover .info-tip__bubble,
.info-tip:focus-within .info-tip__bubble {
  visibility: visible;
  opacity: 1;
}

/* The battery dial: the open arc drawn by draw.arc_gauge() with the
 * reading stacked in its middle and the level word in the arc's gap. All
 * three share the one grid cell (grid-area: 1 / 1 on each). */
.home-battery {
  display: grid;
  justify-content: center;
  align-items: center;
  justify-items: center;
  margin: 0;
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  border-top: 1px solid var(--color-border);
}

/* The 132px drawing shown at 120px on a narrow card; its viewBox scales
 * the arc and its stroke together. */
.home-battery > svg {
  grid-area: 1 / 1;
  width: 120px;
  height: 120px;
}

.home-battery__reading {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  padding-top: var(--space-xs);
}

.home-battery__value {
  font-size: var(--font-page-title-size);
  font-weight: var(--weight-semibold);
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.home-battery__unit {
  margin-left: 1px;
  font-size: var(--font-body-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Same micro-label voice as the header's "Frame state". */
.home-battery__label {
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The level word is always present for low and critical, so colour is
 * never the only signal. It sits in the arc's gap; text is the status
 * colour mixed toward the text colour, readable on its tinted pill in
 * both themes. */
.home-battery__state {
  grid-area: 1 / 1;
  align-self: end;
  /* Drops the pill clear of the arc's round end caps; the zone's bottom
   * padding has room for it. */
  transform: translateY(6px);
  padding: 2px var(--space-sm);
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  white-space: nowrap;
  color: color-mix(in srgb, var(--color-status-warn) 55%, var(--color-text));
  background: color-mix(in srgb, var(--color-status-warn) 18%, var(--color-dominant));
}

.home-battery--critical .home-battery__state {
  color: color-mix(in srgb, var(--color-status-error) 70%, var(--color-text));
  background: color-mix(in srgb, var(--color-status-error) 16%, var(--color-dominant));
}

.home-battery--none {
  justify-content: start;
  padding: var(--space-md) var(--space-lg);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The estimated "Probably charging" pill, shared by Home and Health. The
 * ok tone, never the accent: it reports a state, it is not an action or a
 * selection. The bolt and the words carry the meaning, so colour is
 * never the only signal. */
.charging-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  max-width: 100%;
  padding: 2px var(--space-sm);
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  white-space: nowrap;
  color: color-mix(in srgb, var(--color-status-ok) 55%, var(--color-text));
  background: color-mix(in srgb, var(--color-status-ok) 16%, transparent);
}

.charging-pill .icon {
  flex: none;
}

/* A row of its own under the arc, centred, so it never competes with the
 * level word that sits in the arc's gap for a low battery. */
.home-battery__charging {
  grid-area: 2 / 1;
  justify-self: center;
  /* Clears the level word, which sits 6px below the arc's gap. */
  margin-top: var(--space-md);
}

/* The recessed panel: a step down from the card surface toward the
 * canvas. */
.home-state__switches {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  gap: var(--space-sm);
  min-width: 0;
  padding: var(--space-md);
  border-top: 1px solid var(--color-border);
  /* The card no longer clips (the info tooltip may reach past it), so the
   * recessed panel rounds its own outer corners. */
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  background: color-mix(in srgb, var(--color-canvas) 55%, transparent);
}

/* Each row is a pill and one big tap target: the native switch button
 * stretches its ::after over the whole pill, so the control keeps its
 * role, state and focus behaviour while the finger gets a 56px-tall
 * target. */
.home-switch {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 56px;
  padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-sm);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: 999px;
}

.home-switch:hover {
  border-color: color-mix(in srgb, var(--color-text) 25%, transparent);
}

.home-switch__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-secondary);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.home-switch--on .home-switch__icon {
  color: var(--color-text);
}

.home-switch__icon .icon {
  width: 18px;
  height: 18px;
}

.home-switch .switch,
.home-switch .switch:hover,
.home-switch .switch:active {
  background: none;
  border: 0;
  box-shadow: none;
  /* A transform would make the button the ::after's containing block and
   * shrink the row-wide target to the button mid-press. */
  transform: none;
}

.home-switch .switch::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 999px;
}

.home-switch__text {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  line-height: 1.3;
}

.home-switch__label {
  font-weight: var(--weight-semibold);
}

.home-switch__detail {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  font-variant-numeric: tabular-nums;
}

.home-switch__window {
  /* Above the switch's row-wide ::after so the link takes its own click;
   * the padding and the matching negative margin give a 44px target
   * without moving the line. */
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  box-sizing: border-box;
  min-height: 44px;
  padding: 0 6px;
  margin: -13px -6px;
  white-space: nowrap;
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--color-text) 35%, transparent);
  text-underline-offset: 3px;
  border-radius: var(--radius-control);
}

.home-switch__window:hover {
  color: var(--color-text);
  text-decoration-color: currentColor;
}

/* The chevron marks the window as openable; it carries no underline. */
.home-switch__chevron {
  flex: none;
  width: 16px;
  height: 16px;
}

/* Quiet hours dialog: the two time fields share a row on a wide dialog,
 * stack on a phone, and the preset row keeps its bottom gap off the
 * sentence below it. */
.modal .quiet-times-row {
  gap: var(--space-sm);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.modal .quiet-preset-row {
  margin-bottom: 0;
}

.quiet-intro {
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* With a script the preset cards drive the enable checkbox, so its own row
 * leaves the layout (display:none also takes it out of the tab order);
 * without one the checkbox is the visible control and the cards are hidden
 * by their .js-gate wrapper. */
.js .quiet-enabled-field {
  display: none;
}

.quiet-cards {
  display: grid;
  gap: var(--space-sm);
}

.modal .quiet-card {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  min-height: 64px;
  padding: var(--space-sm) var(--space-md);
  box-sizing: border-box;
  text-align: left;
  font-size: var(--font-body-size);
  color: var(--color-text);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: 14px;
}

.modal .quiet-card[aria-pressed="true"] {
  border: 2px solid var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

.quiet-card__icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--color-secondary);
}

.quiet-card[aria-pressed="true"] .quiet-card__icon {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.quiet-card__text {
  display: grid;
  min-width: 0;
}

.quiet-card__title {
  font-weight: var(--weight-semibold);
}

.quiet-card__detail {
  font-size: var(--font-label-size);
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.quiet-card__check {
  display: none;
  margin-left: auto;
  color: var(--color-accent);
}

.quiet-card[aria-pressed="true"] .quiet-card__check {
  display: inline-flex;
}

.quiet-custom {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-md);
}

.quiet-custom__label {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.quiet-time-field {
  min-width: 0;
}

.quiet-tile {
  display: block;
  box-sizing: border-box;
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-secondary);
  border-radius: 14px;
}

.quiet-tile__label {
  display: block;
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.modal .quiet-tile input[type="time"] {
  padding: 0;
  border: none;
  background: transparent;
  font-size: 1.75rem;
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.modal .quiet-timezone-note {
  margin: 0;
}

/* The link inside the sentence keeps its place in the line; its padding and
 * the matching negative margin give a 44px target without moving the text. */
.quiet-timezone-note a {
  display: inline-block;
  padding: 13px 4px;
  margin: -13px -4px;
}

.home-switch__detail .home-switch__state {
  font-size: var(--font-label-size);
}

.home-switch .quick-action__form {
  flex: none;
}

@container (min-width: 520px) {
  .home-state__body {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .home-battery {
    align-self: stretch;
    align-content: center;
    padding: var(--space-lg) var(--space-xl);
    border-top: 0;
    border-left: 1px solid var(--color-border);
  }

  .home-battery > svg {
    width: 132px;
    height: 132px;
  }

  .home-state__switches {
    grid-column: 1 / -1;
  }
}

@container (min-width: 600px) {
  .home-state__switches {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@container (min-width: 840px) {
  .home-state__body {
    grid-template-columns: minmax(0, 1fr) auto minmax(260px, 320px);
  }

  .home-state__status {
    justify-content: center;
    padding: var(--space-xl);
  }

  .home-state__switches {
    grid-column: auto;
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-lg) var(--space-md);
    border-top: 0;
    border-left: 1px solid var(--color-border);
    border-radius: 0 var(--radius-card) var(--radius-card) 0;
  }
}

/* Without the battery dial (a plain user's Home) the status zone and the
 * switches share the row. */
@container (min-width: 520px) {
  .home-state--no-battery .home-state__body {
    grid-template-columns: minmax(0, 1fr);
  }
}

@container (min-width: 840px) {
  .home-state--no-battery .home-state__body {
    grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
  }
}

.quick-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.quick-action--on {
  border-left-color: var(--color-status-ok);
}

.quick-action--off {
  border-left-color: color-mix(in srgb, var(--color-text) 30%, transparent);
}

.quick-action--neutral {
  border-left-color: var(--color-border);
}

.quick-action__label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.quick-action__icon {
  flex: none;
}

.quick-action__state {
  font-weight: var(--weight-semibold);
}

.quick-action__form {
  flex: none;
  margin: 0;
  display: flex;
  align-items: center;
}

/* One <button type="submit"> reused for three switches. 44x44 is met
 * directly by the button's own padding — WCAG 2.5.5 in both axes at
 * every width. State is never colour-only: thumb position, aria-checked
 * and the state word all change together. */
button.switch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 44px;
  min-height: 44px;
  height: auto;
  padding: var(--space-xs);
  background: none;
  border: 0;
  border-radius: var(--radius-control);
  box-shadow: none;
}

/* button.switch (0,1,1) is declared after the generic button[type="submit"]
 * rule and must stay so: the switch is a submit button, and without this
 * the primary fill and shadow would paint a box behind the track. */
button.switch:hover,
button.switch:active {
  background: none;
  border: 0;
  transform: none;
  box-shadow: none;
}

.switch__track {
  position: relative;
  display: block;
  width: 52px;
  height: 32px;
  border-radius: 16px;
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  transition: background-color var(--motion-fast) ease, border-color var(--motion-fast) ease;
}

.switch__thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-switch-thumb);
  box-shadow: 0 1px 3px var(--color-shadow-strong);
  transition: transform var(--motion-fast) ease;
}

.switch[aria-checked="true"] .switch__track {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.switch[aria-checked="true"] .switch__thumb {
  transform: translateX(20px);
}

.settings-switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

/* [data-pending] is set by quick-switch.js while its fetch is in
 * flight; freshness.js's refresh swap already skips elements carrying
 * it, so a pending control is never repainted mid-request. */
[data-pending] .switch__track {
  opacity: 0.7;
}

.frame-strip__cell {
  display: grid;
  grid-template-rows: auto auto auto;
  row-gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-canvas);
}

.frame-strip__cell.quick-action {
  border-left-width: 4px;
  flex: 1 1 220px;
}

.frame-strip__row--state {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-width: 0;
}

.frame-strip__schedule-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.frame-strip {
  margin-bottom: var(--space-lg);
}

.frame-strip__cells {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--space-lg);
}

.frame-strip__cell--update {
  flex: 1 1 220px;
}

@media (max-width: 699.98px) {
  .frame-strip__cells {
    flex-direction: column;
    align-items: stretch;
  }
  .frame-strip__cell.quick-action,
  .frame-strip__cell--update {
    flex-basis: auto;
  }
}

.now-showing {
  margin: 0;
}

.now-showing__image {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-image-backdrop);
  max-width: 360px;
  margin: 0 auto var(--space-sm);
}

.now-showing__caption {
  text-align: center;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.recent-flights {
  list-style: none;
  margin: 0 0 var(--space-sm);
  padding: 0;
}

/* A wrapping flex line, not a grid row: a grid's auto time-track always
 * resolves to its own max-content width regardless of the callsign's
 * needs, starving the callsign to a sliver in French. Flex lets the
 * time wrap to its own second line only when there is truly no room. */
.recent-flight {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  align-items: baseline;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border);
}

.recent-flight:last-child {
  border-bottom: 0;
}

.recent-flight__callsign {
  font-weight: var(--weight-semibold);
}

/* order: 1 keeps the detail line last visually even though it is the
 * third child before the time (fourth); the inline-start margin
 * reproduces the old grid's thumbnail-width offset (40px + gap). */
.recent-flight__detail {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  overflow-wrap: anywhere;
  order: 1;
  flex: 1 1 100%;
  margin-inline-start: calc(40px + var(--space-md));
}

/* No percentage width cap here: since this item sits in a content-sized
 * auto track, a percentage would resolve against that same content-
 * derived width and always overflow itself (measured: 88px clientWidth
 * vs 147px scrollWidth in English). The track itself is the line cap. */
.recent-flight__time {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  text-align: right;
  margin-inline-start: auto;
  white-space: nowrap;
}

/* img.recent-flight__thumb (tag-qualified, not bare .recent-flight__thumb)
 * avoids tying at equal specificity with the dashed-placeholder span's
 * own rule, which would let source order rather than intent decide. */
img.recent-flight__thumb {
  display: block;
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-image-backdrop);
  object-fit: cover;
  flex: none;
}

.recent-flight__thumb--placeholder {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-control);
  object-fit: cover;
  flex: none;
  border: 1px dashed var(--color-border);
  background: var(--color-canvas);
}

/* Home's phone list: the Flights boarding-pass card (.history-card and
 * its bands) at a compact size, one per recent flight. The cards sit on
 * the section's own surface, so the tear-line notches take that colour
 * instead of the page canvas. Rendered before the desktop list; the pair
 * toggles at 960px. */
.recent-flight-tiles {
  --history-card-notch: var(--color-dominant);
  display: grid;
  gap: var(--space-sm);
  list-style: none;
  margin: 0 0 var(--space-md);
  padding: 0;
}

.recent-flight-tiles ~ .recent-flights {
  display: none;
}

/* The head is one line: callsign, then the airline taking what is left
 * and ellipsised, so a long operator name never wraps the card. */
.history-card--compact .history-card__head {
  padding: var(--space-xs) var(--space-sm) 0 var(--space-sm);
}

.history-card--compact .history-card__id {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding-top: 0;
}

.history-card--compact .history-card__callsign {
  flex: none;
  font-size: var(--font-label-size);
  overflow-wrap: normal;
}

.history-card--compact .history-card__airline {
  min-width: 0;
  font-size: 13px;
}

.history-card--compact .history-card__route {
  grid-template-rows: 18px auto;
  padding: 0 var(--space-sm) 3px;
}

.history-card--compact .history-card__code {
  font-size: 18px;
}

.history-card--compact .history-card__code--none {
  line-height: 18px;
  font-size: 12px;
}

.history-card--compact .icon.history-card__plane {
  width: 14px;
  height: 14px;
}

.history-card--compact .history-card__dir {
  font-size: 10px;
  line-height: 1.1;
}

.history-card--compact .history-card__stub {
  padding: 3px var(--space-sm);
}

.history-card--compact .history-card__art {
  width: 96px;
  height: 26px;
}

/* Too narrow for the "No illustration" words: the dashed outline says
 * it, and the text stays in the markup for a screen reader. */
.history-card--compact .history-card__art--empty {
  padding: 0;
  font-size: 0;
}

.history-card--compact .history-card__when {
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-sm);
}

.history-card--compact .time-value--primary {
  font-size: var(--font-label-size);
}

.history-card--compact .time-value__age {
  font-size: 12px;
}

@media (min-width: 960px) {
  .home-columns {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .recent-flight-tiles {
    display: none;
  }

  .recent-flight-tiles ~ .recent-flights {
    display: block;
  }
}

/* 4. Audit fixes ------------------------------------------------------- */

.config-form input[type="number"],
.config-form input[type="url"],
.config-form input[type="text"] {
  width: 100%;
  max-width: 28rem;
}

.config-form input[type="time"] {
  min-width: 9rem;
}

/* 8ch is a content-fit width for a 3-4 digit number (one of this file's
 * few off-scale literals); 96px keeps it usable in a condensed font.
 * Selected by [name=] at equal (0,2,1) specificity to the type=number
 * rule above it, so source order — not a specificity hack — wins. */
.config-form input[name="wake_interval_s"] {
  width: 8ch;
  min-width: 96px;
}

input[name="radius_km"] {
  width: 8ch;
  min-width: 96px;
}

@media (max-width: 959.98px) {

  /* z-index: 30, one value above the tab bar's 20 at every breakpoint:
   * the save bar is the active task and may cover ambient nav chrome,
   * never the reverse. The bottom offset already adds the tab bar's
   * 56px height so the two never overlap in the normal case. */
  .dirty-bar {
    position: fixed;
    left: var(--space-md);
    right: var(--space-md);
    bottom: calc(var(--space-md) + 56px + env(safe-area-inset-bottom, 0px));
    margin: 0;
    z-index: 30;
    flex-wrap: wrap;
  }

}

.status-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
}

.status-row__label {
  flex: 0 0 auto;
  min-width: 96px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

@media (max-width: 959.98px) {
  /* Below 960px the label wraps freely: no phone-width row has room for a fixed column. */
  .status-row__label {
    min-width: 0;
  }
}

.status-row__verdict {
  font-weight: var(--weight-semibold);
}

.status-row__detail {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.preview-frame {
  margin: 0;
}

@media (min-width: 960px) {
  .preview-frame__image {
    width: min(100%, calc(60vh * 3 / 4));
    max-width: 100%;
    max-height: 60vh;
    margin: 0 auto;
  }
}

.status-card__headline {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-md);
}

/* --color-status-warn as text on --color-dominant measures 3.19:1 in
 * light theme (below WCAG AA's 4.5:1 for normal text) but 10.54:1 in
 * dark. This modifier stays on --color-text in both themes; the warn
 * state is carried by wording and a leading dot, not text colour. */
.status-card__headline--warn {
  color: var(--color-text);
}

.time-value {
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
  font-size: var(--font-label-size);
  font-weight: var(--weight-regular);
}

.time-value--primary {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
}

.time-value__age {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Equal (0,1,1) specificity to button[type="submit"], but later in
 * source order, so this wins without needing a :where() wrapper (which
 * would drop to (0,0,0) and lose to every other rule). box-shadow: none
 * clears the submit button's inset highlight. */
button.calendar-disconnect-btn {
  min-height: 30px;
  padding: 4px var(--space-sm);
  font-size: 12px;
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent);
  border-radius: var(--radius-control);
  box-shadow: none;
}
/* Same equal-specificity/later-source trick as the base rule, one
 * pseudo-class up on both sides; border-color/box-shadow restated since
 * the primary hover rule declares both. */
button.calendar-disconnect-btn:hover,
button.calendar-disconnect-btn:focus-visible {
  background: color-mix(in srgb, var(--color-text) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-text) 20%, transparent);
  box-shadow: none;
}

/* Manage access dialog: a row is the address with its pills on the left and the
 * Remove action on the right, wrapping beneath on a narrow phone. A long
 * address wraps inside its own box instead of widening the page. */
.co-manager__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.co-manager__who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
}

.co-manager__email {
  overflow-wrap: anywhere;
  min-width: 0;
}

.co-manager__remove {
  margin: 0;
}

/* The administrator's picker holds full e-mail addresses; a native select
 * sizes to its longest option, so cap it at the dialog's width. */
.modal__body select {
  max-width: 100%;
}

/* Targets <a>, not <button> — history_page's Show More link — so there
 * is no button[type="submit"] specificity fight to win here. */
a.calendar-disconnect-btn {
  display: inline-block;
  min-height: 30px;
  padding: 4px var(--space-sm);
  font-size: 12px;
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent);
  border-radius: var(--radius-control);
  text-decoration: none;
}
a.calendar-disconnect-btn:hover,
a.calendar-disconnect-btn:focus-visible {
  background: color-mix(in srgb, var(--color-text) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-text) 20%, transparent);
}

.calendar-masked-url {
  font-family: var(--font-mono);
  margin: var(--space-sm) 0 0;
}

/* Calendar connection: one status row on the page (provider tile, status
 * pill, one-line detail, one button) and the Manage sheet, a <dialog>
 * that holds every control. The row carries its verdict in words; the
 * pill's dot is never green beside a failing or stale verdict (the server
 * picks the tone per state). */
.calendar-status {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: var(--space-sm);
  padding: var(--space-sm) 4px 0;
}

.calendar-status__tile {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-control);
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.calendar-status__tile--empty {
  background: transparent;
  border-style: dashed;
}

.calendar-status__main {
  flex: 1 1 0;
  min-width: 0;
}

.calendar-status__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
}

.calendar-status__title b {
  font-weight: var(--weight-semibold);
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The status line leads the Calendar row; the look of its flights follows
 * under a hairline. */
.special-row--calendar > .calendar-status + .special-row__main {
  margin-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-sm);
}

.special-row--calendar > .empty-state--compact {
  margin-bottom: var(--space-xs);
}

.calendar-status__pill .dot {
  width: 8px;
  height: 8px;
  margin-right: 6px;
}

.calendar-status__detail {
  margin: 2px 0 0;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The button stays on the right of the row at every width, vertically
 * centred; in a narrow column the text side wraps instead. */
a.calendar-manage {
  display: inline-flex;
  flex: none;
  white-space: nowrap;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 10px;
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  color: var(--color-text);
  text-decoration: none;
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent);
  border-radius: var(--radius-control);
}

a.calendar-manage:hover {
  background: color-mix(in srgb, var(--color-text) 7.5%, transparent);
}

/* "Choose calendar" is too wide to sit beside the text on a phone: the button
 * takes the row's full width under the text instead. */
@media (max-width: 699.98px) {
  .calendar-status {
    flex-wrap: wrap;
  }

  .calendar-status__main {
    flex: 1 1 0;
  }

  a.calendar-manage {
    flex: 1 1 100%;
  }
}

/* The section's one primary action while nothing is connected. */
a.calendar-manage--primary {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-color: transparent;
}

a.calendar-manage--primary:hover {
  background: var(--color-accent-hover);
}

/* box-shadow at rest is this file's hairline-at-rest exception for a
 * floating modal, as for .lightbox. color is explicit because a <dialog>
 * promoted by showModal() does not reliably inherit it. No entrance
 * animation: the motion budget is spent elsewhere. */
.calendar-sheet {
  box-sizing: border-box;
  width: calc(100% - var(--space-lg) * 2);
  max-width: 440px;
  max-height: calc(100vh - var(--space-lg) * 2);
  overflow-y: auto;
  padding: var(--space-lg);
  background: var(--color-dominant);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card-hover);
}

/* The dialog is a container, not a control: arriving at it by its
 * fragment must not draw a heavy ring round the whole card. Every control
 * inside keeps the global focus ring. */
.calendar-sheet:focus {
  outline: none;
}

/* Scripts blocked: the open dialog is an in-page card, not a modal. */
.calendar-sheet[open]:not(:modal) {
  position: static;
  width: auto;
  max-width: none;
  margin: var(--space-md) 0 0;
  box-shadow: none;
}

.calendar-sheet::backdrop {
  background: var(--color-scrim);
}

.calendar-sheet > * + * {
  margin-top: var(--space-md);
}

.calendar-sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.calendar-sheet__title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--font-heading-size);
  font-weight: var(--weight-regular);
}

a.calendar-sheet__close {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  margin-right: calc(var(--space-sm) * -1);
  font-size: 24px;
  line-height: 1;
  color: var(--color-text);
  text-decoration: none;
  border-radius: var(--radius-control);
}

a.calendar-sheet__close:hover {
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}

.calendar-sheet__label {
  margin: 0;
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
}

.calendar-sheet__link .calendar-masked-url {
  margin-top: var(--space-xs);
}

.calendar-sheet input[type="text"] {
  width: 100%;
}

.calendar-sheet__note {
  margin: var(--space-sm) 0 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.calendar-sheet__field .section-caption {
  margin: var(--space-xs) 0 0;
}

.calendar-sheet__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.calendar-sheet button {
  height: auto;
  min-height: 44px;
}

a.calendar-sheet__cancel {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  font-size: var(--font-label-size);
  color: var(--color-text);
  text-decoration: none;
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 9%, transparent);
  border-radius: var(--radius-control);
}

.calendar-sheet__disconnect {
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

/* The quiet destructive action: error-coloured text, no fill. Equal
 * (0,1,1) specificity to button[type="submit"], later in source, so it
 * wins on position and clears the primary fill and its shadow. */
button.calendar-danger {
  padding: 0 4px;
  color: var(--color-status-error);
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

button.calendar-danger:hover {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  text-decoration: underline;
}

button.calendar-danger--solid {
  padding: 0 12px;
  border-color: var(--color-status-error);
}

.calendar-sheet__confirm {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border: 1px solid var(--color-status-error);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-status-error) 6%, transparent);
}

/* An author display beats the UA [hidden] rule, so restate it. */
.calendar-sheet__confirm[hidden] {
  display: none;
}

.calendar-sheet__confirm-heading {
  margin: 0;
  font-weight: var(--weight-semibold);
}

.calendar-sheet__confirm .text-body {
  margin: 0;
}

.calendar-sheet__confirm .calendar-sheet__foot {
  margin-top: var(--space-xs);
}

.text-link {
  color: var(--color-accent);
  text-decoration: underline;
}

.theme-form input[type="radio"] + label {
  height: 28px;
  /* Resets the global label { margin-bottom: var(--space-sm) } that
   * would otherwise add an 8px tail inside this 2px-padded segment. */
  margin-bottom: 0;
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
  border-radius: 6px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  font-size: 13px;
  font-weight: var(--weight-regular);
  cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}

.theme-form input[type="radio"]:checked + label {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
  font-weight: var(--weight-semibold);
}

.theme-form input[type="radio"]:not(:checked) + label:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

.theme-form input[type="radio"]:focus-visible + label {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Below 360px three equal columns leave ~73px per card with 32px of
 * padding — too narrow for the longest runway label ("Runway 3
 * (07/25)"), which then overflows the card border (measured). Switches
 * to a single-column list rather than re-wrapping, which would put an
 * orphan card alone on its own row. */
@media (max-width: 359.98px) {
  .runway-card {
    flex: 1 1 100%;
  }
}

/* Bottom tab bar (mobile nav). display: none is declared outside the
 * media query below and only lifted under 960px, so a desktop viewport
 * can never show the bar regardless of any later rule order. */
.tab-bar {
  display: none;
}

@media (max-width: 959.98px) {
  /* 959.98px matches the fractional breakpoint used elsewhere so no
   * viewport lands in a gap with neither nav visible. 56px is the tab
   * row's fixed height; the safe-area inset is added as extra padding
   * underneath, not folded into the 56px. */
  .tab-bar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 20;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--color-secondary);
    border-top: 1px solid var(--color-border);
    box-shadow: var(--shadow-card-hover);
  }

  /* Each of the five flex: 1 1 0 cells takes an equal share of the bar
   * (72px at 360px, 78px at 390px) at 56px high, clearing WCAG 2.5.5's
   * 44px target-size floor directly. color is set
   * explicitly because this element doubles as the "More" <summary>, and
   * the global summary rule's accent colour would otherwise win. */
  .tab-bar__link {
    flex: 1 1 0;
    min-width: 0;
    height: 56px;
    display: flex;
    color: var(--color-text);
    text-decoration: none;
    font-family: var(--font-ui);
  }

  /* The pill's horizontal margin is halved to 2px (from the vertical
   * 8px) because "Compagnies" needs 65px in a 78px cell at 11px size;
   * the full margin caused it to truncate with an ellipsis (measured). */
  .tab-bar__pill {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    margin: var(--space-xs) calc(var(--space-xs) / 2);
    border-radius: var(--radius-control);
    transition: background-color .15s ease, color .15s ease;
  }

  /* 11px, not the 12px/uppercase label voice — a destination is not a
   * label. Joins the existing sub-scale exception tier beside
   * .sparkline-axis-label (10px) and .cell-secondary. */
  .tab-bar__label {
    font-size: 11px;
    font-weight: var(--weight-regular);
    line-height: 1;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .tab-bar__link--active {
    color: var(--color-accent);
    font-weight: var(--weight-semibold);
  }

  .tab-bar__link--active .tab-bar__pill {
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  }

  /* The administrator's space switch, first cell of the bar: set apart from
   * the destinations by an accent outline on its pill and a thin divider
   * after it. It names the space it leads to, never the current one. */
  .tab-bar__link--switch {
    flex: 0 0 68px;
    margin-right: 0;
    border-right: 1px solid var(--color-border);
    color: var(--color-accent);
    font-weight: var(--weight-semibold);
  }

  .tab-bar__link--switch .tab-bar__pill {
    border: 1px solid var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  }

  .tab-bar__link--switch:not(.tab-bar__link--active):hover .tab-bar__pill {
    background: color-mix(in srgb, var(--color-accent) 22%, transparent);
  }

  /* :not(.tab-bar__link--active) avoids a same-specificity, later-source
   * hover rule erasing the active tint on pointer-over. */
  .tab-bar__link:not(.tab-bar__link--active):hover .tab-bar__pill {
    background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
  }

  .tab-bar__more {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    position: relative;
  }

  .tab-bar__more > .tab-bar__link {
    flex: 1 1 auto;
  }

  /* Rotation is inverted from the base chevron rule: this sheet opens
   * upward, so closed points up (toward where it will open) and open
   * points down (back toward the bar). Positioned absolutely against
   * .tab-bar__more so it costs the 78x56px cell nothing. */
  .tab-bar__more > .tab-bar__link::before {
    position: absolute;
    top: 7px;
    right: 12px;
    margin-right: 0;
    transform: rotate(-135deg);
  }

  .tab-bar__more[open] > .tab-bar__link::before {
    transform: rotate(45deg);
  }

  /* Radius only on the top corners: the sheet sits flush on the bar and
   * its right edge meets the viewport edge, so a full --radius-card would
   * round corners that touch something. */
  .tab-bar__more-panel {
    position: absolute;
    bottom: 100%;
    right: 0;
    min-width: 180px;
    /* Sized by its longest row, not by the 78px cell it hangs from, so a
     * long French label stays on one line; the cap keeps it on screen. */
    width: max-content;
    max-width: calc(100vw - var(--space-md));
    display: flex;
    flex-direction: column;
    padding: var(--space-sm);
    background: var(--color-secondary);
    border: 1px solid var(--color-border);
    border-top-left-radius: var(--radius-card);
    border-top-right-radius: var(--radius-card);
    box-shadow: var(--shadow-card-hover);
  }

  /* The last card must never sit under the bar. Scoped to the
   * server-rendered .has-tab-bar body class (companion/layout.py's
   * TAB_BAR_BODY_CLASS), present only when the bar actually rendered. */
  .has-tab-bar .page-content {
    padding-bottom: calc(
      var(--space-2xl) + 56px + env(safe-area-inset-bottom, 0px));
  }

}

/* Display: the look card ------------------------------------------------
 * Two framed live pictures (Departures, Arrivals) and the Special looks
 * list. Every saved control is a native radio inside a colour x style
 * table (.look-table); theme-preview.js layers the look sheet over it. */

.look-card__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-lg) var(--space-md);
}

.look-frame {
  min-width: 0;
}

/* Without scripts, an open "Change" table needs the whole card width. */
.look-frame:has(.look-edit[open]) {
  grid-column: 1 / -1;
}

.look-frame__anchor {
  position: relative;
  max-width: 17rem;
}

.look-frame__picture {
  position: relative;
  padding: 8px;
  border-radius: 6px;
  background: linear-gradient(145deg, var(--color-frame-bezel-hi), var(--color-frame-bezel));
  box-shadow: 0 14px 30px -14px var(--color-shadow-strong);
}

.look-frame.is-editing .look-frame__picture {
  outline: 3px solid var(--color-accent);
  outline-offset: 4px;
}

/* The box is reserved before the render arrives (3:4, the canvas's own
 * shape) and painted with the same skeleton sheen as Home's picture. */
.look-frame__image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: contain;
  background: var(--color-image-backdrop);
  background-image: linear-gradient(
    110deg,
    color-mix(in srgb, var(--color-text) 4%, transparent) 0 40%,
    color-mix(in srgb, var(--color-text) 9%, transparent) 50%,
    color-mix(in srgb, var(--color-text) 4%, transparent) 60% 100%
  );
}

/* The whole picture is the opener; its accessible name says so. */
button.look-frame__open {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  box-shadow: none;
  cursor: pointer;
}

button.look-frame__open:hover,
button.look-frame__open:active {
  background: transparent;
  transform: none;
}

button.look-frame__open:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 4px;
}

.look-frame__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  max-width: 17rem;
}

.look-frame:has(.look-edit[open]) .look-frame__meta {
  max-width: none;
}

.look-frame__caption {
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
}

.look-frame__name,
.special-row__title {
  margin: 0;
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.look-frame__sentence,
.special-row__meta {
  margin: 0;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  overflow-wrap: anywhere;
}

.look-edit[open] {
  flex-basis: 100%;
  min-width: 0;
}

.look-edit__summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--color-border);
  border-radius: 22px;
  background: var(--color-dominant);
  color: var(--color-text);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  list-style: none;
  cursor: pointer;
}

.look-edit__summary::-webkit-details-marker {
  display: none;
}

/* These summaries carry their own icon instead of the global chevron. */
.look-edit__summary::before,
.special-add__summary::before {
  content: none;
}

.look-edit__summary--icon {
  justify-content: center;
  width: 44px;
  padding: 0;
  border-color: transparent;
  background: transparent;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

.look-edit__summary:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

.look-edit__summary .icon {
  width: 16px;
  height: 16px;
}

.look-edit__body {
  margin-top: var(--space-md);
}

/* The colour x style table ------------------------------------------- */

.look-table__options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.look-option {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 44px;
  padding: 4px 14px 4px 6px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  font-size: var(--font-label-size);
  cursor: pointer;
}

.look-option__name:first-child {
  padding-left: 8px;
}

.look-table__scroll {
  overflow-x: auto;
}

.look-table {
  border-collapse: separate;
  border-spacing: 2px;
}

.look-table th {
  padding: 0 4px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  text-align: center;
}

.look-table th[scope="row"] {
  padding-right: var(--space-sm);
  text-align: left;
  white-space: nowrap;
}

.look-table__ink {
  display: inline-block;
  margin-right: 6px;
  vertical-align: -1px;
}

.look-table td {
  padding: 0;
  text-align: center;
}

.look-cell {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  margin: 0 auto;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  cursor: pointer;
}

.look-cell:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

.look-swatch {
  display: block;
  width: 24px;
  height: 32px;
  border-radius: 2px;
  box-shadow: 0 0 0 1px var(--color-border);
}

.look-table__gap-mark {
  display: inline-grid;
  place-items: start end;
  width: 24px;
  height: 32px;
  padding: 1px 3px;
  border: 1px dashed var(--color-border);
  border-radius: 2px;
  font-size: 12px;
  line-height: 1;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

.look-table__notes {
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The look sheet ----------------------------------------------------- */

/* Catches a click outside an open sheet; dims the page on phones. */
.look-scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: transparent;
}

.look-sheet {
  position: absolute;
  top: 0;
  z-index: 51;
  width: 22.5rem;
  max-width: calc(100vw - 2 * var(--space-md));
  padding: var(--space-md) 20px;
  border: 1px solid var(--color-border);
  border-radius: 14px;
  background: var(--color-dominant);
  box-shadow: var(--shadow-pop);
}

.look-sheet--departures {
  left: calc(100% + var(--space-md));
}

.look-sheet--arrivals {
  right: calc(100% + var(--space-md));
}

.look-sheet--calendar {
  top: 100%;
  left: 0;
  margin-top: var(--space-sm);
}

.look-sheet__head,
.special-add__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.look-sheet__title,
.special-add__title,
.special-looks__title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: 19px;
  font-weight: var(--weight-regular);
}

button.look-sheet__close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  box-shadow: none;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

button.look-sheet__close[hidden] {
  display: none;
}

.look-sheet__same {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
}

.look-sheet__same[hidden] {
  display: none;
}

.look-axis {
  min-width: 0;
  margin: 0;
  padding: var(--space-sm) 0;
  border: none;
  border-top: 1px solid var(--color-border);
}

.look-axis:first-child {
  border-top: none;
}

.look-axis:disabled {
  opacity: 0.5;
}

.look-axis__label {
  display: flex;
  font-family: var(--font-ui);
  justify-content: space-between;
  width: 100%;
  margin: 0 0 var(--space-sm);
  padding: 0;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.look-axis__value {
  letter-spacing: normal;
  text-transform: none;
  font-size: var(--font-label-size);
  color: var(--color-text);
}

.look-dots {
  display: flex;
  justify-content: space-between;
  gap: var(--space-xs);
}

.look-dot {
  position: relative;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  cursor: pointer;
}

.look-dot__ink {
  display: block;
  width: 34px;
  height: 34px;
}

/* A hairline keeps the black ink visible on the dark theme's surface. */
.look-dot__ink circle {
  stroke: var(--color-border);
  stroke-width: 1;
}

.look-seg {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: 10px;
  background: var(--color-secondary);
}

.look-seg__option {
  display: inline-flex;
  flex: 1 1 0;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 0;
  min-height: 44px;
  padding: 0 var(--space-sm);
  border-radius: 8px;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  white-space: nowrap;
  cursor: pointer;
}

.look-seg__icon {
  flex: none;
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px var(--color-border);
}

.look-axis__why {
  min-height: 1.4em;
  margin: var(--space-sm) 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.look-sheet__foot,
.special-add__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
}

.look-sheet__foot button,
.special-add__foot button {
  height: 40px;
  padding: 0 var(--space-md);
  font-size: var(--font-label-size);
}

button.look-sheet__reset {
  border-color: transparent;
  background: transparent;
  color: var(--color-accent);
}

button.look-sheet__reset[hidden] {
  display: none;
}

button.look-sheet__done {
  border-color: transparent;
  background: var(--color-accent);
  box-shadow: var(--shadow-control);
  color: var(--color-on-accent);
}

button.look-sheet__done:hover {
  background: var(--color-accent-hover);
}

/* Special looks ------------------------------------------------------ */

.special-looks {
  position: relative;
  grid-column: 1 / -1;
  min-width: 0;
}

.special-looks > .section-caption {
  margin: var(--space-xs) 0 var(--space-md);
}

.special-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.special-row {
  position: relative;
  padding: var(--space-sm) 4px;
  border-top: 1px solid var(--color-border);
}

.special-row:last-child {
  border-bottom: 1px solid var(--color-border);
}

.special-row__main {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 64px;
}

.special-row__mini {
  position: relative;
  flex: none;
  padding: 3px;
  border-radius: 3px;
  background: var(--color-frame-bezel);
}

.special-row__image {
  display: block;
  width: 48px;
  height: 64px;
  background: var(--color-image-backdrop);
  background-image: linear-gradient(
    110deg,
    color-mix(in srgb, var(--color-text) 4%, transparent) 0 40%,
    color-mix(in srgb, var(--color-text) 9%, transparent) 50%,
    color-mix(in srgb, var(--color-text) 4%, transparent) 60% 100%
  );
}

.special-row__text {
  flex: 1 1 auto;
  min-width: 0;
}

.special-row__tag {
  display: inline-block;
  margin-right: 6px;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--color-secondary);
  font-size: 11px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  vertical-align: 1px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.special-row .look-edit {
  flex: none;
}

/* In a row, the open no-script table drops below the row's text. */
.special-row .look-edit[open] {
  flex: 1 1 100%;
}

.special-row__main:has(> .look-edit[open]) {
  flex-wrap: wrap;
}

a.special-row__remove {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  border-color: var(--color-border);
  background: transparent;
  color: var(--color-text);
}

a.special-row__remove:hover {
  background: color-mix(in srgb, var(--color-text) 4.5%, transparent);
}

.special-looks__order {
  margin: var(--space-md) 0 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* Add a special look ----------------------------------------------- */

.special-add__summary {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 64px;
  font-weight: var(--weight-semibold);
  color: var(--color-accent);
  list-style: none;
  cursor: pointer;
}

.special-add__summary::-webkit-details-marker {
  display: none;
}

.special-add__plus {
  display: grid;
  flex: none;
  place-items: center;
  width: 54px;
  height: 70px;
  border: 1.5px dashed color-mix(in srgb, var(--color-accent) 55%, transparent);
  border-radius: 4px;
}

.special-add[open] > .special-add__summary {
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

.special-add__panel {
  padding: var(--space-sm) 0 var(--space-md);
}

.special-add__form {
  gap: var(--space-md);
}

.special-add__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
}

.special-add__who,
.special-add__look {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  min-width: 0;
}

.special-add__who .theme-form {
  min-height: 44px;
}

.special-add__who .theme-form input[type="radio"] + label {
  flex: 1 1 0;
  height: 38px;
  font-size: var(--font-label-size);
}

.special-add__who input[type="text"] {
  font-family: var(--font-mono);
  text-transform: uppercase;
}

/* The preview heads the form: the framed picture takes 44% of a phone sheet's
 * content width (9rem on a wide sheet) with its caption beside it. */
.special-add__preview {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.special-add__preview .special-row__mini {
  width: 44%;
  padding: 4px;
  border-radius: 5px;
}

@media (min-width: 700px) {
  .special-add__preview .special-row__mini {
    width: 9rem;
  }
}

.special-add__preview .special-row__image {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
}

.special-add__preview .text-label {
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.special-add__look-label {
  margin: 0;
}

/* The three choices name themselves; the "Look" heading only stays for
 * assistive technology once the picker replaces the table. */
.special-add.is-enhanced .special-add__look-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.rule-suggestions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.rule-suggestions[hidden],
.rule-suggestions__group[hidden] {
  display: none;
}

.rule-suggestions > .text-label {
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.rule-suggestions__group {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

button.rule-suggestion-chip {
  height: 36px;
  border-color: transparent;
  border-radius: 18px;
  background: var(--color-secondary);
  font-weight: var(--weight-regular);
}

/* With scripts, the add form is a dialog anchored beside the list. */
.special-add.is-enhanced[open] > .special-add__panel {
  position: absolute;
  right: calc(100% + var(--space-md));
  bottom: 0;
  z-index: 51;
  width: min(38rem, calc(100vw - 2 * var(--space-md)));
  padding: var(--space-md) 20px;
  border: 1px solid var(--color-border);
  border-radius: 14px;
  background: var(--color-dominant);
  box-shadow: var(--shadow-pop);
}

@media (min-width: 700px) {
  .special-add.is-enhanced .special-add__cols {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-lg);
  }
}

@media (min-width: 1200px) {
  .look-card__grid {
    grid-template-columns: minmax(0, 15rem) minmax(0, 15rem) minmax(17rem, 1fr);
    gap: var(--space-xl) var(--space-lg);
  }

  .special-looks {
    grid-column: auto;
  }

  /* Without scripts, an open table or add form needs the card's width. */
  .special-looks:has(.special-add[open]:not(.is-enhanced)),
  .special-looks:has(.look-edit[open]) {
    grid-column: 1 / -1;
  }

  .look-sheet--calendar {
    top: 0;
    right: calc(100% + var(--space-md));
    left: auto;
    margin-top: 0;
  }
}

@media (max-width: 1199.98px) {
  .special-add.is-enhanced[open] > .special-add__panel {
    right: auto;
    left: 0;
    top: auto;
    bottom: calc(100% - 64px);
  }
}

/* Phones: both dialogs become bottom sheets over a dimmed page. */
@media (max-width: 699.98px) {
  .look-scrim {
    background: var(--color-scrim);
  }

  .look-sheet,
  .special-add.is-enhanced[open] > .special-add__panel {
    position: fixed;
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 51;
    width: auto;
    max-width: none;
    max-height: 88vh;
    margin: 0;
    padding: 22px var(--space-md) calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
    border-radius: 18px 18px 0 0;
  }

  .special-add__foot button {
    height: 44px;
  }

  .special-add__foot {
    position: sticky;
    bottom: 0;
    padding-top: var(--space-sm);
    background: var(--color-dominant);
  }

  .look-sheet::before,
  .special-add.is-enhanced[open] > .special-add__panel::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    width: 40px;
    height: 4px;
    margin-left: -20px;
    border-radius: 2px;
    background: var(--color-border);
  }

  .look-card__grid {
    gap: var(--space-lg) var(--space-sm);
  }

  .look-frame__meta {
    flex-direction: column;
    gap: var(--space-xs);
  }

  .look-frame__picture {
    padding: 5px;
  }

  /* The colour's name stays for assistive technology; its ink dot names
   * the row on screen, so five 44px cells fit a 360px phone. */
  .look-table__colour {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .look-table__ink {
    margin-right: 0;
  }

  .look-table th[scope="row"] {
    padding-right: 2px;
  }

  .look-table th {
    padding: 0;
    font-size: 11px;
  }

  .look-cell {
    width: 44px;
  }
}

/* Audit log: the filter chips as plain links, always visible, on one line that
 * scrolls sideways on a narrow phone; the pager links keep a full touch
 * target. */
.audit-chips {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  margin: 0 0 var(--space-md);
}

.audit-chips .filter-chip {
  flex: 1 0 auto;
  min-width: 44px;
  scroll-snap-align: start;
  text-decoration: none;
}

.audit-chips .filter-chip:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
  border-radius: var(--radius-control);
}

.audit-pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.audit-pager .text-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
}

/* Shared modal dialog (companion/ui_modal.py). One server-rendered
 * <dialog>: a centred card from 700px up, a bottom sheet below it.
 * color is explicit because a showModal() dialog does not reliably
 * inherit it. modal.js adds .is-modal when it promotes the dialog; until
 * then (scripts blocked) :target or a server-rendered `open` shows it as
 * an in-page card over a box-shadow scrim. */
dialog.modal {
  box-sizing: border-box;
  width: min(480px, calc(100% - 48px));
  max-width: none;
  max-height: 88vh;
  margin: auto;
  padding: var(--space-lg);
  overflow-y: auto;
  background: var(--color-dominant);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
}

dialog.modal::backdrop {
  background: var(--color-scrim);
}

/* The fade and rise on open, as .lightbox does; close is instant so a
 * closed dialog is never hit-testable meanwhile. */
dialog.modal.is-modal {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--motion-fast) ease-out,
    transform var(--motion-fast) ease-out;
}

dialog.modal.is-modal[open] {
  opacity: 1;
  transform: none;
}

@starting-style {
  dialog.modal.is-modal[open] {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* No script: a fixed card, and a scrim made from a huge shadow. */
.modal:target:not(.is-modal),
.modal[open]:not(.is-modal) {
  display: block;
  position: fixed;
  inset: 0;
  height: fit-content;
  z-index: 60;
  box-shadow: 0 0 0 100vmax var(--color-scrim), var(--shadow-pop);
}

.modal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
}

.modal__title {
  margin: 0;
  overflow-wrap: anywhere;
}

.modal__subtitle {
  margin: var(--space-xs) 0 0;
  overflow-wrap: anywhere;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

a.modal__close {
  display: grid;
  flex: none;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: calc(-1 * var(--space-sm)) calc(-1 * var(--space-sm)) 0 0;
  border-radius: 50%;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  text-decoration: none;
}

.modal__body {
  display: grid;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.modal input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),
.modal select,
.modal textarea {
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
  min-height: 44px;
}

.modal__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

dialog.modal button {
  height: auto;
  min-height: 44px;
  min-width: 44px;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-label-size);
}

a.modal__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  text-align: center;
}

@media (min-width: 700px) {
  .modal__actions {
    flex-direction: row-reverse;
    justify-content: flex-start;
    align-items: center;
  }
}

@media (max-width: 699.98px) {
  dialog.modal,
  .modal:target:not(.is-modal),
  .modal[open]:not(.is-modal) {
    position: fixed;
    inset: auto 0 0;
    width: 100%;
    margin: 0;
    padding: 22px var(--space-md) calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
    border-radius: 18px 18px 0 0;
  }

  dialog.modal.is-modal {
    transform: translateY(24px);
  }

  dialog.modal.is-modal[open] {
    transform: none;
  }

  dialog.modal::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    width: 40px;
    height: 4px;
    margin-left: -20px;
    border-radius: 2px;
    background: var(--color-border);
  }
}

/* Shared entity list (companion/ui_entity_list.py): rows of an optional
 * dot, a text block and one action. A phone shows one column; from 960px
 * the same DOM becomes columns, with the dot in the row's left padding.
 * Text always wraps (no ellipsis, no monospace) so nothing clips. */
ul.entity-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.entity-count {
  margin: 0 0 var(--space-sm);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

li.entity-row {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

li.entity-row:last-child {
  border-bottom: 0;
}

li.entity-row--no-dot {
  grid-template-columns: minmax(0, 1fr) auto;
}

.entity-row__dot {
  display: flex;
  align-items: center;
  align-self: start;
  margin-top: 6px;
}

.entity-row__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
}

.entity-row__title {
  font-size: var(--font-body-size);
  font-weight: var(--weight-semibold);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

a.entity-row__title {
  position: relative;
  color: var(--color-accent);
}

/* The name link is one text line high; this widens its tap area to 44px
 * without moving anything (the row padding is 16px, so it stays inside the row). */
a.entity-row__title::before {
  content: "";
  position: absolute;
  inset: -10px 0;
}

.entity-row__meta {
  overflow-wrap: anywhere;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.entity-row__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* An Account frame row: the CSS-drawn frame first, then the name, the
 * hardware or owner line and the status dot with its last-seen word. */
li.entity-row.account-frame {
  grid-template-columns: auto minmax(0, 1fr) auto;
  padding-left: var(--space-md);
}

.account-frame .entity-row__dot {
  position: static;
  align-self: center;
  margin-top: 0;
}

.account-frame__pic {
  display: block;
  box-sizing: border-box;
  width: 44px;
  height: 58px;
  padding: 3px;
  background: var(--color-frame-bezel);
  border: 1px solid var(--color-frame-bezel-hi);
  border-radius: 5px;
}

.account-frame__pic i {
  display: block;
  height: 100%;
  background: var(--color-image-backdrop);
  border-radius: 1px;
}

.account-frame__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

@media (min-width: 960px) {
  li.entity-row.account-frame {
    grid-template-columns: auto minmax(0, 1fr) auto;
    padding-left: var(--space-md);
  }

  .account-frame .entity-row__text {
    display: flex;
    grid-column: auto;
  }

  .account-frame .entity-row__meta,
  .account-frame .entity-row__pills {
    grid-column: auto;
  }
}

details.row-menu {
  position: relative;
}

.row-menu__toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  list-style: none;
  cursor: pointer;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.row-menu__toggle::-webkit-details-marker {
  display: none;
}

/* The global summary chevron would sit next to the kebab icon. */
.row-menu__toggle::before {
  content: none;
}

.row-menu__toggle:hover,
details.row-menu[open] > .row-menu__toggle {
  background: var(--color-control-wash);
}

.row-menu__panel {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 30;
  box-sizing: border-box;
  min-width: 12rem;
  max-width: calc(100vw - 2 * var(--space-md));
  padding: var(--space-xs) 0;
  background: var(--color-dominant);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
}

.row-menu__item {
  display: flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  color: var(--color-text);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.row-menu__item:hover {
  background: var(--color-control-wash);
}

.row-menu__item--destructive {
  color: var(--color-status-error);
}

@media (min-width: 960px) {
  ul.entity-list {
    max-width: 56rem;
  }

  li.entity-row {
    grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) auto;
    padding-left: calc(var(--space-md) + 12px + var(--space-sm));
  }

  li.entity-row--no-dot {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr) auto;
    padding-left: var(--space-md);
  }

  .entity-row__dot {
    position: absolute;
    left: var(--space-md);
    top: var(--space-md);
  }

  .entity-row__text {
    display: grid;
    grid-column: 1 / 3;
    grid-template-columns: subgrid;
    column-gap: var(--space-sm);
    align-items: center;
  }

  .entity-row__meta {
    grid-column: 2;
  }

  .entity-row__pills {
    grid-column: 2;
  }
}

/* Console page header with a primary action: the title and the action
 * share the first row, the purpose line spans both. */
.page-header:has(> .page-header__actions) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-md);
  align-items: center;
}

.page-header:has(> .page-header__actions) > .page-header__purpose {
  grid-column: 1 / -1;
}

.page-header__actions {
  grid-column: 2;
  grid-row: 1;
}

a.page-header__action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  border-radius: 6px;
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  white-space: nowrap;
}

a.page-header__action:hover {
  background: var(--color-accent-hover);
}

/* Add a frame: the link under the frame list and in the empty state, and the
 * page that takes the pairing code. */
.pair-actions {
  margin: var(--space-md) 0 0;
}

a.pair-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  border-radius: 6px;
  border: 1px solid var(--color-border);
  color: var(--color-accent);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  text-decoration: none;
}

a.pair-link--primary {
  background: var(--color-accent);
  border-color: transparent;
  color: var(--color-on-accent);
}

a.pair-link--primary:hover {
  background: var(--color-accent-hover);
}

.pair-form {
  display: grid;
  gap: var(--space-md);
  max-width: 28rem;
}

.pair-form input {
  box-sizing: border-box;
  width: 100%;
}

.pair-form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}

.pair-form__actions button {
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
}

a.pair-form__cancel {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
  color: var(--color-accent);
}

/* A labelled control inside a dialog, with its help or error text below. */
.modal__field > p {
  margin: var(--space-xs) 0 0;
}

/* The collapsed block of pending and retired frames under the selected frame. */
details.fleet-other {
  margin-top: var(--space-md);
}

/* The selected frame's header: its name and owner, and its actions menu. */
.fleet-detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin: 0 0 var(--space-md);
}

.fleet-detail-head__text {
  min-width: 0;
}

.fleet-detail-head__text > * {
  margin: 0;
  overflow-wrap: anywhere;
}

/* The "Manage user" button of a Users row: a 44px bordered text link. */
a.entity-row__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  color: var(--color-accent);
  font-size: var(--font-label-size);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  text-align: center;
}

a.entity-row__button:hover {
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
}

.quiet-timezone-note {
  margin: var(--space-md) 0 0;
}

/* Personalisation ------------------------------------------------------
 * One segmented navigation (Departures, Arrivals, Special looks, Calendar:
 * plain links, one view each) over the current view. Departures and Arrivals
 * are the look picker: the frame's picture and the three choices. */

.personalisation {
  display: block;
}

/* The pill bar scrolls sideways on a phone rather than wrapping. A script
 * (theme-preview.js) adds .is-enhanced and slides the thumb under the
 * current link; without one the current link carries the highlight. */
.personalisation-nav {
  position: relative;
  display: flex;
  gap: var(--space-xs);
  margin: 0 0 var(--space-lg);
  padding: var(--space-xs);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  border-radius: var(--radius-card);
  background: var(--color-secondary);
  scrollbar-width: none;
}

.personalisation-nav::-webkit-scrollbar {
  display: none;
}

.personalisation-nav__thumb {
  display: none;
}

.personalisation-nav__link {
  position: relative;
  z-index: 1;
  scroll-snap-align: center;
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-sm);
  min-height: 44px;
  padding: 0 var(--space-md);
  border-radius: var(--radius-control);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  white-space: nowrap;
}

.personalisation-nav__link svg {
  flex: none;
  width: 20px;
  height: 20px;
}

/* Arrivals reuses the plane, nose down. */
.personalisation-nav__link[data-pers-view="arrivals"] svg {
  transform: rotate(90deg);
}

.personalisation-nav__link:hover {
  color: var(--color-text);
}

.personalisation-nav__link[aria-current="page"] {
  background: var(--color-dominant);
  box-shadow: var(--shadow-card);
  color: var(--color-accent);
}

.personalisation-nav.is-enhanced .personalisation-nav__thumb {
  position: absolute;
  top: var(--space-xs);
  bottom: var(--space-xs);
  left: 0;
  display: block;
  width: 0;
  border-radius: var(--radius-control);
  background: var(--color-dominant);
  box-shadow: var(--shadow-card);
}

.personalisation-nav.is-enhanced .personalisation-nav__thumb.is-sliding {
  transition: transform var(--motion-fast) ease, width var(--motion-fast) ease;
}

.personalisation-nav.is-enhanced .personalisation-nav__link[aria-current="page"] {
  background: transparent;
  box-shadow: none;
}

.personalisation__view {
  min-width: 0;
  max-width: 56rem;
}

.personalisation-card {
  scroll-margin-top: var(--space-lg);
}

.personalisation-card__current {
  margin: 0 0 var(--space-md);
}

/* The card title already names the Special looks list. */
.personalisation-card .special-looks__title {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* In the card, "New special look" is the section's one primary action. */
.personalisation-card .special-add:not([open]) > .special-add__summary {
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--space-md);
  gap: var(--space-sm);
  border-radius: var(--radius-control);
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.personalisation-card .special-add:not([open]) > .special-add__summary:hover {
  background: var(--color-accent-hover);
}

.personalisation-card .special-add:not([open]) .special-add__plus {
  width: auto;
  height: auto;
  border: 0;
}

.personalisation-card .special-row--add,
.personalisation-card .special-row--add:last-child {
  border: 0;
  padding-top: var(--space-md);
}

.personalisation-card .special-looks .empty-state--compact {
  margin-bottom: var(--space-sm);
}

.personalisation-switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.personalisation-switch .personalisation-card__current {
  margin: 0;
}

.look-pick__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.look-pick__card {
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-sm);
}

.look-pick__preview {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: contain;
  border-radius: 4px;
  background: var(--color-image-backdrop);
}

.look-pick__name {
  min-width: 0;
  overflow-wrap: anywhere;
}

.look-pick__arrivals {
  margin-top: var(--space-lg);
}

button.look-pick__customise {
  height: auto;
  max-width: 100%;
  padding-block: 6px;
  white-space: normal;
  text-align: left;
}

/* The look views (Departures, Arrivals) ---------------------------------- */

.look-picker {
  --js-gate-display: block;
  max-width: 35rem;
  margin-top: var(--space-md);
}

/* With a script the picker below replaces the look cards, the "Change"
 * table and the "Customise" button; without one they are the way in. */
.js .look-view .look-pick__grid,
.js .look-view .look-edit,
.js .look-view .look-pick__customise {
  display: none;
}

.js .look-view .look-panel > .look-frame {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: var(--space-md);
  align-items: flex-start;
}

.js .look-view .look-panel > .look-frame > .look-frame__meta,
.js .look-view .look-pick > .look-frame__meta {
  margin-top: 0;
}

.js .look-view .look-panel > .look-frame > .field-error {
  grid-column: 1 / -1;
}

.look-panel[hidden] {
  display: none;
}

.look-sheet__head[hidden],
button.look-sheet__done[hidden] {
  display: none;
}

/* The inline picker is the sheet without its dialog behaviour. */
.look-sheet[data-look-inline] {
  position: static;
  z-index: auto;
  width: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: var(--space-md) 20px;
  overflow: visible;
  border-radius: var(--radius-card);
  box-shadow: none;
}

.look-sheet[data-look-inline]::before {
  display: none;
}

.look-sheet[data-look-inline] .look-sheet__foot button {
  min-height: 48px;
}

button.look-sheet__save {
  border-color: transparent;
  background: var(--color-accent);
  box-shadow: var(--shadow-control);
  color: var(--color-on-accent);
}

button.look-sheet__save:hover {
  background: var(--color-accent-hover);
}

/* On a phone the large picture is smaller and its read-back sits beside it,
 * so the first row of looks is on the first screen. */
@media (max-width: 699.98px) {
  .js .look-view .look-panel > .look-frame {
    grid-template-columns: 7.5rem minmax(0, 1fr);
  }

  .look-pick {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    gap: var(--space-md);
    align-items: flex-start;
  }

  .look-pick > .look-frame__anchor {
    max-width: 7.5rem;
  }

  .look-pick > .look-frame__meta {
    margin-top: 0;
    max-width: none;
  }

  .look-pick > .look-pick__grid,
  .look-pick > .field-error {
    grid-column: 1 / -1;
  }
}

@media (min-width: 700px) {
  /* The add dialog opens over its own card, never beside it. */
  .personalisation-card .special-add.is-enhanced[open] > .special-add__panel {
    top: auto;
    right: auto;
    bottom: calc(100% - 64px);
    left: 0;
  }

  .look-pick__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* Inside a card the look sheet opens under the picture it changes. */
  .personalisation-card .look-sheet {
    top: 100%;
    right: auto;
    left: 0;
    margin-top: var(--space-sm);
  }
}

/* From 960px a look view is two columns, the picture on the left and the
 * picker on the right, both on the first screen of a laptop. The picture is
 * as wide as its column and the screen height allow (3:4). */
@media (min-width: 960px) {
  .js .personalisation__view:has(.look-view) {
    max-width: none;
  }

  .js .look-view {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 28rem);
    gap: var(--space-lg);
    align-items: flex-start;
  }

  .js .look-view .look-panel > .look-frame {
    display: block;
  }

  .look-view .look-frame__anchor {
    max-width: min(100%, calc((100vh - 21rem) * 0.75));
    margin-inline: auto;
  }

  .look-view .look-frame__meta {
    max-width: min(100%, calc((100vh - 21rem) * 0.75));
    margin-inline: auto;
  }

  .js .look-view .look-picker {
    max-width: none;
    margin-top: 0;
  }
}

@media (min-width: 1280px) {
  .look-pick__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Airline collection: a one-line header (title, a thin progress bar with
 * one short sentence, small round badge icons), one control bar and a grid
 * of picture cards. The dimming of a card that has not been seen applies
 * to the picture only, never to its text. */
.page-header:has(> .collection-head__status) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-lg);
  margin-bottom: var(--space-md);
}

.page-header:has(> .collection-head__status) .page-title {
  margin: 0;
}

.collection-head__status {
  display: flex;
  flex: 1 1 360px;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-md);
  min-width: 0;
}

.collection-progress {
  flex: 1 1 auto;
  max-width: 360px;
  min-width: 0;
}

.collection-progress__headline {
  margin: var(--space-xs) 0 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.collection-progress__bar {
  display: block;
  width: 100%;
  height: 6px;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  border-radius: 999px;
  background: var(--color-secondary);
  overflow: hidden;
}

.collection-progress__bar::-webkit-progress-bar {
  background: var(--color-secondary);
}

.collection-progress__bar::-webkit-progress-value {
  background: var(--color-accent);
  border-radius: 999px;
}

.collection-progress__bar::-moz-progress-bar {
  background: var(--color-accent);
  border-radius: 999px;
}

.collection-badges {
  display: flex;
  flex: none;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.collection-badge {
  display: grid;
  flex: none;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
}

.collection-badge--earned {
  background: color-mix(in srgb, var(--color-accent) 14%, var(--color-dominant));
  color: var(--color-accent);
}

.collection-badge--locked {
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--color-text) 40%, transparent);
  box-sizing: border-box;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
}

/* One control bar: the filter field, the segmented show filter, the sort
 * and the legend button on one row where they fit, wrapping cleanly on a
 * phone. Every control is at least 44px tall. */
.collection-controls {
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.collection-controls .filter-bar__search {
  flex: 1 1 150px;
  min-width: 150px;
  max-width: 340px;
}

.collection-controls .filter-bar__chips {
  display: flex;
  flex: 0 1 auto;
  max-width: 100%;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}

.collection-controls .filter-chip {
  flex: 1 0 auto;
  scroll-snap-align: start;
}

/* Label and count side by side, centred in the segment. */
.collection-controls .filter-chip__box {
  flex-direction: row-reverse;
  justify-content: center;
  gap: var(--space-xs);
  padding: 0 var(--space-sm);
  text-align: center;
}

.collection-controls .filter-chip__text {
  overflow: visible;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--font-label-size);
  white-space: nowrap;
}

.collection-controls .filter-chip__count {
  font-size: var(--font-label-size);
}

.collection-sort {
  display: flex;
  flex: 0 1 auto;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  min-height: 44px;
  box-sizing: border-box;
  padding: 0 var(--space-xs) 0 var(--space-sm);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
}

.collection-sort__label {
  flex: none;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.collection-sort select {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  border: 0;
  background-color: transparent;
}

.collection-controls__apply {
  min-height: 48px;
  padding: 0 var(--space-md);
  font-size: var(--font-body-size);
}

/* With a script the sort and the chips submit on change. */
.js .collection-controls__apply {
  display: none;
}

.collection-legend .row-menu__toggle {
  border: 1px solid var(--color-border);
}

.collection-legend__panel {
  width: max-content;
  padding: var(--space-sm) var(--space-md);
}

.rarity-legend {
  display: grid;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.rarity-legend__tier {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}

@media (max-width: 699.98px) {
  .page-header:has(> .collection-head__status) .page-title {
    font-size: 1.625rem;
  }

  .collection-head__status {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-xs);
  }

  .collection-progress {
    max-width: none;
  }

  /* The badge icons scroll sideways rather than wrapping. */
  .collection-badges {
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
  }

  .collection-controls .filter-bar__search {
    flex: 1 1 100%;
    max-width: none;
  }

  .collection-controls .filter-bar__chips {
    flex: 1 1 100%;
  }

  .collection-sort {
    flex: 1 1 0;
  }
}

.collection-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md);
  margin: 0 0 var(--space-lg);
  padding: 0;
  list-style: none;
}

.collection-item {
  min-width: 0;
}

.collection-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  height: 100%;
  box-sizing: border-box;
  min-height: 44px;
  padding: var(--space-sm);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  color: var(--color-text);
  text-decoration: none;
}

.collection-card:hover,
.collection-card:focus-visible {
  border-color: transparent;
  box-shadow: var(--shadow-card-hover);
  color: var(--color-text);
}

.collection-card__art {
  display: block;
}

.collection-card__image {
  display: block;
  width: 100%;
  height: auto;
}

.collection-card__blank {
  margin-bottom: 0;
}

.collection-card--unseen .collection-card__art {
  opacity: 0.45;
  filter: grayscale(1);
}

.collection-card__name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.collection-card__seen {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.collection-card__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.rarity-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 20px;
  padding: 0 8px;
  border: 1px solid color-mix(in srgb, var(--color-text) 30%, transparent);
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  color: var(--color-text);
}

.rarity-pill--occasional {
  color: var(--color-rarity-occasional);
  border-color: var(--color-rarity-occasional);
  background: color-mix(in srgb, var(--color-rarity-occasional) 10%, var(--color-dominant));
}

.rarity-pill--rare {
  color: var(--color-rarity-rare);
  border-color: var(--color-rarity-rare);
  background: color-mix(in srgb, var(--color-rarity-rare) 10%, var(--color-dominant));
}

.rarity-pill--occasional .rarity-dot {
  background: var(--color-rarity-occasional);
}

.rarity-pill--rare .rarity-dot {
  background: var(--color-rarity-rare);
}

.collection-card--occasional {
  border-top: 3px solid var(--color-rarity-occasional);
}

.collection-card--rare {
  border-top: 3px solid var(--color-rarity-rare);
}

.type-chips {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.type-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
}

.type-chip__tag {
  font-size: 12px;
  font-weight: var(--weight-semibold);
}

.type-chip--occasional {
  color: var(--color-rarity-occasional);
  border-color: var(--color-rarity-occasional);
}

.type-chip--rare {
  color: var(--color-rarity-rare);
  border-color: var(--color-rarity-rare);
}

.rarity-dots {
  display: inline-flex;
  gap: 3px;
}

.rarity-dot {
  display: block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-text);
}

/* The detail sheet fits the viewport: the head with its close button and
 * the Close foot stay in place and only the body scrolls. */
dialog.collection-sheet[open],
.collection-sheet:target:not(.is-modal) {
  display: flex;
  flex-direction: column;
  max-height: 85vh;
  max-height: 85dvh;
  overflow: hidden;
}

.collection-sheet .modal__head {
  flex: none;
}

.collection-sheet .modal__body {
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.collection-sheet .modal__actions {
  flex: none;
  margin-top: var(--space-sm);
}

.collection-sheet__art {
  display: flex;
  justify-content: center;
}

.collection-sheet__art .collection-card__image {
  width: auto;
  max-width: 100%;
  max-height: 160px;
}

.collection-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm) var(--space-md);
  margin: 0;
}

.collection-facts > div {
  display: grid;
  align-content: start;
  min-width: 0;
}

.collection-facts > .collection-facts__wide {
  grid-column: 1 / -1;
}

.collection-facts dt {
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.collection-facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

@media (max-width: 699.98px) {
  .collection-sheet__art .collection-card__image {
    max-height: 72px;
  }
}

.collection-sheet__note {
  margin: 0;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

@media (min-width: 700px) {
  .collection-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 1280px) {
  .collection-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Users table ----------------------------------------------------------
 * One <table> of users. Below 70rem every row is a card: avatar and
 * email with the "..." menu on top, the role select and the Active switch
 * under it, frames and last sign-in at the bottom. */
.users-table,
.users-table tbody {
  display: block;
}

.users-table thead {
  display: none;
}

.users-table .users-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "user user"
    "role active"
    "frames seen";
  position: relative;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
  padding: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.users-cell {
  min-width: 0;
  padding: 0;
  text-align: left;
  font-weight: var(--weight-regular);
}

.users-cell--user {
  grid-area: user;
  padding-right: 44px;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.users-cell--role { grid-area: role; }
.users-cell--active { grid-area: active; justify-self: end; }
.users-cell--menu {
  position: absolute;
  top: var(--space-xs);
  right: var(--space-sm);
}

.users-cell--frames {
  grid-area: frames;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.users-cell--seen {
  grid-area: seen;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.users-avatar {
  display: grid;
  flex: none;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent);
  font-weight: var(--weight-semibold);
}

.users-who {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.users-email {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.users-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.users-pills:empty {
  display: none;
}

.users-role-form,
.users-active-form {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
}

.users-role-form select {
  min-width: 0;
  max-width: 100%;
}

.users-role-fixed {
  display: block;
  font-weight: var(--weight-semibold);
}

.users-guard {
  display: block;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.users-table button.users-role-save {
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-sm);
  font-size: var(--font-label-size);
}

.users-table button.switch:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.users-menu__form {
  margin: 0;
}

/* A submit button used as a menu entry: undo the primary-button look. */
button.row-menu__item {
  width: 100%;
  box-sizing: border-box;
  justify-content: flex-start;
  background: none;
  border: 0;
  box-shadow: none;
  font: inherit;
  text-align: left;
  color: var(--color-text);
}

button.row-menu__item:hover {
  background: var(--color-control-wash);
  box-shadow: none;
}

button.row-menu__item--destructive,
button.row-menu__item--destructive:hover {
  color: var(--color-status-error);
}

/* On a phone the invite action sits under the title at full width, so a long
 * localised title never meets the button; from 700px it shares the title's row. */
@media (max-width: 699.98px) {
  .page-header:has(> .page-header__actions--stack) {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-md);
  }

  .page-header__actions--stack {
    grid-column: 1;
    grid-row: 2;
  }

  .page-header__actions--stack a.page-header__action {
    display: flex;
    width: 100%;
  }
}

@media (min-width: 70rem) {
  .users-table {
    display: table;
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--color-dominant);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
  }

  .users-table thead {
    display: table-header-group;
  }

  .users-table tbody {
    display: table-row-group;
  }

  .users-table .users-row {
    display: table-row;
    position: static;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }

  .users-cell {
    display: table-cell;
    vertical-align: middle;
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
  }

  .users-col {
    display: table-cell;
    vertical-align: middle;
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
    font-size: var(--font-label-size);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: left;
    color: color-mix(in srgb, var(--color-text) 70%, transparent);
  }

  .users-table tbody .users-row:last-child .users-cell {
    border-bottom: 0;
  }

  .users-cell--user {
    display: table-cell;
    padding-right: var(--space-md);
  }

  .users-col--user {
    width: 34%;
  }

  .users-avatar {
    display: inline-grid;
    margin-right: var(--space-sm);
  }

  .users-who {
    display: inline-flex;
    vertical-align: middle;
    max-width: calc(100% - 48px);
  }

  .users-cell--active .users-active-label {
    display: none;
  }

  .users-cell--menu {
    position: static;
  }
}

/* Airlines editor table ------------------------------------------------
 * One <tbody> per airline: its row and, behind the native chevron, a
 * sub-row per aircraft type. Below 70rem every row is a card. */
.airlines-table,
.airlines-table tbody {
  display: block;
}

.airlines-table thead {
  display: none;
}

.airline-group {
  margin-bottom: var(--space-md);
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.airline-group[hidden] {
  display: none;
}

.airline-group--gap {
  background: color-mix(in srgb, var(--color-status-warn) 12%, var(--color-dominant));
  border-color: var(--color-status-warn);
}

.airlines-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 44px;
  grid-template-areas:
    "name name menu"
    "prefixes views views"
    "art seen seen";
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-md);
}

.airlines-subrow {
  display: none;
  grid-template-columns: minmax(0, 1fr) auto 44px;
  grid-template-areas:
    "name views menu"
    "art art art";
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-md);
  border-top: 1px dashed var(--color-border);
  background: var(--color-secondary);
  border-radius: 0 0 var(--radius-card) var(--radius-card);
}

.airline-group:has(details.airlines-expand[open]) .airlines-subrow {
  display: grid;
}

.airlines-cell {
  min-width: 0;
  padding: 0;
  text-align: left;
  font-weight: var(--weight-regular);
}

.airlines-cell--name { grid-area: name; }

.airlines-namebox {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
}
.airlines-cell--prefixes {
  grid-area: prefixes;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.airlines-cell--art { grid-area: art; }

.airlines-cell--views {
  grid-area: views;
  justify-self: end;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.airlines-cell--seen {
  grid-area: seen;
  justify-self: end;
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.airlines-cell--menu { grid-area: menu; justify-self: end; }

.airlines-cell--type {
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  padding-left: calc(44px + var(--space-xs));
}


.airlines-expand {
  flex: none;
  width: 44px;
}

.airlines-expand--none {
  display: block;
  height: 44px;
}

.airlines-expand > summary {
  justify-content: center;
  width: 44px;
  min-height: 44px;
}

.airlines-expand > summary::before {
  margin-right: 0;
}

.airlines-name {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  margin: 0;
}

.airlines-name--fixed {
  display: block;
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.airlines-name input[type="text"] {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  font-weight: var(--weight-semibold);
  background: transparent;
  border: 1px solid transparent;
  border-bottom: 1px dashed var(--color-border);
  border-radius: var(--radius-control);
}

.airlines-name input[type="text"]:hover,
.airlines-name input[type="text"]:focus {
  background: var(--color-dominant);
  border-color: var(--color-border);
}

.airlines-art {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
}

.airlines-thumb {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 44px;
  min-height: 44px;
  box-sizing: border-box;
  padding: var(--space-xs);
  background: var(--color-secondary);
  border-radius: var(--radius-control);
  color: var(--color-accent);
  text-decoration: none;
  cursor: pointer;
}

.airlines-thumb:focus-within {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.airlines-thumb .airline-card__placeholder {
  width: 88px;
  margin: 0;
}

.airlines-thumb__image {
  display: block;
  width: 88px;
  height: auto;
  aspect-ratio: 450 / 132;
  object-fit: contain;
}

/* The "Change artwork" menu item is a label around the hidden file input. */
.airlines-art label.row-menu__item {
  cursor: pointer;
}

.airlines-art label.row-menu__item:focus-within {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.airlines-uses {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

.airlines-name__save,
.airlines-art__upload {
  min-height: 44px;
  min-width: 44px;
  padding: 0 var(--space-sm);
  font-size: var(--font-label-size);
}

.airlines-menu__form {
  margin: 0;
}

.airlines-cell--action {
  align-self: center;
}

a.airlines-name-it {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0 var(--space-md);
  box-sizing: border-box;
  font-weight: var(--weight-semibold);
  text-decoration: none;
}

@media (min-width: 70rem) {
  .airlines-table {
    display: table;
    table-layout: fixed;
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--color-dominant);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
  }

  .airlines-table thead {
    display: table-header-group;
  }

  .airlines-table tbody {
    display: table-row-group;
  }

  .airline-group {
    margin: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }

  .airline-group--gap {
    background: color-mix(in srgb, var(--color-status-warn) 12%, var(--color-dominant));
  }

  .airlines-row {
    display: table-row;
    padding: 0;
  }

  .airlines-subrow {
    padding: 0;
    background: var(--color-secondary);
    border: 0;
  }

  .airline-group:has(details.airlines-expand[open]) .airlines-subrow {
    display: table-row;
  }

  .airlines-cell {
    display: table-cell;
    vertical-align: middle;
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
  }

  .airlines-cell--views,
  .airlines-cell--seen,
  .airlines-cell--menu {
    justify-self: auto;
  }

  .airlines-col {
    display: table-cell;
    vertical-align: middle;
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
    font-size: var(--font-label-size);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: left;
    color: color-mix(in srgb, var(--color-text) 70%, transparent);
  }

  .airlines-col--name { width: 32%; }
  .airlines-col--prefixes { width: 14%; }
  .airlines-col--art { width: 22%; }
  .airlines-col--views { width: 11%; }
  .airlines-col--menu { width: 64px; }
}

/* ------------------------------------------------------------------
 * Sky and motion language (appended last on purpose: equal-specificity
 * rules here win over the base rules above).
 * ------------------------------------------------------------------ */

/* From the desktop breakpoint the page title grows to 44-48px. */
@media (min-width: 960px) {
  :root {
    --font-page-title-size: clamp(2.75rem, 2.2rem + 1.6vw, 3rem);
  }
}

/* The entrance of a card: a fade and a short rise, finite, painted by the
 * animations only (fill-mode backwards, never forwards), so once they end
 * the element is back in its plain layout state and creates no stacking
 * context that could trap a dropdown. Siblings stagger through
 * --reveal-index. The rise moves in whole pixels (steps) on purpose: a
 * fractional translate under a box that already sits at a fractional
 * offset makes the browser hit-test the box one pixel short, so a 44px
 * control inside would answer to 43px while the card is still arriving.
 * The fade keeps the smooth easing in its own animation. The global
 * reduced-motion rule above zeroes duration and delay for every animation. */
@keyframes skypane-reveal {
  from {
    transform: translateY(12px);
  }

  to {
    transform: none;
  }
}

@keyframes skypane-reveal-fade {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.reveal,
.dashboard-main .page-section {
  animation:
    skypane-reveal var(--motion-enter) steps(12, end) backwards,
    skypane-reveal-fade var(--motion-enter) var(--ease-out) backwards;
  animation-delay: calc(var(--reveal-index, 0) * var(--motion-stagger));
}

.reveal:nth-child(2),
.dashboard-main .page-section:nth-child(2) {
  --reveal-index: 1;
}

.reveal:nth-child(3),
.dashboard-main .page-section:nth-child(3) {
  --reveal-index: 2;
}

.reveal:nth-child(4),
.dashboard-main .page-section:nth-child(4) {
  --reveal-index: 3;
}

.reveal:nth-child(5),
.dashboard-main .page-section:nth-child(5) {
  --reveal-index: 4;
}

.reveal:nth-child(n + 6),
.dashboard-main .page-section:nth-child(n + 6) {
  --reveal-index: 5;
}

/* A table or an empty state that sits straight on the sky is a card. Its
 * solid fill is the fallback of the glass below; the edge covers a scrolling
 * table used to paint in the page colour would show as opaque bands over it,
 * so the card fill replaces them. They enter like the other cards. */
.dashboard-main > .empty-state {
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  animation:
    skypane-reveal var(--motion-enter) steps(12, end) backwards,
    skypane-reveal-fade var(--motion-enter) var(--ease-out) backwards;
}

/* A compact empty state that sits straight on the sky and is followed by more
 * content keeps a gap at least as tall as the entrance rise, so while it arrives
 * (a transformed box paints over its in-flow siblings) it never covers the
 * controls underneath. */
.dashboard-main > .empty-state--compact:not(:last-child) {
  margin-bottom: var(--space-md);
}

.dashboard-main > .data-table-wrap {
  background: var(--color-dominant);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  overflow-x: auto;
  animation:
    skypane-reveal var(--motion-enter) steps(12, end) backwards,
    skypane-reveal-fade var(--motion-enter) var(--ease-out) backwards;
}

/* Frosted glass. Without backdrop-filter support none of this applies and
 * every surface keeps the solid fill its own rule declares (--color-dominant
 * for cards, --color-secondary for the bars). With it, surfaces become
 * translucent over the sky and the overlay surfaces (bars, sheets, dialogs,
 * the login card, Home's state card) also blur what passes behind them.
 * The blur is kept to those few surfaces on purpose; the repeated cards are
 * translucent only. No border-radius here: --radius-card carries it. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .page-section,
  .fleet-block,
  .battery-trend-section,
  .health-rows,
  .airline-group,
  .stat-tile,
  .airline-card,
  .collection-card,
  .frame-tile {
    background: var(--glass-bg);
    border-color: var(--glass-border);
    box-shadow: var(--shadow-glass);
  }

  /* The look picker's cards signal selection with their own box-shadow
   * ring, so an unselected one must stay shadow-free. */
  .runway-card {
    background: var(--glass-bg);
    border-color: var(--glass-border);
  }

  .home-state,
  .login-card,
  .look-sheet,
  .calendar-sheet,
  .lightbox,
  dialog.modal {
    background: var(--glass-bg);
    border-color: var(--glass-border);
    box-shadow: var(--shadow-glass);
    -webkit-backdrop-filter: blur(var(--glass-blur));
    backdrop-filter: blur(var(--glass-blur));
  }

  .dashboard-sidebar,
  .site-header,
  .tab-bar {
    background: var(--glass-bar-bg);
    border-color: var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur));
    backdrop-filter: blur(var(--glass-blur));
  }

  /* The floating save bar scrolls over content like the tab bar does, so it
   * takes the bar glass, without a blur of its own. */
  .dirty-bar {
    background: var(--glass-bar-bg);
    border-color: var(--glass-border);
  }

  /* Tables and empty states that sit straight on the sky become cards. */
  .dashboard-main > .data-table-wrap,
  .dashboard-main > .empty-state {
    background: var(--glass-bg);
    border-color: var(--glass-border);
    box-shadow: var(--shadow-glass);
  }

  /* The airline editor's table is a table only on a wide screen. */
  @media (min-width: 70rem) {
    .airlines-table {
      background: var(--glass-bg);
      border-color: var(--glass-border);
      box-shadow: var(--shadow-glass);
    }
  }
}

/* The glass rule above outranks the cards' own hover shadow: give it back. */
.frame-tile:is(:hover),
.collection-card:is(:hover),
.airline-card:is(:hover) {
  box-shadow: var(--shadow-card-hover);
}

/* Home: personal greeting, plane on a trail, glow ring, card entrance.
 * Appended last: equal-specificity rules here win over the base rules. */

/* The greeting line above the title. Its box is reserved whether or not
 * the script has swapped the wording (one line, two when the frame name
 * follows), so the title never moves. */
.home-greeting {
  margin: 0 0 var(--space-xs);
  display: flex;
  flex-direction: column;
  min-height: calc(var(--font-body-size) * 1.4);
  font-family: var(--font-serif);
  font-size: var(--font-body-size);
  line-height: 1.4;
  color: var(--color-accent);
}

.home-greeting--framed {
  min-height: calc(var(--font-body-size) * 2.8);
}

.home-greeting__frame {
  font-family: var(--font-ui);
  font-size: var(--font-label-size);
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}

/* The plane's trail under the title: a dotted curve and a small plane at
 * its end. The box keeps the viewBox's 320x56 ratio, so it reserves its
 * height before it paints. */
.home-trail {
  margin: 0 0 var(--space-sm);
  max-width: 320px;
}

.home-trail__svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 320 / 56;
  overflow: visible;
}

.home-trail__path {
  fill: none;
  stroke: color-mix(in srgb, var(--color-text) 45%, transparent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 0.5 7;
}

.home-trail__plane {
  fill: var(--color-accent);
  transform-box: fill-box;
  transform-origin: center;
  animation: skypane-plane-fly var(--motion-flight) var(--ease-out) backwards;
}

/* Along the trail, the plane's own offsets from its resting point; the
 * resting state is the plain layout state (fill-mode backwards only). */
@keyframes skypane-plane-fly {
  from {
    opacity: 0;
    transform: translate(-292px, 32px) rotate(6deg);
  }

  15% {
    opacity: 1;
  }

  35% {
    transform: translate(-235px, 26px) rotate(-8deg);
  }

  60% {
    transform: translate(-172px, 14px) rotate(-18deg);
  }

  80% {
    transform: translate(-96px, 3px) rotate(-14deg);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* The cards come in one after the other: the state card, the current
 * frame, then the recent flights. */
.home-state.reveal {
  --reveal-index: 0;
}

.dashboard-main .home-columns .page-section:nth-child(1) {
  --reveal-index: 1;
}

.dashboard-main .home-columns .page-section:nth-child(2) {
  --reveal-index: 2;
}

/* The other pages in the same language: the same entrance on the blocks that
 * sit straight on the sky, a soft arrival for the active navigation item, and
 * a stacked frame row on a phone. Appended last. */
@keyframes skypane-soft-in {
  from {
    opacity: 0;
    transform: scaleY(0.3);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* A thin accent bar settles beside the current sidebar item; it is a
 * pseudo-element, so the link's own box and hit area never change. */
.sidebar-link--active::after {
  content: "";
  position: absolute;
  left: 0;
  top: 10px;
  bottom: 10px;
  width: 3px;
  border-radius: 2px;
  background: var(--color-accent);
  pointer-events: none;
  animation: skypane-soft-in var(--motion-enter) var(--ease-out) backwards;
}

.tab-bar__link--active .tab-bar__pill {
  animation: skypane-soft-in var(--motion-enter) var(--ease-out) backwards;
}

@media (max-width: 599.98px) {
  li.entity-row.account-frame {
    grid-template-columns: auto minmax(0, 1fr);
  }

  li.entity-row.account-frame > :last-child:not(.entity-row__text) {
    grid-column: 1 / -1;
    justify-self: start;
  }
}

/* Placeholder text is secondary ink, never the browser's pale default: it has
 * to clear 4.5:1 on the field under every sky. */
::placeholder {
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
  opacity: 1;
}
