/* os-ds — flattened CSS bundle for Propshaft/importmap consumers.
   GENERATED by bin/build_css_bundle.rb — do not edit by hand.
   Regenerate with `rake os_ds:bundle` (or run the script directly). */


/* ===== os_ds/tokens.css ===== */
/*
 * tokens.css — DiveOS design token ENTRY (v4 — repo spec: sky-700 + Inter)
 *
 * Two-layer token architecture (split 2026-07-11, os-ds extraction step 1):
 *   - tokens/primitives.css — product-INVARIANT raw scales (ramps, spacing,
 *     type, radius, shadow, z, motion, layout). Shared byte-for-byte across
 *     DiveOS / HotelOS; never referenced by components directly.
 *   - tokens/semantic.css   — the THEME surface: role mappings (neutrals,
 *     accent, status, input, notifications, interaction states) + fonts.
 *     The ~10% a product re-brands; components consume ONLY these.
 * A consuming product re-themes by overriding tokens/semantic.css alone.
 *
 * v4 migration (2026-06-06): aligned to the diveOS-design-system repo
 * (gitlab.nsfrg.my:novas/diveOS-design-system), the locked design source of
 * truth — body font Inter, accent sky-700 #0369A1, cool-gray neutrals,
 * ramp-based status tints, dark mode removed (light-only).
 *
 * Loaded via application.css before all other stylesheets. The token @imports
 * below are inlined by postcss-import at build time into one :root cascade;
 * order is cosmetic since every var() resolves at use-time.
 */

/* Self-hosted webfonts (woff2 face declarations) — replaces the former remote
   Google Fonts imports. A consuming app tightened its CSP to `font_src :self`
   with no external stylesheet host, which blocked the remote font import and
   dropped the type. Fonts now ship in the gem (app/assets/fonts, on the engine
   asset path) and resolve via Propshaft's url() rewriting. Declared here at the
   top of the token entry so the face rules land near the top of the flattened
   bundle, ahead of any rule that uses the families. Material Symbols was dropped
   (dead — no consumer). */
/* THE @import STATEMENTS MUST STAY FIRST. CSS ignores any `@import` that
   appears after another statement, and `@font-face` counts. From v0.16.1 —
   when the fonts were self-hosted and these face rules were added ABOVE the
   imports — both token imports became invalid, so a consumer that loads THIS
   FILE (rather than the flattened bundle) silently lost the entire token
   layer: every primitive and every semantic role.

   It stayed invisible for thirteen releases because the two consumers load
   os-ds differently. arena-ops loads the flattened `os_ds.css`, where the
   builder has already inlined these imports, so it is unaffected. DiveOS
   imports the SOURCE files through postcss-import, which honoured the spec,
   dropped both, and left `--accent`, `--ink`, `--surface` and every scale
   undefined. Its Tailwind theme hardcodes fallbacks (`var(--accent, #0369A1)`)
   so utilities kept working — but os-ds components carry no fallback, so
   `.dv-btn--primary` computed a TRANSPARENT background there.

   Found by rendering the real app, not by reading CSS: the class-level diff
   between versions reported zero regressions, because tokens are not classes. */
/*
 * tokens/primitives.css — PRODUCT-INVARIANT design primitives.
 *
 * The raw scales every product shares byte-for-byte: colour ramps, the
 * spacing / type / radius / shadow scales, z-index stack, motion timings,
 * and structural layout defaults. These are the SOURCE the semantic layer
 * (tokens/semantic.css) maps into roles — components NEVER reference a
 * primitive directly, only its semantic alias.
 *
 * When the design system is extracted into the shared os-ds gem, this file
 * ships once and is identical across DiveOS / HotelOS. A product re-themes by
 * overriding tokens/semantic.css, not this file (it may ADD a brand ramp here
 * — e.g. a --teal-* scale — then point --accent at it in its semantic layer).
 *
 * Loaded via tokens.css (the entry) before tokens/semantic.css. Ordering is
 * cosmetic: all vars land in one :root cascade and var() resolves at use-time.
 */

:root {
  /* ─── Foundation ramps (semantic source + showcase swatches) ─────── */
  --gray-50:  #F9FAFB; --gray-100: #F3F4F6; --gray-200: #E5E7EB;
  --gray-300: #D1D5DB; --gray-400: #9CA3AF; --gray-500: #6B7280;
  --gray-600: #4B5563; --gray-700: #374151; --gray-800: #1F2937;
  --gray-900: #111827; --gray-950: #030712;

  --sky-50:  #F0F9FF; --sky-100: #E0F2FE; --sky-200: #BAE6FD;
  --sky-300: #7DD3FC; --sky-400: #38BDF8; --sky-500: #0EA5E9;
  --sky-600: #0284C7; --sky-700: #0369A1; --sky-800: #075985;
  --sky-900: #0C4A6E; --sky-950: #082F49;

  --amber-50:  #FFFBEB; --amber-100: #FEF3C7; --amber-200: #FDE68A;
  --amber-300: #FCD34D; --amber-400: #FBBF24; --amber-500: #F59E0B;
  --amber-600: #D97706; --amber-700: #B45309; --amber-800: #92400E;
  --amber-900: #78350F; --amber-950: #451A03;

  --green-50:  #F0FDF4; --green-100: #DCFCE7; --green-200: #BBF7D0;
  --green-300: #86EFAC; --green-400: #4ADE80; --green-500: #22C55E;
  --green-600: #16A34A; --green-700: #15803D; --green-800: #166534;
  --green-900: #14532D; --green-950: #052E16;

  --red-50:  #FEF2F2; --red-100: #FEE2E2; --red-200: #FECACA;
  --red-300: #FCA5A5; --red-400: #F87171; --red-500: #EF4444;
  --red-600: #DC2626; --red-700: #B91C1C; --red-800: #991B1B;
  --red-900: #7F1D1D; --red-950: #450A0A;

  --slate-50:  #F8FAFC; --slate-100: #F1F5F9; --slate-200: #E2E8F0;
  --slate-300: #CBD5E1; --slate-400: #94A3B8; --slate-500: #64748B;
  --slate-600: #475569; --slate-700: #334155; --slate-800: #1E293B;
  --slate-900: #0F172A; --slate-950: #020617;

  --rose-50:  #FFF1F2; --rose-100: #FFE4E6; --rose-200: #FECDD3;
  --rose-300: #FDA4AF; --rose-400: #FB7185; --rose-500: #F43F5E;
  --rose-600: #E11D48; --rose-700: #BE123C; --rose-800: #9F1239;
  --rose-900: #881337; --rose-950: #4C0519;

  --stone-50:  #FAFAF9; --stone-100: #F5F5F4; --stone-200: #E7E5E4;
  --stone-300: #D6D3D1; --stone-400: #A8A29E; --stone-500: #78716C;
  --stone-600: #57534E; --stone-700: #44403C; --stone-800: #292524;
  --stone-900: #1C1917; --stone-950: #0C0A09;

  /* ─── Spacing scale — 8-point system (4px sub-grid) ──────────────────
     One unit = 4px. Use these for all component padding / gap / margin so
     primitives stay on the grid. Half-step (--space-0-5 = 2px) is reserved
     for hairline-level nudges only (icon optical alignment), not layout. */
  --space-0-5: 2px;
  --space-1:   4px;
  --space-2:   8px;
  --space-3:   12px;
  --space-4:   16px;
  --space-5:   20px;
  --space-6:   24px;
  --space-8:   32px;
  --space-10:  40px;
  --space-12:  48px;
  --space-16:  64px;

  /* ─── Type scale — even steps on the system (no odd/half sizes) ───────
     Component font-size MUST use these tokens, never a raw px. The old
     11/12.5/13/13.5/15px values snap to the nearest step. Bigger one-off
     display/hero sizes (>28) stay raw — they're not primitives. */
  --text-2xs:  10px;   /* micro labels, table th eyebrows */
  --text-xs:   12px;   /* eyebrows, sub-lines, meta, chips (← 11/11.5/12.5) */
  --text-sm:   14px;   /* body / primary cell / card heading (← 13/13.5/15) */
  --text-md:   16px;   /* prominent body, small headings (← 15.5/17) */
  --text-lg:   18px;   /* card/section headings (← 19) */
  --text-xl:   20px;   /* large headings (← 21/22) */
  --text-2xl:  24px;   /* (← 23/25/26) */
  --text-3xl:  28px;   /* page titles (← 27/30/31/32) */

  /* ─── Line-height scale — paired 1:1 with the type scale ──────────────
     Practical UI, Ch.5 (Typography): line-height is ABSOLUTE and lands on
     the 4pt vertical grid (so text aligns to the layout rhythm), the RATIO
     DECREASES as font-size grows (headings tighter than body), and body
     text stays ≥1.5 for readability. Each --leading-* is the leading for
     the same-suffix --text-*; a text role sets font-size AND its paired
     line-height, never a raw ratio. Book anchors: H3 24/32, H4 20/28,
     Small 16/24, Tiny 14/20. */
  --leading-2xs: 14px;   /* 10px · 1.40 — micro labels (single-line) */
  --leading-xs:  18px;   /* 12px · 1.50 — meta / small print */
  --leading-sm:  22px;   /* 14px · 1.57 — default body (≥1.5) */
  --leading-md:  24px;   /* 16px · 1.50 — body / small heading (book Small) */
  --leading-lg:  28px;   /* 18px · 1.56 — roomy body / card heading */
  --leading-xl:  28px;   /* 20px · 1.40 — heading (book H4 20/28) */
  --leading-2xl: 32px;   /* 24px · 1.33 — heading (book H3 24/32) */
  --leading-3xl: 36px;   /* 28px · 1.29 — page title (book H2/H3 pattern) */
  --leading-relaxed: 1.6; /* long-form prose paragraphs (unitless, opt-in) */

  /* ─── Border radius scale — small / medium / large intent ────────────
     Practical UI uses 3 radii (small/medium/large). Ours is finer for a
     dense UI, but maps to the same intent. --radius is the default; --radius-md
     is a back-compat alias of it (they were a duplicate 8px). */
  --radius-sm:   6px;             /* small — chips, inputs, tight controls */
  --radius:      8px;             /* medium (default) — buttons, cards */
  --radius-md:   var(--radius);   /* alias of the default (dedup: was 8px twice) */
  --radius-lg:   10px;            /* large — panels, popovers */
  --radius-xl:   14px;            /* x-large — hero / sheet surfaces */
  --radius-2xl:  18px;            /* xx-large — the FAB (.os-fab) only, so far.
                                     Added for a 56px floating button: at that
                                     size --radius-xl reads as a plain square
                                     and --radius-pill as a circle, and neither
                                     is the intended "softened square". Reach
                                     for this only on a large standalone
                                     control, never on a card or panel. */
  --radius-pill: 9999px;

  /* ─── Control size scale — the height of an INTERACTIVE control ──────
     Every control in the system (button, input, select, filter pill,
     segmented option) is one of seven heights. Before this scale they were
     seven independent LITERALS, re-typed per component: .os-btn 36px,
     --sm 32px, --field 46px, --touch 48px, and forms.css / dv_select.css
     spelling those same four numbers again from memory. Nothing tied them
     together, so "the field height" was a number you had to know rather
     than a token you could read — and the numbers had already begun to
     drift (dv_select's small variant is 34px, not the 32px --sm it means).

     WHY a scale AND a resolved trio (below), rather than one token per
     component: a control's height is a property of the SURFACE it sits on,
     not of the control. A desk admin table wants 36px; a handheld used
     one-handed on a boat, in sun, wants 48px. The alternative — adding a
     `--field` twin to each component — is N patches that drift apart, which
     is exactly how we got here. Density is set ONCE on a layout wrapper
     (see tokens/density.css) and every control inside re-sizes.

     --control-h-md (40px) is NEW: the missing rung between --sm (32) and
     --field (46). It exists as a density STEP, not as a per-component
     modifier, so it cannot become the next one-off.

     Paired --control-pad-x-* / --control-fs-* keep a control's horizontal
     padding and label size proportional to its height; a 48px control with
     14px side padding reads as a slab. */
  --control-h-xs:    22px;   /* inline action beside a badge (.os-btn--xs) */
  --control-h-sm:    32px;   /* dense desktop row (.os-btn--sm) */
  --control-h-base:  36px;   /* THE default — desktop admin (.os-btn) */
  --control-h-md:    40px;   /* comfortable desktop / small tablet (new) */
  --control-h-lg:    42px;   /* prominent CTA (.os-btn--lg) */
  --control-h-field: 46px;   /* canonical form-control height (.os-field) */
  --control-h-touch: 48px;   /* 48pt minimum touch target (Fitts's Law) */

  --control-pad-x-xs:    var(--space-2);   /* 8px  */
  --control-pad-x-sm:    var(--space-3);   /* 12px */
  --control-pad-x-base:  14px;             /* px-3.5 — off-grid, see NOTE */
  --control-pad-x-md:    14px;
  --control-pad-x-lg:    var(--space-4);   /* 16px */
  --control-pad-x-field: 14px;
  --control-pad-x-touch: var(--space-5);   /* 20px */
  /* NOTE: 14px is not on the 4px spacing grid. It is the shipped .os-btn
     padding (Tailwind px-3.5) and re-grid-ing it would resize every button
     in both products — a token contract cannot also be a visual redesign.
     Left as a literal so the off-grid value is visible, not laundered. */

  --control-fs-xs:    var(--text-2xs);
  --control-fs-sm:    var(--text-xs);
  --control-fs-base:  var(--text-sm);
  --control-fs-md:    var(--text-sm);
  --control-fs-lg:    var(--text-sm);
  --control-fs-field: var(--text-sm);
  --control-fs-touch: var(--text-sm);

  /* The RESOLVED trio every interactive component reads. Defaults to the
     base step; a density scope (tokens/density.css) re-points these three
     on a wrapper and the whole subtree follows. A component that must NOT
     follow density re-declares them on itself — which is precisely how the
     existing size modifiers stay pinned (see buttons.css). */
  --control-h:     var(--control-h-base);
  --control-pad-x: var(--control-pad-x-base);
  --control-fs:    var(--control-fs-base);

  /* ─── Elevation — TWO tiers (Practical UI Ch.3: ~2 elevations is enough) ──
     `raised`  = anything lifted off the page (cards, menus, popovers, dropdowns).
     `overlay` = surfaces floating above the page (drawers, modals, toasts).
     These two are the only concrete shadow values; the legacy --shadow-* names
     below all derive from them (back-compat), so a component can keep its old
     token and still get the collapsed 2-tier system. The card "ring" is a
     structural hairline border, composited on top of `raised` — not a 3rd
     elevation. (Visual-reviewed before/after 2026-07-11.) */
  --elevation-raised:  0 1px 2px rgba(17, 24, 39, 0.06), 0 2px 6px -1px rgba(17, 24, 39, 0.05);
  --elevation-overlay: 0 16px 40px -12px rgba(17, 24, 39, 0.20), 0 6px 16px -8px rgba(17, 24, 39, 0.10);

  /* Legacy shadow names → the 2 tiers (was 5 distinct values). --shadow-sm is
     the exception: it's a control WHISPER (segmented options, toggle knobs,
     small pills), NOT a real elevation, so it keeps its subtle 1-layer value —
     folding it into --elevation-raised made those inline controls read too
     heavy. --shadow-md is the actual raised tier. */
  --shadow-sm:    0 1px 2px rgba(17, 24, 39, 0.05);
  --shadow-md:    var(--elevation-raised);
  --shadow-card:  0 1px 2px rgba(17, 24, 39, 0.05), 0 0 0 1px var(--hairline);  /* raised + hairline ring */
  --shadow-hover: var(--elevation-raised), 0 0 0 1px var(--hairline);           /* card hover = raised + ring */
  --shadow-panel: var(--elevation-overlay);

  /* ─── Z-index stack ──────────────────────────────────────────────── */
  --z-toast:    60;
  --z-modal:    50;
  --z-panel:    40;
  --z-dropdown: 30;
  --z-tooltip:  20;

  /* ─── Motion ─────────────────────────────────────────────────────── */
  --ease-out-quart: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-standard:  cubic-bezier(0.4, 0, 0.2, 1);
  --t-fast:  120ms;
  --t-med:   180ms;
  --t-slow:  240ms;

  /* ─── Layout primitives ─────────────────────────────────────────── */
  --sidebar-w: 240px;
  --panel-w:   520px;

  /* ─── Width scale — floating-surface widths (popovers, dialogs, panels) ──
     A fixed-width scale so a menu / popover / dialog / side-panel snaps to a
     named step instead of a raw px. Follows the de-facto container-width
     convention (Tailwind max-width, Chakra `sizes`) — 4rem steps; Carbon &
     Polaris drive layout from grid + spacing and ship no floating-surface
     width primitive, so this is the closest applicable reference. Component
     widths (os-pop 320, os-popover 380, side-panel ~560) snap to these. */
  --width-xs: 320px;   /* dropdowns / pickers (os-pop) */
  --width-sm: 384px;   /* small dialogs / popovers (os-popover; 380 → 384) */
  --width-md: 448px;   /* medium dialogs */
  --width-lg: 512px;   /* large dialogs / confirm */
  --width-xl: 576px;   /* side panels / sheets (~560 → 576) */

  /* Page width tiers — the content column ceiling per archetype. The shell
     (.diveos-content) defaults to --page-wide; forms/wizards opt into --narrow
     via content_for(:page_width). Below the ceiling the fluid --gutter breathes;
     past it, margin-inline:auto converts surplus to symmetric side space.

     THREE tiers since v1.11.0. It was two, on the reasoning that "reading pages
     stay wide and cap PROSE with --measure" — which holds for an article and
     fails for a DOCUMENT. A registration card, a folio, a checkout review, a
     statement: these are not prose, so --measure never engages, and they are
     not lists either, so --wide gives them a 1280px field of mostly-empty row.
     Their content is field/value pairs, a signature block, a party table —
     structures that want to sit together, not stretch. --narrow is the other
     failure: a two-column field grid plus a signature pad does not survive
     768px, so consumers were pinning their own local ~900-1000px values, which
     is how six different literals ended up in one app.

     PICK BY WHAT THE PAGE IS, not by how wide it looks right:
       --page-wide     you are scanning MANY records         (board, list, dashboard)
       --page-reading  you are reading or signing ONE record (document, review, receipt)
       --page-narrow   you are answering questions in order  (form, wizard, single column) */
  --page-wide:    1280px;   /* default: lists, kanban, calendar, dashboard, detail, settings */
  --page-reading:  960px;   /* documents: registration cards, folios, statements, reviews */
  --page-narrow:  768px;    /* forms, wizards, focused reading */
  /* Measure (max line length) SCALES DOWN as text grows — Practical UI Ch.4/5:
     optimal line length shrinks at larger sizes (~90ch small body → ~45ch
     display). Apply via .dv-measure (default) / --lg / --display; never on
     tables or cards. */
  --measure:         68ch;  /* default: 14–16px body — the reading cap */
  --measure-lg:      56ch;  /* 18–20px lead paragraphs / large body */
  --measure-display: 45ch;  /* 24px+ display / hero copy that wraps */
  --gutter:       clamp(16px, 4cqi, 64px);  /* fluid side gutter (measured on the content pane) */
  --content-max:  var(--page-narrow);       /* legacy alias the wizard CSS consumes (was undefined) */
}

/*
 * tokens/semantic.css — the THEME surface (per-product override point).
 *
 * Maps the raw primitives (tokens/primitives.css) into named ROLES that
 * components consume: neutrals (canvas/surface/ink/…), the accent family,
 * status palette, input tokens, notification semantics, and interaction
 * states. This is the ~10% a product re-brands: swap the accent, the fonts,
 * or the status mapping here and every .dv-* component re-themes with no
 * component edits.
 *
 * In the shared os-ds gem this file ships as the DEFAULT (DiveOS: sky-700 /
 * Inter). A consuming app (HotelOS) overrides it with its own tokens/semantic.css
 * loaded after the gem's — same role names, different primitive targets.
 *
 * NOTE the dive-domain status names (briefing / inwater) live here: they are a
 * semantic/domain choice, so HotelOS replaces this block with its own statuses
 * rather than inheriting them.
 */

:root {
  /* ─── Typography — role fonts (a product may rebrand these) ────────────
     Single typeface for UI: Inter for BOTH display/headings and body
     (General Sans was dropped — headings + body are now one family).
     Geist Mono for tabular numerals, IDs, timestamps, financial figures.
     --font-display is kept as a distinct token (same value as --font-sans)
     so heading call-sites keep their semantic name and can diverge later
     without a sweep. */
  --font-display: 'Inter', ui-sans-serif, system-ui, sans-serif;
  --font-sans:    'Inter', ui-sans-serif, system-ui, sans-serif;
  --font-mono:    'Geist Mono', ui-monospace, monospace;

  /* ─── Semantic neutrals (v4 — cool gray ramp aliases) ──────────────── */
  --canvas:           var(--gray-50);      /* page bg */
  --surface:          #FFFFFF;             /* cards, modals */
  --surface-alt:      var(--gray-100);     /* header zone, sidebar, subtle distinction */
  --hairline:         var(--gray-200);     /* default border */
  --hairline-2:       #F0F1F4;             /* inner row separator */
  --hairline-strong:  var(--gray-300);     /* active/focused border */
  --ink:              var(--gray-900);     /* body + headings */
  --muted:            var(--gray-500);     /* less prominent text */
  --muted-2:          #6A707A;             /* captions, meta — darkened from gray-400 to clear 4.5:1 (Practical UI Ch.3 contrast; was 2.5:1) */

  /* ─── Accent (v4 — sky-700, swappable per tenant) ─────────────────────
     --accent-rgb holds the accent's raw channels so the alpha-derived tokens
     (soft/softer/border here + input-border-focus/focus-ring/focus-ring-color
     below) compose from ONE source. A re-theming consumer overrides --accent
     AND --accent-rgb (two lines) and every derived surface — including focus
     rings — follows; before this, the six alphas were hardcoded to sky's
     channels, so swapping --accent alone left a half-sky UI. */
  --accent:           var(--sky-700);
  --accent-strong:    var(--sky-800);
  --accent-rgb:       3, 105, 161;             /* sky-700 channels; override when re-theming */
  --accent-soft:      rgba(var(--accent-rgb), 0.08);
  --accent-softer:    rgba(var(--accent-rgb), 0.04);
  --accent-border:    rgba(var(--accent-rgb), 0.26);
  --on-accent:        #FFFFFF;                 /* text/glyphs on an accent fill (buttons, markers, pills) */

  /* ─── v1/v2 compat aliases — back-compat for existing callsites ───── */
  --bg:               var(--canvas);
  --text-primary:     var(--ink);
  --text-secondary:   var(--gray-700);
  --text-muted:       var(--muted);
  --border:           var(--hairline);
  --border-strong:    var(--hairline-strong);
  --accent-tint:      var(--accent-soft);
  --accent-light:     var(--sky-100);       /* lit accent tint for current-step */

  /* ─── Sequential data-viz ramp (NOT status colors) ─────────────────
     Monochromatic sky ramp for functional 5-step sequential data
     visualisations (e.g. pricing-zone bands). Purple/violet is banned;
     use these for any light→intense sequential encoding. Text on any
     step: --viz-ink (sky-800 stays readable on sky-50..400). */
  --viz-1:   var(--sky-50);
  --viz-2:   var(--sky-100);
  --viz-3:   var(--sky-200);
  --viz-4:   var(--sky-300);
  --viz-5:   var(--sky-400);
  --viz-ink: var(--sky-800);

  --sidebar-bg:       var(--gray-100);
  --hover-warm:       var(--gray-100);      /* legacy name; now neutral gray hover */

  /* ─── Status palette — single source (v4 ramp-based soft tints) ─────
     Both legacy names (--status-*-fg / --status-*-bg) and v2 short names
     (--<status>, --<status>-bg, --<status>-border) resolve to the SAME
     ramp values so no chip can drift between aliases.
     5 repo statuses + 2 app-derived (returning=slate, completed=gray). */
  --status-pending-fg:    var(--stone-600);
  --status-pending-bg:    var(--stone-100);
  --status-pending-border:var(--stone-300);
  --pending:              var(--status-pending-fg);
  --pending-bg:           var(--status-pending-bg);
  --pending-border:       var(--status-pending-border);

  --status-confirmed-fg:  var(--green-700);
  --status-confirmed-bg:  var(--green-50);
  --status-confirmed-border: var(--green-200);
  --confirmed:            var(--status-confirmed-fg);
  --confirmed-bg:         var(--status-confirmed-bg);
  --confirmed-border:     var(--status-confirmed-border);

  --status-briefing-fg:   var(--amber-800);
  --status-briefing-bg:   var(--amber-50);
  --status-briefing-border: var(--amber-300);
  --briefing:             var(--status-briefing-fg);
  --briefing-bg:          var(--status-briefing-bg);
  --briefing-border:      var(--status-briefing-border);

  --status-in-water-fg:   var(--sky-800);
  --status-in-water-bg:   var(--sky-50);
  --status-in-water-border: var(--sky-200);
  --inwater:              var(--status-in-water-fg);
  --inwater-bg:           var(--status-in-water-bg);
  --inwater-border:       var(--status-in-water-border);

  --status-returning-fg:  var(--slate-600);
  --status-returning-bg:  var(--slate-100);
  --status-returning-border: var(--slate-300);

  --status-completed-fg:  var(--gray-500);
  --status-completed-bg:  var(--gray-100);
  --status-completed-border: var(--gray-300);

  --status-cancelled-fg:  var(--rose-800);
  --status-cancelled-bg:  var(--rose-50);
  --status-cancelled-border: var(--rose-200);
  --cancelled:            var(--status-cancelled-fg);
  --cancelled-bg:         var(--status-cancelled-bg);
  --cancelled-border:     var(--status-cancelled-border);

  /* ─── Bar / rail tints (peripheral indicators — lighter ramp steps) ── */
  --bar-confirmed:  var(--green-300);
  --bar-briefing:   var(--amber-300);
  --bar-inwater:    var(--sky-300);
  --bar-pending:    var(--stone-300);
  --bar-cancelled:  var(--rose-300);
  --bar-returning:  var(--slate-300);

  /* ─── Input tokens (v4 repo model) ─────────────────────────────────── */
  --input-bg:           var(--gray-50);
  --input-border:       var(--gray-300);
  --input-border-hover: var(--gray-400);
  --input-border-focus: rgba(var(--accent-rgb), 0.60);
  --input-placeholder:  rgba(107, 114, 128, 0.80);  /* darkened from 0.50 to clear ~3:1 supplementary-text bar (label always present) */
  --focus-ring:         rgba(var(--accent-rgb), 0.10);
  --error:              #C13028;

  /* ─── Notification semantics (toasts / inline alerts) ───────────────
     Generic success/danger/warning/info layer, distinct from the
     booking-status palette. Resolves to existing colour ramps. */
  --success:         var(--green-700);
  --success-bg:      var(--green-50);
  --success-border:  var(--green-200);

  --danger:          var(--rose-800);
  --danger-bg:       var(--rose-50);
  --danger-border:   var(--rose-200);

  --warning:         var(--amber-700);
  --warning-bg:      var(--amber-50);
  --warning-border:  var(--amber-200);

  --info:            var(--accent);
  --info-bg:         var(--sky-50);
  --info-border:     var(--sky-200);

  /* ─── Interaction states ─────────────────────────────────────────────
     Canonical state vocabulary every interactive primitive uses:
       hover    → neutral darken overlay (or accent-strong on filled)
       active   → stronger overlay / translateY(1px) press
       focus    → --shadow-focus keyboard ring (focus-visible only)
       selected → accent-soft fill + accent text
       disabled → --disabled-opacity, no pointer events
     See /admin/design_system "Interaction states" for the recipe. */
  --state-hover:    rgba(17, 24, 39, 0.04);
  --state-active:   rgba(17, 24, 39, 0.08);
  --state-selected: var(--accent-soft);
  --focus-ring-color: rgba(var(--accent-rgb), 0.35);
  --shadow-focus:   0 0 0 3px var(--focus-ring-color);
  --disabled-opacity: 0.5;
  --transition-base: var(--t-med) var(--ease-standard);
}

/* Control-density SCOPE classes (.os-density--*). Imported last of the three
   so its scope rules land after the :root control defaults they override. */
/*
 * tokens/density.css — the CONTROL DENSITY SCOPE.
 *
 * One class, set ONCE on a layout wrapper, re-points the resolved control
 * trio (--control-h / --control-pad-x / --control-fs, defined in
 * tokens/primitives.css) for everything inside it. Every interactive
 * component in the gem reads that trio, so a handheld or kiosk surface
 * conforms to the design system by declaring its density rather than by
 * minting local classes.
 *
 *   <div class="os-density--touch">   <!-- boat handheld, one-handed, in sun -->
 *     <button class="os-btn os-btn--primary">Check in</button>   <!-- 48px -->
 *   </div>
 *
 * WHY A CLASS, not a data attribute (`[data-density="touch"]`)
 * ----------------------------------------------------------------------
 * Both work; the class wins on four counts specific to THIS gem.
 *   1. Composition. A wrapper very often already carries `class:` in the ERB
 *      helper and pattern partials; adding a token to that string needs no
 *      new helper argument and no rewrite of the tag call to pass a data hash,
 *      at the ~40 call sites that render a layout shell.
 *   2. The prefix contract. `.os-*` IS the gem's public surface (README →
 *      Versioning) and the no-legacy-prefix gate polices it. A data attribute
 *      would be a second, unpoliced public namespace with its own rules.
 *   3. Tooling. hallmark, the alias gate and the component index all read
 *      CLASSES. A density expressed as an attribute is invisible to every
 *      one of them, including the lookalike detector that would otherwise
 *      catch a consumer hand-rolling a 48px button instead of scoping it.
 *   4. Specificity honesty. Attribute and class selectors have identical
 *      specificity, so the attribute buys nothing there — but it DOES read
 *      as configuration rather than styling, which invites consumers to set
 *      it from server state. Density is a layout decision, not data.
 *
 * The counter-argument (an attribute self-documents its allowed values) is
 * answered by this file: the four steps are enumerated here, in one place.
 *
 * SCOPES NEST. The trio is inherited, so an inner scope overrides an outer
 * one — a `--touch` page may hold a `--compact` toolbar. That falls out of
 * the cascade; no extra rules.
 *
 * WHAT DENSITY DOES NOT TOUCH — the back-compat contract.
 * Every explicit size modifier (.os-btn--sm/--lg/--field/--touch/--icon/--xs,
 * .os-field--sm, .os-select--sm …) re-declares the trio ON ITSELF. A
 * declaration on the element always beats one inherited from an ancestor, so
 * a modifier renders at its documented size inside ANY density scope. This is
 * both the compatibility guarantee for pinned consumers and the right
 * semantics: "make this button small" is an instruction about that button.
 * Density only sizes controls that did not ask for a specific size.
 *
 * Loaded via tokens.css after primitives + semantic. Scope rules cannot live
 * in primitives.css: that file is a single :root block by contract.
 */

/* ── The four density steps ──────────────────────────────────────────────
   Named for the SURFACE they describe, not the pixel value, so a consumer
   picks by intent. Each step re-points all three tokens together — height
   without matching padding and label size produces a stretched control. */

/* Dense desktop — data-heavy admin tables, toolbars, filter rows. 32px. */
.os-density--compact {
  --control-h:     var(--control-h-sm);
  --control-pad-x: var(--control-pad-x-sm);
  --control-fs:    var(--control-fs-sm);
}

/* The system default — desktop admin. 36px. Exists as an explicit scope so a
   region can opt BACK OUT of an enclosing --touch/--field scope. */
.os-density--base {
  --control-h:     var(--control-h-base);
  --control-pad-x: var(--control-pad-x-base);
  --control-fs:    var(--control-fs-base);
}

/* Comfortable — small tablet, or a desktop form that wants air without going
   full field height. 40px: the rung that did not exist before v1.6.0, and the
   reason `--sm`-to-`--field` was a 14px cliff consumers papered over locally. */
.os-density--comfortable {
  --control-h:     var(--control-h-md);
  --control-pad-x: var(--control-pad-x-md);
  --control-fs:    var(--control-fs-md);
}

/* Form surfaces — controls align on the canonical 46px field baseline, so a
   button sitting inline with a select or datepicker matches it without the
   per-call-site `.os-btn--field` twin. */
.os-density--field {
  --control-h:     var(--control-h-field);
  --control-pad-x: var(--control-pad-x-field);
  --control-fs:    var(--control-fs-field);
}

/* Handheld / kiosk — 48pt minimum target. The originating case: a dive
   handheld used one-handed on a moving boat in direct sun, where a 32px
   filter button is below a safe target size and the consumer had to keep
   local classes because the gem offered no field-size variant of it. */
.os-density--touch {
  --control-h:     var(--control-h-touch);
  --control-pad-x: var(--control-pad-x-touch);
  --control-fs:    var(--control-fs-touch);
}


@font-face { font-family:'Inter'; font-style:normal; font-weight:100 900; font-display:swap; src:url("/assets/inter-var-9b45d99b.woff2") format('woff2'); }
@font-face { font-family:'Geist Mono'; font-style:normal; font-weight:400; font-display:swap; src:url("/assets/GeistMono-Regular-8d81938c.woff2") format('woff2'); }
@font-face { font-family:'Geist Mono'; font-style:normal; font-weight:500; font-display:swap; src:url("/assets/GeistMono-Medium-0fcc9bdd.woff2") format('woff2'); }
@font-face { font-family:'Geist Mono'; font-style:normal; font-weight:600; font-display:swap; src:url("/assets/GeistMono-SemiBold-b24349fd.woff2") format('woff2'); }


/* ===== os_ds/patterns.css ===== */
/*
 * Bullet Train's base CSS applies a global `a:hover { text-decoration-
 * line: underline; color: secondary-600; }` to every anchor. That
 * overrides custom hover treatments because text-decoration-line is
 * different from text-decoration (the latter doesn't reset the former).
 * Reset hover behavior for the custom link classes used in patterns +
 * shell. They handle their own hover state via background-color +
 * color changes — no underline, no link recoloring.
 */
/* Reset BT's global a:hover (text-decoration-line: underline + color
   secondary-600) for our custom link components. Also reset base a
   color (BT sets a { color: var(--secondary-500) }) so descendant text
   inherits --text-primary instead of the link blue. Both rules need
   to be on the parent anchor — the children inherit from there. */
.os-sidebar__link, .os-sidebar__owner, :is(.open-row__link, .os-open-row__link), :is(.ledger-row__link, .os-ledger-row__link), :is(.filter-pill, .os-filter-pill), :is(.segmented__option, .os-segmented__option), :is(.tabs__tab, .os-tabs__tab), :is(.settings-sub-nav__link, .os-settings-sub-nav__link), :is(.trip-row__link, .os-trip-row__link), :is(.setting-item--linked, .os-setting-item--linked), :is(.header-zone__action-button, .os-header-zone__action-button), :is(.org-outlet-row, .os-org-outlet-row), :is(.lead-card-clickable__link, .os-lead-card-clickable__link), :is(.run-reopen-row, .os-run-reopen-row) {
  color: inherit;
}
.os-sidebar__link:hover, .os-sidebar__owner:hover, :is(.open-row__link, .os-open-row__link):hover, :is(.ledger-row__link, .os-ledger-row__link):hover, :is(.filter-pill, .os-filter-pill):hover, :is(.segmented__option, .os-segmented__option):hover, :is(.tabs__tab, .os-tabs__tab):hover, :is(.settings-sub-nav__link, .os-settings-sub-nav__link):hover, :is(.trip-row__link, .os-trip-row__link):hover, :is(.setting-item--linked, .os-setting-item--linked):hover, :is(.header-zone__action-button, .os-header-zone__action-button):hover, :is(.org-outlet-row, .os-org-outlet-row):hover, :is(.lead-card-clickable__link, .os-lead-card-clickable__link):hover, :is(.run-reopen-row, .os-run-reopen-row):hover {
  text-decoration-line: none;
  color: inherit;
}
/* Hover background colors set explicitly elsewhere; preserve the
   defined hover states for backgrounds. */
.os-sidebar__link:hover { color: var(--text-primary); }
.os-sidebar__item--active .os-sidebar__link:hover { color: var(--accent-strong); }
:is(.filter-pill, .os-filter-pill):hover { color: var(--text-secondary); }
:is(.filter-pill--active, .os-filter-pill--active):hover { color: var(--accent-strong); }
:is(.segmented__option, .os-segmented__option):hover { color: var(--text-primary); }
:is(.tabs__tab, .os-tabs__tab):hover { color: var(--text-primary); }
:is(.settings-sub-nav__link, .os-settings-sub-nav__link):hover { color: var(--text-primary); }
:is(.settings-sub-nav__item--active, .os-settings-sub-nav__item--active) :is(.settings-sub-nav__link, .os-settings-sub-nav__link):hover { color: var(--accent-strong); }

/*
 * patterns.css — DiveOS shared pattern styles
 *
 * One file per concern would be tidier but Propshaft imports flat. The
 * patterns live here in the same order as UI-ARCHITECTURE.md §4 so a
 * reader scrolling the spec finds the CSS for each pattern in sequence.
 *
 * Resolves all colors through tokens.css semantic aliases (light-only
 * since v4 — dark mode removed per the diveOS-design-system repo spec).
 *
 * Inter is the body font (loaded by tokens.css). Geist Mono is used for
 * any tabular-nums context (time anchors, KPI values, capacity ratios).
 * Apply via inline class `font-mono` or local rule.
 */

/* ─── Text roles ────────────────────────────────────────────────────────
   Named typographic roles. Use these instead of hand-speccing
   size/weight/transform/tracking/color inline (the `text-[10px] uppercase
   tracking-[0.08em] text-muted` soup that recurs in ~47 files). Each maps to a
   --text-* token, so nothing is off-scale. They carry NO layout (no margins /
   display) so they compose on any element — for a standalone eyebrow WITH
   block spacing use .section-label; for a card's header use .dv-card__title.
   Role names, not size names (body-14 etc.): a name shouldn't lie when the
   scale shifts. Each role names its px in the comment for reference. */

/* Headings — structural h1–h5 classes that apply a heading level's appearance
   to ANY element, decoupled from the semantic tag (à la Primer .h1–.h6 / Carbon
   heading tokens / Polaris headingXl–Sm). Decoupling is deliberate: pick the
   tag for document structure, the class for size — bare <h1>–<h5> stay on
   Tailwind's preflight reset so a level never forces a size. Semantic role
   aliases share each style (.dv-title === .dv-h4), so both vocabularies work
   with one definition. Display font, 600, tight tracking. */
.os-h1, .os-display  { font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 600; line-height: var(--leading-3xl); letter-spacing: -0.025em; color: var(--ink); }  /* 28/36 — page hero */
.os-h2, .os-title-lg { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 600; line-height: var(--leading-2xl); letter-spacing: -0.02em;  color: var(--ink); }  /* 24/32 — major section */
.os-h3               { font-family: var(--font-display); font-size: var(--text-xl);  font-weight: 600; line-height: var(--leading-xl);  letter-spacing: -0.015em; color: var(--ink); }  /* 20/28 — subsection */
.os-h4, .os-title    { font-family: var(--font-display); font-size: var(--text-lg);  font-weight: 600; line-height: var(--leading-lg);  letter-spacing: -0.01em;  color: var(--ink); }  /* 18/28 — card / panel heading */
.os-h5, .os-title-sm { font-family: var(--font-display); font-size: var(--text-md);  font-weight: 600; line-height: var(--leading-md);  letter-spacing: -0.01em;  color: var(--ink); }  /* 16/24 — small heading */
/* Subtitle — supporting subhead under a title. */
.os-subtitle { font-size: var(--text-md); line-height: var(--leading-md); color: var(--muted); }  /* 16/24 — muted lead-in */
/* Body. */
.os-body-lg  { font-size: var(--text-md); line-height: var(--leading-md); color: var(--ink); }  /* 16/24 — roomy body */
.os-body     { font-size: var(--text-sm); line-height: var(--leading-sm); color: var(--ink); }  /* 14/22 — default body */
.os-small    { font-size: var(--text-xs); line-height: var(--leading-xs); color: var(--ink); }  /* 12/18 — small print */

.os-eyebrow {                         /* micro uppercase label — "STATUS", "BOAT" */
  font-size: var(--text-2xs);         /* 10px */
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.os-meta {                            /* secondary / supporting line */
  font-size: var(--text-xs);          /* 12px */
  color: var(--muted);
}
/* Mono variant — a dense monospace meta line (cabin/pax/ref codes). Geist Mono
   via --font-mono, one step smaller. Self-contained (carries the muted colour
   so it reads the same standalone or alongside .os-meta). From arena .ao-meta. */
.os-meta--mono {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);         /* 10px */
  color: var(--muted-2);
}
/* Mono numerals — tabular figures for money / IDs / counts (Geist Mono via
   --font-mono). Standalone; the gem previously had .os-mono only SCOPED inside
   header-band/popover. From arena .ao-mono. */
.os-mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.os-figure {                          /* numeric — money / rate / id / count */
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* Tone modifiers — additive; defined AFTER the base color rule so the semantic
   tone wins the cascade over the base color:var(--ink). Signals a status zone
   on a figure (e.g. pricing floor / recommended / ceiling). Token-only. */
.os-figure--confirmed { color: var(--confirmed); }
.os-figure--cancelled { color: var(--cancelled); }
.os-figure--pending   { color: var(--pending); }
.os-figure--accent    { color: var(--accent); }
.os-figure--muted     { color: var(--muted); }   /* notional / de-emphasised figure */
.os-caption {                         /* quietest micro caption */
  font-size: var(--text-2xs);         /* 10px */
  color: var(--muted-2);
}

/* ─── Standalone primitives promoted from arena-ops (HotelOS) 2026-07-25 ──────
   The gem previously had these names only SCOPED (a toolbar divider, a
   setting-item dot, a sum-prog row). These are the general-purpose standalone
   versions. Token-only. */

/* Divider — a hairline separator. --thin stretches to fill a flex row (a
   section rule between a label and the row edge); was arena .ao-rule. */
.os-divider { border: 0; margin: 0; height: 1px; background: var(--hairline); }
.os-divider--thin { flex: 1; height: 1px; align-self: center; background: var(--hairline); }

/* Dot — a small status dot; colour via currentColor so a parent/inline colour
   drives it. --lit adds a soft ring. Was arena .ao-dot. */
.os-dot { width: 8px; height: 8px; border-radius: var(--radius-pill); background: currentColor; flex: none; }
.os-dot--lit { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Row — a general list row: flex, vertically CENTRED (the scoped sum-prog row
   was baseline-aligned), hover highlight. Was arena .ao-row. */
.os-row { display: flex; align-items: center; gap: var(--space-3); transition: background var(--t-fast) var(--ease-standard); }
.os-row:hover { background: var(--canvas); }

/* ─── Section label ─────────────────────────────────────────────────── */
:is(.section-label, .os-section-label) {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 24px 0 8px;
  line-height: 1.4;
}

/* ─── Layout primitives: Stack + Inline ─────────────────────────────────
   Name the layout instead of hand-rolling `flex flex-col gap-3` /
   `flex items-center gap-2`. Stack = vertical flow, Inline = horizontal flow,
   both with a token gap off the --space scale. (Polaris BlockStack/InlineStack,
   Primer Stack, Braid Stack/Inline.) Default gap = the common case; the size
   modifiers step the scale and apply to both. Inline centers + wraps by default;
   alignment/justify modifiers cover the rest. */
.os-stack  { display: flex; flex-direction: column; gap: var(--space-3); }            /* 12 */
.os-inline { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--space-2); }  /* 8 */
/* Gap steps (both Stack + Inline). */
.os-stack--xs, .os-inline--xs { gap: var(--space-1); }  /* 4 */
.os-stack--sm, .os-inline--sm { gap: var(--space-2); }  /* 8 */
.os-stack--md, .os-inline--md { gap: var(--space-3); }  /* 12 */
.os-stack--lg, .os-inline--lg { gap: var(--space-4); }  /* 16 */
.os-stack--xl, .os-inline--xl { gap: var(--space-6); }  /* 24 */
/* Cross-axis + main-axis modifiers. */
.os-stack--center    { align-items: center; }
.os-inline--top      { align-items: flex-start; }
.os-inline--baseline { align-items: baseline; }
.os-inline--between  { justify-content: space-between; }
.os-inline--end      { justify-content: flex-end; }
.os-inline--nowrap   { flex-wrap: nowrap; }

/* ─── KPI strip ─────────────────────────────────────────────────────── */
:is(.kpi-strip, .os-kpi-strip) {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 28px 36px;
  margin: 0 0 28px;
  padding: 16px 0 0;
  border-top: 1px solid var(--border);
}
:is(.kpi-strip__stat, .os-kpi-strip__stat) {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
:is(.kpi-strip__value, .os-kpi-strip__value) {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-3xl);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  line-height: 1;
  margin: 0;
}
:is(.kpi-strip__label, .os-kpi-strip__label) {
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  color: var(--text-muted);
  margin: 0;
}

/* ─── StatBand — bordered metrics card (content-pane sibling of kpi_strip)
   2–5 cells, big tabular number over an uppercase label, inner hairlines.
   Sits on the gray canvas (NOT in the header band). Cell count via --cols.
   Spec: Design/agent-view-dive-sites-stat-card/handover/stat-band/. ──────── */
:is(.stat-band, .os-stat-band) {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), 1fr);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  margin-bottom: 20px;
}
:is(.stat-band__cell, .os-stat-band__cell) {
  padding: 18px 20px;
  border-left: 1px solid var(--hairline-2);
}
:is(.stat-band__cell, .os-stat-band__cell):first-child { border-left: 0; }
:is(.stat-band__label, .os-stat-band__label) {
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
:is(.stat-band__value, .os-stat-band__value) {
  margin-top: 9px;
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--ink);
}
:is(.stat-band__unit, .os-stat-band__unit) { font-size: 0.6em; font-weight: 500; color: var(--muted); }
:is(.stat-band__value--display, .os-stat-band__value--display) { font-family: var(--font-display); font-variant-numeric: normal; }
:is(.stat-band__value--positive, .os-stat-band__value--positive)  { color: var(--status-confirmed-fg); }
:is(.stat-band__value--attention, .os-stat-band__value--attention) { color: var(--status-briefing-fg); }
:is(.stat-band__value--negative, .os-stat-band__value--negative)  { color: var(--status-cancelled-fg); }
:is(.stat-band__value--empty, .os-stat-band__value--empty)     { color: var(--muted-2); font-weight: 500; }
/* Vertical variant — single column, cells stacked with top hairlines.
   For a narrow rail (e.g. dive-site facts) where vertical space is ample. */
:is(.stat-band--vertical, .os-stat-band--vertical) { grid-template-columns: 1fr; }
:is(.stat-band--vertical, .os-stat-band--vertical) :is(.stat-band__cell, .os-stat-band__cell) { border-left: 0; border-top: 1px solid var(--hairline-2); }
:is(.stat-band--vertical, .os-stat-band--vertical) :is(.stat-band__cell, .os-stat-band__cell):first-child { border-top: 0; }
@media (max-width: 640px) {
  :is(.stat-band, .os-stat-band) { grid-template-columns: repeat(2, 1fr); }
  :is(.stat-band__cell, .os-stat-band__cell):nth-child(odd)   { border-left: 0; }
  :is(.stat-band__cell, .os-stat-band__cell):nth-child(n + 3) { border-top: 1px solid var(--hairline-2); }
}

/* ─── Stat card — clickable KPI/filter tile ─────────────────────────────
   A bordered tile (swatch + label, big tabular value, sub-line) that toggles
   a filter when clicked. Replaces hand-rolled utility-soup tiles (gear
   category KPIs) that drifted off the type scale and underlined on hover
   because BT's global a:hover beat the `no-underline` utility. As an <a> it
   resets its own hover underline. Sizes are tokens, not raw px. */
.os-stat-card {
  display: block;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  transition: border-color 150ms ease-out, background 150ms ease-out;
}
.os-stat-card:hover { border-color: var(--hairline-strong); text-decoration: none; }
.os-stat-card--active { border-color: var(--accent); background: var(--accent-softer); }
.os-stat-card--active:hover { border-color: var(--accent); }
.os-stat-card__head { display: flex; align-items: center; gap: var(--space-2); }
.os-stat-card__swatch { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
.os-stat-card__label {
  font-family: var(--font-display); font-size: var(--text-sm); font-weight: 500; color: var(--ink);
}
.os-stat-card__value {
  font-family: var(--font-mono); font-size: var(--text-lg); font-weight: 500;
  font-variant-numeric: tabular-nums; color: var(--ink); line-height: 1; margin-top: 6px;
}
.os-stat-card__sub { font-size: var(--text-2xs); font-weight: 500; color: var(--muted); margin-top: 6px; }
.os-stat-card__sub--due { color: var(--briefing); }

/* ─── Header zone ───────────────────────────────────────────────────── */
:is(.header-zone, .os-header-zone) {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}
:is(.header-zone__title, .os-header-zone__title) {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-3xl);
  line-height: 1.1;
  letter-spacing: -0.025em;
  color: var(--text-primary);
  margin: 0;
}
:is(.header-zone__meta, .os-header-zone__meta) {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  margin: 8px 0 0;
}
:is(.header-zone__actions, .os-header-zone__actions) {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}
:is(.header-zone__action-button, .os-header-zone__action-button) {
  display: inline-flex;
  align-items: center;
  padding: 8px 14px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 150ms ease-out;
}
:is(.header-zone__action-button, .os-header-zone__action-button):hover { background: var(--surface-alt); }
:is(.header-zone__action-button, .os-header-zone__action-button):disabled { opacity: 0.5; cursor: not-allowed; }
:is(.header-zone__action-button--primary, .os-header-zone__action-button--primary) {
  background: var(--accent);
  color: #FFFFFF;
  border-color: var(--accent);
}
:is(.header-zone__action-button--primary, .os-header-zone__action-button--primary):hover { background: var(--accent-strong); border-color: var(--accent-strong); }

/* Destructive action — used for bottom-LEFT footer actions in side panels
   (Set inactive, Retire item). Ghost background + danger/red text; on hover it
   gains a soft red tint and red border so the destructive intent reads clearly
   without competing with the filled primary on the right. */
:is(.header-zone__action-button--danger, .os-header-zone__action-button--danger) {
  background: transparent;
  color: var(--cancelled);
  border-color: transparent;
}
:is(.header-zone__action-button--danger, .os-header-zone__action-button--danger):hover {
  background: var(--cancelled-bg);
  color: var(--cancelled);
  border-color: var(--cancelled-border);
}

/* ─── Dives surface specifics ──────────────────────────────────────── */
:is(.dives-view-bar, .os-dives-view-bar) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 16px;
}
:is(.dives-list, .os-dives-list) {
  list-style: none;
  padding: 0;
  margin: 0 0 24px;
}
:is(.dives-empty, .os-dives-empty) {
  padding: 64px 16px;
  text-align: center;
  color: var(--text-muted);
}
:is(.dives-empty--wide, .os-dives-empty--wide) { padding: 96px 16px 64px; }
:is(.dives-empty__line, .os-dives-empty__line) {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-lg);
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0 0 8px;
}
:is(.dives-empty__hint, .os-dives-empty__hint) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
  max-width: 360px;
  margin-left: auto;
  margin-right: auto;
}

/* ─── Drawer body lists (Divers / Stops tabs) ──────────────────────── */
:is(.drawer-list, .os-drawer-list) {
  list-style: none;
  padding: 0;
  margin: 0 0 8px;
}
:is(.drawer-empty, .os-drawer-empty) {
  color: var(--text-muted);
  font-style: italic;
  font-size: var(--text-sm);
  margin: 8px 0 16px;
}
:is(.drawer-notes, .os-drawer-notes) {
  font-size: var(--text-sm);
  color: var(--text-primary);
  line-height: 1.5;
  margin: 0 0 16px;
}
:is(.drawer-notes--incident, .os-drawer-notes--incident) { color: var(--red-700); }

/* ─── Settings shell layout (T12) ──────────────────────────────────── */
:is(.settings-layout, .os-settings-layout) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 32px;
  margin-top: 8px;
}
@media (max-width: 1023px) {
  :is(.settings-layout, .os-settings-layout) { grid-template-columns: 1fr; gap: 16px; }
}

/* ─── Settings hub (v3): nav + detail split INSIDE the .diveos-main pane ──
   Container query, not viewport: the split lives in the pane (viewport − the
   240px sidebar), so at a ~1024px window the pane is ~784px and the 200px nav
   left the detail card only ~409px — the description wrapped one word per line
   and the status chips clipped. Stack on a narrow PANE so the cards get full
   width; the nav becomes a horizontal wrapping bar on top. */
:is(.settings-grid, .os-settings-grid) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 32px;
  margin-top: 8px;
}
:is(.settings-subnav, .os-settings-subnav) { border-right: 1px solid var(--hairline); padding-right: 16px; }
:is(.settings-subnav__items, .os-settings-subnav__items) { display: flex; flex-direction: column; gap: 2px; }
/* Card fix FIRST (pane ≤880): the row's status chips drop BELOW the title/meta
   instead of competing for row width, so the description reads on a full measure
   line rather than one word per line. This alone de-cramps the card while the
   nav stays a 2-column rail — no need to stack the nav (which would leave a
   short section as one lonely card over a tall void). */
@container (max-width: 880px) {
  :is(.setting-item, .os-setting-item) { grid-template-columns: auto 1fr; row-gap: 8px; }
  :is(.setting-item__trail, .os-setting-item__trail) {
    grid-column: 2;
    flex-wrap: wrap;
    justify-content: flex-start;
  }
}
/* Legacy settings feature-cards (integrations, developer tools, PMS): the row
   is `icon | body(flex-1) | trailing action/status`. On a narrow pane the body
   collapses to one word per line and the trailing element crowds/overlaps it.
   Wrap the row so the trailing action drops to its own full-width line under the
   body, which keeps a readable measure. */
@container (max-width: 700px) {
  .os-feature-row { flex-wrap: wrap; row-gap: 12px; }
  .os-feature-row > .os-feature-row__end {
    flex-basis: 100%;
    justify-content: flex-start;
    max-width: none;
    text-align: left;
  }
}

/* Only stack the nav to a top bar once the pane is too narrow for the 200px
   rail + a usable detail column (phones / very narrow panes). */
@container (max-width: 640px) {
  :is(.settings-grid, .os-settings-grid) { grid-template-columns: 1fr; gap: 16px; }
  :is(.settings-subnav, .os-settings-subnav) {
    border-right: 0;
    border-bottom: 1px solid var(--hairline);
    padding-right: 0;
    padding-bottom: 10px;
  }
  :is(.settings-subnav__group, .os-settings-subnav__group) { margin-bottom: 10px; }
  :is(.settings-subnav__items, .os-settings-subnav__items) { flex-direction: row; flex-wrap: wrap; gap: 4px; }
}
:is(.settings-sub-nav, .os-settings-sub-nav) {
  border-right: 1px solid var(--border);
  padding-right: 16px;
}
@media (max-width: 1023px) {
  :is(.settings-sub-nav, .os-settings-sub-nav) { border-right: none; padding-right: 0; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
  :is(.settings-sub-nav__items, .os-settings-sub-nav__items) { display: flex; flex-wrap: wrap; gap: 4px; }
}
:is(.settings-sub-nav__items, .os-settings-sub-nav__items) {
  list-style: none;
  padding: 0;
  margin: 0;
}
:is(.settings-sub-nav__item, .os-settings-sub-nav__item) { position: relative; }
:is(.settings-sub-nav__link, .os-settings-sub-nav__link) {
  display: block;
  padding: 8px 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background 150ms ease-out, color 150ms ease-out;
}
:is(.settings-sub-nav__link, .os-settings-sub-nav__link):hover {
  background: var(--surface-alt);
  color: var(--text-primary);
}
:is(.settings-sub-nav__item--active, .os-settings-sub-nav__item--active) :is(.settings-sub-nav__link, .os-settings-sub-nav__link) {
  background: var(--accent-tint);
  color: var(--accent-strong);
}
:is(.settings-sub-nav__item--active, .os-settings-sub-nav__item--active)::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 2px;
  background: var(--accent);
  border-radius: 0 1px 1px 0;
}
@media (max-width: 1023px) {
  :is(.settings-sub-nav__item--active, .os-settings-sub-nav__item--active)::before { display: none; }
}
:is(.settings-content, .os-settings-content) { min-width: 0; }

/* ─── Settings nav item (redesigned, icon + label) ──────────────────────
   The grouped Settings sub-nav (Shop / Operations / People / Account) — an
   icon + label row that is the active section's accent-soft chip and warms on
   hover. Promotes the per-item utility soup (the hand-rolled active/hover
   string with an arbitrary hover:bg-[var(--hover-warm)]) into a token-driven
   component, mirroring how .dv-filter-bar scopes the filter row. The group
   heading uses .dv-eyebrow (named micro-label). */
.os-settings-nav__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  transition: background 150ms ease, color 150ms ease;
}
.os-settings-nav__item:hover, .os-settings-nav__item:focus-visible { background: var(--hover-warm); text-decoration: none; color: var(--ink); }
.os-settings-nav__item svg { color: var(--muted); flex-shrink: 0; }
.os-settings-nav__item.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.os-settings-nav__item.is-active:hover { background: var(--accent-soft); color: var(--accent); }
.os-settings-nav__item.is-active svg { color: var(--accent); }
:is(.settings-link-list, .os-settings-link-list) {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Status summary grid — each section partial renders this above the link
   list to show at-a-glance configuration state. Two columns; --ok rows
   show in green-700, --todo rows in amber-700, both via tokens.css. */
:is(.settings-status, .os-settings-status) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 24px;
  margin: 16px 0 24px;
  padding: 0;
}
@media (max-width: 1023px) {
  :is(.settings-status, .os-settings-status) { grid-template-columns: 1fr; }
}
:is(.settings-status__row, .os-settings-status__row) {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  margin: 0;
}
:is(.settings-status__label, .os-settings-status__label) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
}
:is(.settings-status__value, .os-settings-status__value) {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  margin: 0;
  font-variant-numeric: tabular-nums;
}
:is(.settings-status__row--ok, .os-settings-status__row--ok) :is(.settings-status__value, .os-settings-status__value) {
  color: var(--green-700);
}
:is(.settings-status__row--todo, .os-settings-status__row--todo) :is(.settings-status__value, .os-settings-status__value) {
  color: var(--amber-700);
}
:is(.settings-status__swatch, .os-settings-status__swatch) {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
}
:is(.settings-status__hint, .os-settings-status__hint) {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-left: 6px;
}

/* ─── Modern settings list (card rows) ───────────────────────────────
   v4 modernization. Replaces the .settings-status stat-strip + tight
   ledger-row with richer card rows: leading icon, title + description,
   trailing status pill + chevron. One row per editable thing; status is
   folded onto the row (no separate stat-strip), counts neutral, only true
   needs-attention / healthy state takes color. */
:is(.setting-list, .os-setting-list) {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}
:is(.setting-list__item, .os-setting-list__item) { margin: 0; }
:is(.setting-item, .os-setting-item) {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
  transition: background 150ms ease;
}
:is(.setting-list__item, .os-setting-list__item):last-child :is(.setting-item, .os-setting-item) { border-bottom: 0; }
:is(.setting-item--linked, .os-setting-item--linked) { cursor: pointer; }
:is(.setting-item--linked, .os-setting-item--linked):hover { background: var(--hover-warm); }
:is(.setting-item__icon, .os-setting-item__icon) {
  width: 36px;
  height: 36px;
  border-radius: 9px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  flex-shrink: 0;
}
:is(.setting-item__body, .os-setting-item__body) {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
:is(.setting-item__title, .os-setting-item__title) {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
:is(.setting-item__meta, .os-setting-item__meta) {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.4;
}
:is(.setting-item__trail, .os-setting-item__trail) {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
:is(.setting-item__pill, .os-setting-item__pill) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  padding: 4px 9px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  border: 1px solid transparent;
}
:is(.setting-item__pill--count, .os-setting-item__pill--count) { background: var(--surface-alt); color: var(--text-muted); border-color: var(--border); }
:is(.setting-item__pill--warn, .os-setting-item__pill--warn)  { background: var(--status-briefing-bg);  color: var(--amber-700); border-color: var(--status-briefing-border, var(--border)); }
:is(.setting-item__pill--ok, .os-setting-item__pill--ok)    { background: var(--status-confirmed-bg);  color: var(--green-700); border-color: var(--status-confirmed-border, var(--border)); }
:is(.setting-item__pill, .os-setting-item__pill) :is(.dot, .os-dot) { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
:is(.setting-item__chevron, .os-setting-item__chevron) { color: var(--text-muted); display: inline-flex; align-items: center; }
:is(.setting-item--linked, .os-setting-item--linked):focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) { :is(.setting-item, .os-setting-item) { transition: none; } }

/* ─── Filter bar ────────────────────────────────────────────────────── */
:is(.filter-bar, .os-filter-strip) {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-bottom: 16px;
}
:is(.filter-bar, .os-filter-strip)::-webkit-scrollbar { display: none; }
/* v4: aligned to the repo .chip spec — 28px pill, hairline border on
   surface; active = accent-soft bg + accent-border + accent text. */
:is(.filter-pill, .os-filter-pill) {
  display: inline-flex;
  align-items: center;
  /* A filter pill is deliberately ONE STEP SHORTER than the primary control
     beside it — it is a scoping affordance, not an action, and a strip of
     full-height pills reads as a row of buttons. 8px is that shipped offset
     (36 base minus 28), so at base density this computes to exactly the 28px
     literal it replaces. The 28px floor stops --compact shrinking a pill to
     an unhittable 24.

     THIS IS THE COMPONENT THE AUDIT WAS ABOUT: a filter control frozen at a
     desktop size, with no field or touch variant, on a surface used one-handed
     on a boat. It reaches 40px under .os-density--touch — over the safe target
     size — with no local class and no new per-component modifier. */
  height: max(28px, calc(var(--control-h) - 8px));
  padding: 0 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  text-decoration: none;
  transition: background 150ms ease-out;
}
:is(.filter-pill, .os-filter-pill):hover { background: var(--canvas); }
:is(.filter-pill--active, .os-filter-pill--active) {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent-border);
}

/* ─── Operations bar (.dv-opbar) ─────────────────────────────────────────
   Consolidated status + actions strip at the foot of a form-detail page
   (sharing / public-access / documents). Uses the shared cool blue-gray
   summary surface (#F2F5F9 / #D5DEEA — same as .ds-sum) so it reads as a
   DISTINCT operations panel, not another white form-field info_card. Status +
   meta on the left, action buttons + a QR tile on the right. Standard for
   status/CTA sections across forms. */
.os-opbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
  background: #F2F5F9;             /* shared cool blue-gray summary surface */
  border: 1px solid #D5DEEA;
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}
.os-opbar__main { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.os-opbar__label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--slate-500);
}
.os-opbar__label svg { color: var(--muted-2); }
.os-opbar__facts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.os-opbar__meta { font-size: var(--text-sm); color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }
.os-opbar__meta svg { color: var(--confirmed); }
.os-opbar__dot { color: var(--hairline-strong); }
.os-opbar__files { display: flex; gap: 16px; font-size: var(--text-sm); }
.os-opbar__actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.os-opbar__actions form { margin: 0; }
/* QR on demand via a <details> popover (C variant — the inline thumbnail was
   the A variant). "Show QR" is a secondary button; the QR floats below it. */
.os-opbar__qrtoggle { position: relative; }
.os-opbar__qrtoggle > summary { list-style: none; cursor: pointer; }
.os-opbar__qrtoggle > summary::-webkit-details-marker { display: none; }
.os-opbar__qrtoggle > summary::marker { content: ""; }
.os-opbar__qrpop {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-panel);
  padding: 14px; text-align: center; width: max-content;
}
.os-opbar__qr { width: 132px; height: 132px; display: grid; place-items: center; }
.os-opbar__qr svg { width: 100%; height: 100%; display: block; }
.os-opbar__qrcap { display: block; font-size: var(--text-xs); color: var(--muted-2); margin-top: 8px; }

/* ─── Status pill ───────────────────────────────────────────────────── */
/* The gem owns the FULL component: base, size/shape modifiers, the seven
   colour variants, the child elements, and the interactive (button) states.
   Every variant is a pure pass-through to the --status-<key>-{bg,fg,border}
   tokens in tokens/semantic.css, so the palette is defined once, here.

   A consuming app supplies only two things: the human LABEL, and the mapping
   from its own domain status to a variant key (e.g. a status-palette helper
   deciding that an unpaid invoice is `--pending`). It must NOT redefine any
   `.status-pill*` rule locally — a local copy silently overrides the gem and
   the two systems drift apart. */
:is(.status-pill, .os-status-pill) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
/* Leading Lucide glyph inside the pill. This rule MUST live here: the gem's
   own `status_icon_svg` (helpers/os_ds/status_palette_helper.rb) emits
   `class="status-icon"`, so a gem-rendered icon would be unstyled if the rule
   sat in a consuming app. Sized in fixed px rather than a token — it tracks the
   pill's 12px optical glyph size, not the type scale. */
:is(.status-icon, .os-status-icon) { width: 12px; height: 12px; flex-shrink: 0; }

/* Size modifier (mirrors .dv-badge--sm) */
:is(.status-pill--sm, .os-status-pill--sm) { font-size: var(--text-2xs); padding: 2px 8px; }
/* Shape modifier: square uppercase tag (mirrors .dv-badge--square) */
:is(.status-pill--square, .os-status-pill--square) {
  border-radius: 5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-2xs);
  padding: 3px 8px;
}

:is(.status-pill__label, .os-status-pill__label) { line-height: 1; }
:is(.status-pill__dot, .os-status-pill__dot) {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
/* Legacy alias: `.status-pill-dot` (single dash) is the pre-BEM spelling of
   `.status-pill__dot` and is still used by host-app views. Kept so those
   callers keep rendering; prefer `.status-pill__dot` in new markup. */
:is(.status-pill-dot, .os-status-pill-dot) {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* Colour variants — pure token pass-throughs. Palette pivot 2026-05-23:
   Confirmed=green (was blue), In Water=sky (was emerald). The theme is
   light-only, so there is no dark branch here or in the tokens. */
:is(.status-pill--pending, .os-status-pill--pending)   { background: var(--status-pending-bg);   border-color: var(--status-pending-border);   color: var(--status-pending-fg); }
:is(.status-pill--confirmed, .os-status-pill--confirmed) { background: var(--status-confirmed-bg); border-color: var(--status-confirmed-border); color: var(--status-confirmed-fg); }
:is(.status-pill--briefing, .os-status-pill--briefing)  { background: var(--status-briefing-bg);  border-color: var(--status-briefing-border);  color: var(--status-briefing-fg); }
:is(.status-pill--in_water, .os-status-pill--in_water)  { background: var(--status-in-water-bg);  border-color: var(--status-in-water-border);  color: var(--status-in-water-fg); }
:is(.status-pill--returning, .os-status-pill--returning) { background: var(--status-returning-bg); border-color: var(--status-returning-border); color: var(--status-returning-fg); }
:is(.status-pill--completed, .os-status-pill--completed) { background: var(--status-completed-bg); border-color: var(--status-completed-border); color: var(--status-completed-fg); }
:is(.status-pill--cancelled, .os-status-pill--cancelled) { background: var(--status-cancelled-bg); border-color: var(--status-cancelled-border); color: var(--status-cancelled-fg); }

/* NON-STATUS variants. Everything above names a dive/stay STATE; these two name
   an EMPHASIS, for pills that are not statuses at all — a count, a label, a
   flag. Added because a consuming product had built its own tag family whose
   only genuinely missing colours were these two, while its `--alert` and
   `--warn` turned out to be exact duplicates of `--cancelled` and `--briefing`.
   Naming them here keeps a product from re-deriving a parallel pill set just to
   get "accent" and "muted". */
:is(.status-pill--accent, .os-status-pill--accent) { background: var(--accent-soft); border-color: var(--accent-border); color: var(--accent-strong); }
:is(.status-pill--muted, .os-status-pill--muted)  { background: var(--surface-alt); border-color: var(--hairline);      color: var(--muted); }

/* Editable status pill — the badge-dropdown trigger is a button.status-pill.
   It gets interaction affordances the static pill never does: a subtle hover
   and a token-driven focus-visible ring. The static .status-pill stays inert. */
button:is(.status-pill, .os-status-pill) {
  cursor: pointer;
  /* No resting shadow — the chevron + hover brightness carry the control
     affordance. (A shadow here reads as unwanted elevation on a flat chip.) */
  transition: filter 120ms ease;
}
button:is(.status-pill, .os-status-pill):hover {
  filter: brightness(0.97);
}
button:is(.status-pill, .os-status-pill):focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* ─── Open row ──────────────────────────────────────────────────────── */
:is(.open-row, .os-open-row) {
  list-style: none;
  border-bottom: 1px solid var(--border);
}
:is(.open-row__link, .os-open-row__link), :is(.open-row, .os-open-row) > *:not(:has(:is(.open-row__link, .os-open-row__link))) {
  display: grid;
  grid-template-columns: 96px 1fr auto;
  gap: 24px;
  align-items: center;
  padding: 16px;
  text-decoration: none;
  color: inherit;
  min-height: 96px;
}
:is(.open-row__link, .os-open-row__link) { transition: background 150ms ease-out; }
:is(.open-row__link, .os-open-row__link):hover { background: var(--surface-alt); }
:is(.open-row__time-block, .os-open-row__time-block) {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
:is(.open-row__time, .os-open-row__time) {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-3xl);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  line-height: 1;
}
:is(.open-row__time-meta, .os-open-row__time-meta) {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-2xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 6px;
}
:is(.open-row__body, .os-open-row__body) { min-width: 0; }
:is(.open-row__primary, .os-open-row__primary) {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 4px;
}
:is(.open-row__title, .os-open-row__title) {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--text-md);
  color: var(--text-primary);
}
:is(.open-row__meta, .os-open-row__meta) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
  line-height: 1.4;
}
:is(.open-row__right, .os-open-row__right) {
  text-align: right;
}
:is(.open-row__right-value, .os-open-row__right-value) {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  line-height: 1;
}
:is(.open-row__right-label, .os-open-row__right-label) {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-2xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 4px;
}

/* ─── Ledger row ────────────────────────────────────────────────────── */
:is(.ledger-row, .os-ledger-row) {
  list-style: none;
  border-bottom: 1px solid var(--border);
}
:is(.ledger-row__link, .os-ledger-row__link), :is(.ledger-row, .os-ledger-row) > *:not(:has(:is(.ledger-row__link, .os-ledger-row__link))) {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 12px 16px;
  min-height: 48px;
  text-decoration: none;
  color: inherit;
}
:is(.ledger-row__link, .os-ledger-row__link) { transition: background 150ms ease-out; }
:is(.ledger-row__link, .os-ledger-row__link):hover { background: var(--surface-alt); }
:is(.ledger-row__time, .os-ledger-row__time) {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--text-muted);
  flex-shrink: 0;
  width: 60px;
}
:is(.ledger-row__title, .os-ledger-row__title) {
  font-size: var(--text-sm);
  color: var(--text-primary);
  flex: 1;
}
:is(.ledger-row__meta, .os-ledger-row__meta) {
  font-size: var(--text-sm);
  color: var(--text-muted);
}
:is(.ledger-row__value, .os-ledger-row__value) {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--text-primary);
  flex-shrink: 0;
}

/* ─── Side panel (drawer) ───────────────────────────────────────────── */
:is(.side-panel, .os-side-panel) {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
}
:is(.side-panel__header, .os-side-panel__header) {
  padding: 22px 24px 16px;
  border-bottom: 1px solid var(--border);
}
:is(.side-panel__eyebrow, .os-side-panel__eyebrow) {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 8px;
}
:is(.side-panel__close, .os-side-panel__close) {
  float: right;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  transition: background 150ms ease-out, color 150ms ease-out;
}
:is(.side-panel__close, .os-side-panel__close):hover {
  background: var(--surface-alt);
  color: var(--text-primary);
}
/* --split — OPT-IN header layout for drawers that pair a wrapped title/subtitle
   block with the close on one row. The DEFAULT header stays block-flow (the
   close floats, title/subtitle stack as direct children) — do NOT flex the
   header by default: consumers that place title/subtitle as direct children
   would have them laid out horizontally. Under --split the header becomes a
   flex row and the close right-aligns via auto inline-start margin instead of
   the float (float is a no-op inside a flex container). align-items:flex-start
   keeps the close pinned to the top while the title block sets the row height. */
:is(.side-panel__header--split, .os-side-panel__header--split) {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
:is(.side-panel__header--split, .os-side-panel__header--split) :is(.side-panel__close, .os-side-panel__close) {
  float: none;
  margin-inline-start: auto;
}
:is(.side-panel__close-icon, .os-side-panel__close-icon) { width: 18px; height: 18px; stroke-width: 1.75; }
:is(.side-panel__title, .os-side-panel__title) {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xl);
  color: var(--text-primary);
  margin: 0 0 8px;
  letter-spacing: -0.01em;
}
/* Title + status on one row, meta drops below (booking drawer, #113). */
:is(.side-panel__title-row, .os-side-panel__title-row) {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 8px;
}
:is(.side-panel__title--inline, .os-side-panel__title--inline) {
  margin: 0;
}
:is(.side-panel__subtitle, .os-side-panel__subtitle) {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
}
:is(.side-panel__meta, .os-side-panel__meta) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
:is(.side-panel__tab-zone, .os-side-panel__tab-zone) {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
/* Tab bar shares the panel's 24px gutter (header/body) instead of sitting
   flush-left. Applies to every drawer that uses the side_panel chrome. */
:is(.side-panel__tab-zone, .os-side-panel__tab-zone) > :is(.tabs, .os-tabs) { padding-left: 24px; padding-right: 24px; }
:is(.side-panel__body, .os-side-panel__body) {
  flex: 1;
  overflow-y: auto;
  padding: 20px 24px;
  /* Positioning context so absolutely-positioned descendants (e.g. the
     `.dv-segment__input.sr-only` DIN/INT radios) are contained by the
     scroll body, not the `position: fixed` `.side-drawer` wrapper. Without
     this, focusing a visually-hidden control scroll-into-views the fixed
     wrapper and yanks the whole panel off-screen ("drawer scroll broke"). */
  position: relative;
}
:is(.side-panel__action-bar, .os-side-panel__action-bar) {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 24px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
:is(.side-panel__action-bar-left, .os-side-panel__action-bar-left) { flex: 1; }
:is(.side-panel__action-bar-right, .os-side-panel__action-bar-right) {
  display: flex;
  gap: 8px;
  /* Always pin to the right edge. With a left/destructive action present the
     flex:1 left pushes us right (left|……|right); with no left action the
     margin-left:auto still drives us to the right edge so a lone primary CTA
     is bottom-right by default (not flex-start/left). */
  margin-left: auto;
}
/* Single-CTA footers (e.g. the booking quick-view "View full booking") pin
   their one action to the right edge. Now redundant since the default
   right-aligns via margin-left:auto — kept for any explicit callers. */
:is(.side-panel__action-bar--end, .os-side-panel__action-bar--end) { justify-content: flex-end; }

/* Width modifier — opt-in wider (560px) read-only summary drawer. Add
   `.side-panel--wide` to the `.side-panel` section when a surface needs the
   roomier variant. */
:is(.side-panel--wide, .os-side-panel--wide) { max-width: 560px; }

/* ─── Field grid ────────────────────────────────────────────────────── */
:is(.field-grid, .os-field-grid) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
/* Cancel the stacked-field rhythm inside any field grid — the columns are
   adjacent siblings, so the `.os-field + .os-field` top-margin in forms.css
   would push every non-first cell down and break horizontal alignment across
   columns. The grid `gap` owns spacing. Shared by `.field-grid` and
   `.field-split` so the twin patterns can't drift apart. */
:is(.field-grid, .os-field-grid, .field-split, .os-field-split) .os-field + .os-field { margin-top: 0; }
:is(.field-grid__field--span-2, .os-field-grid__field--span-2) { grid-column: 1 / -1; }
:is(.field-grid__label, .os-field-grid__label) {
  display: block;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-bottom: 4px;
}
:is(.field-grid__value, .os-field-grid__value) {
  font-size: var(--text-sm);
  color: var(--text-primary);
}

/* ─── Segmented control ─────────────────────────────────────────────── */
:is(.segmented, .os-segmented) {
  display: inline-flex;
  background: var(--surface-alt);
  border-radius: var(--radius-md);
  padding: 4px;
  gap: 4px;
}
:is(.segmented__option, .os-segmented__option) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Button chrome reset. This component was authored for `<a>` (hence the
     `text-decoration: none` below) and never reset the UA's button styles — so
     rendered as a `<button>`, which is the CORRECT element for a control that
     toggles rather than navigates, the browser's `border: 2px outset` and
     `background: buttonface` showed through and the group read as two
     mismatched boxes instead of one segmented control. Reported by the product
     owner as "the entire page is off"; the measured computed values were
     `border: 2px rgb(0,0,0)` and `background: rgb(239,239,239)`.
     `.os-btn` has always carried this reset; this component simply never did. */
  appearance: none;
  border: 0;
  background: none;
  font-family: inherit;
  line-height: inherit;
  cursor: pointer;
  /* Density is applied to the VERTICAL PADDING, not to a height. A segmented
     option has never declared a height — it is sized by its label's line box
     (~20px) plus 6px of padding, and introducing a min-height here would set
     a floor this component has never had, which could grow an option whose
     label is smaller than the default. Scaling the padding instead leaves the
     natural sizing intact and moves nothing at base density:
     (36 − 24) / 2 = 6px, the exact literal it replaces. The 6px floor holds
     --compact at today's size rather than crushing the option.

     Horizontal padding stays a 12px literal: this control ships 12px, not the
     button's 14px --control-pad-x, and a token contract may not resize it. */
  padding: max(6px, calc((var(--control-h) - 24px) / 2)) 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background 150ms ease-out, color 150ms ease-out;
}
:is(.segmented__option, .os-segmented__option):hover { color: var(--text-primary); }
:is(.segmented__option, .os-segmented__option):focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
:is(.segmented__option--active, .os-segmented__option--active) {
  background: var(--surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}
:is(.segmented__option--active, .os-segmented__option--active):focus-visible {
  box-shadow: var(--shadow-sm), var(--shadow-focus);
}

/* Form-field variant — full-width equal segments, backed by a native radio
   group; the checked segment lifts to white with accent text (A5 difficulty
   picker). Native radios + :has, no JS. */
:is(.segmented--field, .os-segmented--field) { display: flex; width: 100%; }
:is(.segmented--field, .os-segmented--field) :is(.segmented__option, .os-segmented__option) {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  cursor: pointer;
  user-select: none;
}
:is(.segmented__radio, .os-segmented__radio) { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
:is(.segmented--field, .os-segmented--field) :is(.segmented__option, .os-segmented__option):has(:is(.segmented__radio, .os-segmented__radio):checked) {
  background: var(--surface);
  color: var(--accent);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}
:is(.segmented--field, .os-segmented--field) :is(.segmented__option, .os-segmented__option):has(:is(.segmented__radio, .os-segmented__radio):focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Count pill inside a segment (e.g. Arrivals 12 / Departures 3). Mono, tints to
   the accent on the active segment. --seg-count-size overrides the glyph size. */
.os-segmented__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  margin-left: 6px;
  border-radius: 9999px;
  background: var(--surface-alt);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--seg-count-size, 11px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
:is(.os-segmented__option--active, .os-segmented__option:has(.os-segmented__radio:checked)) .os-segmented__count {
  background: var(--accent-soft);
  color: var(--accent-strong);
}

/* ─── Toggle switch (form-bound) ────────────────────────────────────────
   A styled boolean control backed by a native checkbox that submits with the
   form (unlike the fetch-based .toggle_switch component). Native checkbox +
   :has(), no JS. */
:is(.toggle-switch, .os-toggle-switch) {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
}
:is(.toggle-switch__input, .os-toggle-switch__input) {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
:is(.toggle-switch__track, .os-toggle-switch__track) {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 24px;
  background: var(--hairline-strong);
  border-radius: var(--radius-pill);
  transition: background 150ms ease-out;
}
:is(.toggle-switch__knob, .os-toggle-switch__knob) {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  background: var(--surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-sm);
  transition: transform 150ms ease-out;
}
:is(.toggle-switch__input, .os-toggle-switch__input):checked ~ :is(.toggle-switch__track, .os-toggle-switch__track), :is(.toggle-switch, .os-toggle-switch):has(:is(.toggle-switch__input, .os-toggle-switch__input):checked) :is(.toggle-switch__track, .os-toggle-switch__track) {
  background: var(--accent);
}
:is(.toggle-switch__input, .os-toggle-switch__input):checked ~ :is(.toggle-switch__track, .os-toggle-switch__track) :is(.toggle-switch__knob, .os-toggle-switch__knob), :is(.toggle-switch, .os-toggle-switch):has(:is(.toggle-switch__input, .os-toggle-switch__input):checked) :is(.toggle-switch__knob, .os-toggle-switch__knob) {
  transform: translateX(16px);
}
:is(.toggle-switch__input, .os-toggle-switch__input):focus-visible ~ :is(.toggle-switch__track, .os-toggle-switch__track) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ─── Tabs ──────────────────────────────────────────────────────────── */
:is(.tabs, .os-tabs) {
  display: flex;
  gap: 24px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 16px;
}
:is(.tabs__tab, .os-tabs__tab) {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 12px 0;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 150ms ease-out, border-color 150ms ease-out;
}
:is(.tabs__tab, .os-tabs__tab):hover { color: var(--text-primary); }
:is(.tabs__tab, .os-tabs__tab):focus-visible {
  outline: none;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-focus);
}
:is(.tabs__tab--active, .os-tabs__tab--active) {
  color: var(--text-primary);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
:is(.tabs__count, .os-tabs__count) {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
:is(.tabs__tab--active, .os-tabs__tab--active) :is(.tabs__count, .os-tabs__count) { color: var(--text-secondary); }

/* ─── Equipment lifecycle action bar (P4-1 T4) ────────────────────────
   State-aware action zone on the EquipmentItem show page. Status pill
   + context-appropriate transition buttons. Calm-surface: button row
   reads as one composition with the status, not as 4 separate widgets. */
:is(.equipment-item-status, .os-equipment-item-status) {
  margin-bottom: 12px;
}
:is(.equipment-item-actions, .os-equipment-item-actions) {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}
:is(.equipment-item-actions, .os-equipment-item-actions) form { margin: 0; }
:is(.equipment-item-actions, .os-equipment-item-actions) :is(.button, .os-button) {
  font-size: var(--text-sm);
}

/* ─── Participant form (P4-2) ────────────────────────────────────────
   Collapsible sections via <details>. Per design 1C: tracked + free-form
   gear default collapsed when empty, auto-expand when assigned. */
:is(.participant-form__section, .os-participant-form__section) {
  margin: 16px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
:is(.participant-form__section-summary, .os-participant-form__section-summary) {
  cursor: pointer;
  padding: 12px 16px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--text-primary);
  user-select: none;
  list-style: none;
}
:is(.participant-form__section-summary, .os-participant-form__section-summary)::-webkit-details-marker { display: none; }
:is(.participant-form__section-summary, .os-participant-form__section-summary)::before {
  content: "▸";
  display: inline-block;
  margin-right: 8px;
  font-size: var(--text-2xs);
  color: var(--text-muted);
  transition: transform 150ms ease-out;
}
:is(.participant-form__section, .os-participant-form__section)[open] > :is(.participant-form__section-summary, .os-participant-form__section-summary)::before {
  transform: rotate(90deg);
}
:is(.participant-form__section-hint, .os-participant-form__section-hint) {
  margin-left: 8px;
  font-weight: 400;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
:is(.participant-form__section-body, .os-participant-form__section-body) {
  padding: 8px 16px 16px;
  border-top: 1px solid var(--border);
}
:is(.participant-form__empty-hint, .os-participant-form__empty-hint) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 8px 0;
}

:is(.participant-form__tracked-items, .os-participant-form__tracked-items) {
  list-style: none;
  padding: 0;
  margin: 8px 0 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
:is(.participant-form__tracked-item, .os-participant-form__tracked-item) {
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  transition: background 100ms ease-out;
}
:is(.participant-form__tracked-item, .os-participant-form__tracked-item):hover { background: var(--surface-alt); }
:is(.participant-form__tracked-label, .os-participant-form__tracked-label) {
  display: flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
  min-height: 44px;  /* WCAG AA touch target per design 6C */
}
:is(.participant-form__tracked-input, .os-participant-form__tracked-input) { margin: 0; }
:is(.participant-form__tracked-identifier, .os-participant-form__tracked-identifier) {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
:is(.participant-form__tracked-meta, .os-participant-form__tracked-meta) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  flex: 1;
}

/* ── Phase 6 Pattern Library Additions (R3-R14 + D1-D11) ─────────────────
   form_section, wizard_layout, hero_show, empty_state, loading_state.
   See plan §3 + DESIGN.md tokens for the visual grammar driving these. */

/* ── form_section ───────────────────────────────────────────────────── */
/* Section card — A5 forms spec: surface card, soft shadow, fixed padding.
   Sections stack with an 18px gap in the centered .form-page column. */
:is(.form-section, .os-form-section) {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 22px 24px 24px;
  margin-bottom: 18px;
}
/* The arena theme styles `form.form` (post/patch) with BT's `-mt-5 space-y-5`
   rhythm: a -1.25rem (-20px) top margin plus 20px between children. The
   NEGATIVE margin collapses with the .form-page's 2rem band gap (32 + -20 =
   12px), eating most of it — that's why forms looked like they lost the 2rem.
   The space-y also overrides our 18px section stack. We use form_section
   cards + the band's 2rem, not BT's form rhythm, so neutralize both. The
   selectors mirror the gem's so they tie on specificity and win on source
   order (patterns.css loads after the theme). */
form:is(.form, .os-form)[method="post" i], form:is(.form, .os-form)[method="patch" i] { margin-top: 0; }
/* space-y-5 sets BOTH margins on later children (a Rails hidden CSRF input is
   the first child, so the first section counts as "later"): margin-top 20px
   AND margin-bottom: calc(1.25rem * --tw-space-y-reverse) = 0, which zeroes
   our 18px section stack. Restore both — flush top (band owns the 2rem gap),
   18px between/after sections. */
form:is(.form, .os-form) > :is(.form-section, .os-form-section):not([hidden]) { margin-top: 0; margin-bottom: 18px; }
:is(.form-section__header, .os-form-section__header) {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Muted icon — a quiet marker that keeps the section stack calm (A5 forms
   spec). The heading carries the weight in ink; the icon is NOT accent. */
:is(.form-section__icon, .os-form-section__icon) {
  color: var(--muted);
}
/* Title-case heading — General Sans, the DS CardTitle treatment chosen over
   the uppercase SectionLabel because each header carries a description. */
:is(.form-section__title, .os-form-section__title) {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}
/* Description — muted, at the card's left edge (under the icon), not indented
   under the title. */
:is(.form-section__subtitle, .os-form-section__subtitle) {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--muted);
  margin: 6px 0 0;
}
/* Header→field gap. The card-title-with-description variant breathes at 32px
   between the title/description and the first field; tighter without one. */
:is(.form-section__body, .os-form-section__body) { margin-top: 20px; }
:is(.form-section, .os-form-section):has(:is(.form-section__subtitle, .os-form-section__subtitle)) :is(.form-section__body, .os-form-section__body) { margin-top: 32px; }
@media (max-width: 640px) {
  :is(.form-section, .os-form-section) { padding: 18px 16px 20px; }
}

/* ── wizard_layout ──────────────────────────────────────────────────── */
:is(.wizard-layout, .os-wizard-layout) {
  max-width: 860px;
  margin: 0 auto;
  padding: 28px 40px;
}
:is(.wizard-layout__header, .os-wizard-layout__header) { margin-bottom: 24px; }
:is(.wizard-layout__title, .os-wizard-layout__title) {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-2xl);
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0;
}
:is(.wizard-layout__subtitle, .os-wizard-layout__subtitle) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 4px 0 0;
}
:is(.wizard-layout__stepper, .os-wizard-layout__stepper) {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 20px;
  margin-bottom: 20px;
}
:is(.wizard-layout__step, .os-wizard-layout__step) {
  margin-bottom: 20px;
}
:is(.wizard-layout__actions, .os-wizard-layout__actions) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
:is(.wizard-layout__actions-right, .os-wizard-layout__actions-right) {
  display: flex;
  align-items: center;
  gap: 12px;
}
:is(.wizard-layout__cancel, .os-wizard-layout__cancel), :is(.wizard-layout__back, .os-wizard-layout__back), :is(.wizard-layout__next, .os-wizard-layout__next) {
  display: inline-flex;
  align-items: center;
  height: 44px;  /* WCAG AAA touch target */
  padding: 0 18px;
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: var(--radius-sm);
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
}
:is(.wizard-layout__cancel, .os-wizard-layout__cancel) {
  color: var(--text-muted);
}
:is(.wizard-layout__cancel, .os-wizard-layout__cancel):hover { color: var(--text-primary); }
:is(.wizard-layout__back, .os-wizard-layout__back) {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--border);
}
:is(.wizard-layout__back, .os-wizard-layout__back):hover { background: var(--surface-alt); }
:is(.wizard-layout__next, .os-wizard-layout__next) {
  color: white;
  background: var(--accent);
  border-color: var(--accent);
}
:is(.wizard-layout__next, .os-wizard-layout__next):hover { background: var(--accent-strong); border-color: var(--accent-strong); }
@media (max-width: 640px) {
  :is(.wizard-layout, .os-wizard-layout) { padding: 20px 16px; }
  :is(.wizard-layout__stepper, .os-wizard-layout__stepper) { padding: 14px; }
  /* Stack action bar vertically + full width on mobile. */
  :is(.wizard-layout__actions, .os-wizard-layout__actions) {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  :is(.wizard-layout__actions-right, .os-wizard-layout__actions-right) {
    flex-direction: column;
    align-items: stretch;
  }
  :is(.wizard-layout__cancel, .os-wizard-layout__cancel), :is(.wizard-layout__back, .os-wizard-layout__back), :is(.wizard-layout__next, .os-wizard-layout__next) {
    justify-content: center;
  }
}

/* ── hero_show ──────────────────────────────────────────────────────── */
/* Fills the diveos-content shell (1280 column + 48px gutters) so the
   header band can bleed full-width, like every other account page. */
:is(.hero-show, .os-hero-show) {
  display: block;
}
/* White header band — reuses .dv-page-band (full-bleed white + bleed math).
   This adds only the vertical rhythm: pull up to negate diveos-content's
   top padding (40px), breathe before the body. */
:is(.hero-show__band, .os-hero-show__band) {
  margin-top: calc(-1 * var(--shell-pad-top, 40px));
  margin-bottom: 2rem;
  padding-top: 26px;
  padding-bottom: 22px;
}
/* Page header band — form/settings/list header (reuses .dv-page-band +
   the .hero-show__breadcrumb/__crumb* classes for the crumb row). */
:is(.page-header, .os-page-header) { margin-top: calc(-1 * var(--shell-pad-top, 40px)); padding-top: var(--space-8); padding-bottom: var(--space-6); }
:is(.page-header, .os-page-header) :is(.hero-show__breadcrumb, .os-hero-show__breadcrumb) { margin-bottom: 16px; }
:is(.page-header__row, .os-page-header__row) {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}
:is(.page-header__actions, .os-page-header__actions) { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
@media (max-width: 640px) { :is(.page-header, .os-page-header) { margin-top: -56px; } }
/* Form column under a page_header — centered, 640px (A5 forms spec): a
   field-led single column of section cards. Top gap from the global
   band→body rule below. */
:is(.form-page, .os-form-page) { max-width: 640px; margin-inline: auto; }

/* Content-sized field rows — small numerics/codes group in a wrapping row at
   their natural width instead of stretching the column (A5 field-sizing). */
:is(.field-row, .os-field-row) { display: flex; flex-wrap: wrap; gap: 16px 20px; align-items: flex-start; }
:is(.field-w-116, .os-field-w-116) { width: 116px; }
:is(.field-w-168, .os-field-w-168) { width: 168px; }
:is(.field-w-116, .os-field-w-116) :where(.os-input, input[type], select), :is(.field-w-168, .os-field-w-168) :where(.os-input, input[type], select) { width: 100%; }
/* 50/50 split — equal columns for text PAIRS (names), not numerics. Field
   widths are contextual: compact for numbers/codes, split/full for text. */
:is(.field-split, .os-field-split) { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 20px; align-items: start; }
/* Stacked-field rhythm neutralized by the shared field-grid/field-split reset
   in the Field grid section above. */
/* Inline unit suffix (e.g. % on a commission rate) — sits in the input, not
   as a hint below. */
:is(.field-pct, .os-field-pct) .os-field__input { position: relative; }
:is(.field-pct, .os-field-pct) .os-field__input::after {
  content: "%"; position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  font-size: var(--text-sm); color: var(--muted); pointer-events: none;
}
:is(.field-pct, .os-field-pct) .os-input { padding-right: 26px; }

/* DS Trix toolbar — make the rich-text controls read as part of the system:
   hairline-bordered button groups on surface, muted glyphs, accent-tint
   active, DS-radius. Kept consistently visible (no opacity dimming). */
trix-toolbar { opacity: 1; }
trix-toolbar :is(.trix-button-row, .os-trix-button-row) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
trix-toolbar :is(.trix-button-group, .os-trix-button-group) {
  display: inline-flex;
  margin: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}
trix-toolbar :is(.trix-button, .os-trix-button) {
  width: 32px;
  height: 30px;
  margin: 0;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--hairline-2);
  border-radius: 0;
  background: var(--surface);
  color: var(--muted);
  font-size: 0;
}
trix-toolbar :is(.trix-button-group, .os-trix-button-group) :is(.trix-button, .os-trix-button):first-child { border-left: 0; }
trix-toolbar :is(.trix-button, .os-trix-button)::before { opacity: 0.65; }
trix-toolbar :is(.trix-button, .os-trix-button):not(:disabled):hover { background: var(--surface-alt); }
trix-toolbar :is(.trix-button, .os-trix-button):not(:disabled):hover::before { opacity: 0.9; }
trix-toolbar :is(.trix-button, .os-trix-button):is(.trix-active, .os-trix-active) {
  background: var(--accent-soft);
}
trix-toolbar :is(.trix-button, .os-trix-button):is(.trix-active, .os-trix-active)::before { opacity: 1; }
trix-toolbar :is(.trix-button, .os-trix-button):disabled::before { opacity: 0.25; }


:is(.hero-show__back-row, .os-hero-show__back-row) { margin-bottom: 18px; }
:is(.hero-show__back-link, .os-hero-show__back-link) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
}
:is(.hero-show__back-link, .os-hero-show__back-link):hover { text-decoration: underline; }
/* Name-led header inside the white band — title + status, meta line,
   actions. The band (.dv-page-band) owns the bottom hairline + bg. */
:is(.hero-show__header, .os-hero-show__header) {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}
:is(.hero-show__identity, .os-hero-show__identity) {
  flex: 1;
  min-width: 0;
}
:is(.hero-show__title-row, .os-hero-show__title-row) {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 8px;
}
:is(.hero-show__title, .os-hero-show__title) {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-3xl);
  line-height: 1.1;
  letter-spacing: -0.025em;
  color: var(--ink);
  margin: 0;
}
/* Meta line — middot-separated id + facts. Mirrors booking's
   .bk-contact-line so entity headers read identically. */
:is(.hero-show__meta, .os-hero-show__meta) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: var(--text-sm);
  color: var(--muted);
}
:is(.hero-show__meta-id, .os-hero-show__meta-id) {
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}
:is(.hero-show__meta-dot, .os-hero-show__meta-dot) { font-size: var(--text-xl); line-height: 0; color: var(--muted-2); }
:is(.hero-show__meta-item, .os-hero-show__meta-item) { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
:is(.hero-show__meta-item, .os-hero-show__meta-item) i, :is(.hero-show__meta-item, .os-hero-show__meta-item) svg { color: var(--muted-2); flex-shrink: 0; }
:is(.hero-show__actions, .os-hero-show__actions) {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
:is(.hero-show__action, .os-hero-show__action) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 14px;
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: var(--radius-sm);
  text-decoration: none;
  border: 1px solid transparent;
}
:is(.hero-show__action--primary, .os-hero-show__action--primary) {
  color: white;
  background: var(--accent);
  border-color: var(--accent);
}
:is(.hero-show__action--primary, .os-hero-show__action--primary):hover { background: var(--accent-strong); border-color: var(--accent-strong); }
:is(.hero-show__action--secondary, .os-hero-show__action--secondary) {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--border);
}
:is(.hero-show__action--secondary, .os-hero-show__action--secondary):hover { background: var(--surface-alt); }
:is(.hero-show__action--danger, .os-hero-show__action--danger) {
  color: var(--cancelled);
  background: var(--surface);
  border-color: var(--hairline);
}
:is(.hero-show__action--danger, .os-hero-show__action--danger):hover { background: var(--cancelled-bg); border-color: var(--cancelled-border); }
/* button_to wraps each action in a form; keep it inline so the flex row
   doesn't break. */
:is(.hero-show__action-form, .os-hero-show__action-form) { display: inline-flex; margin: 0; }

/* Breadcrumb row — replaces the back link on catalog-detail headers. */
:is(.hero-show__breadcrumb, .os-hero-show__breadcrumb) {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 22px;
  font-size: var(--text-sm);
}
:is(.hero-show__crumb, .os-hero-show__crumb) { color: var(--muted); text-decoration: none; }
:is(.hero-show__crumb, .os-hero-show__crumb):hover { color: var(--ink); }
:is(.hero-show__crumb--current, .os-hero-show__crumb--current) { color: var(--ink); font-weight: 600; }
:is(.hero-show__crumb-sep, .os-hero-show__crumb-sep) { display: inline-flex; color: var(--muted-2); }

/* Eyebrow pill — category/type marker above the title. */
:is(.hero-show__eyebrow, .os-hero-show__eyebrow) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-alt);
  border: 1px solid var(--hairline);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
:is(.hero-show__eyebrow, .os-hero-show__eyebrow) svg { color: var(--muted-2); }

/* Subtitle helper line under the title. */
:is(.hero-show__subtitle, .os-hero-show__subtitle) {
  margin: 8px 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Rail fact card — stacked label/value rows with hairline separators. */
:is(.hero-rail-card, .os-hero-rail-card) {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: 2px 20px;
}
:is(.hero-rail-stat, .os-hero-rail-stat) {
  padding: 16px 0;
  border-bottom: 1px solid var(--hairline-2);
}
:is(.hero-rail-stat, .os-hero-rail-stat):last-child { border-bottom: 0; }
:is(.hero-rail-stat__lbl, .os-hero-rail-stat__lbl) {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
:is(.hero-rail-stat__val, .os-hero-rail-stat__val) {
  margin-top: 6px;
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.1;
  color: var(--ink);
}
:is(.hero-rail-stat__unit, .os-hero-rail-stat__unit) { font-size: var(--text-sm); font-weight: 500; color: var(--muted); margin-left: 2px; }

/* Content card — body section with an icon + title head. */
:is(.hero-content-card, .os-hero-content-card) {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: 24px;
  margin-bottom: 20px;
}
:is(.hero-content-card__head, .os-hero-content-card__head) {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}
:is(.hero-content-card__title, .os-hero-content-card__title) {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}
:is(.hero-content-card__body, .os-hero-content-card__body) {
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--ink);
}
:is(.hero-show__body--one-col, .os-hero-show__body--one-col) :is(.hero-show__main, .os-hero-show__main) { width: 100%; }
:is(.hero-show__body--two-col, .os-hero-show__body--two-col) {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 20px;
}
:is(.hero-show__rail, .os-hero-show__rail) {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
@media (max-width: 1024px) {
  :is(.hero-show__body--two-col, .os-hero-show__body--two-col) {
    grid-template-columns: 1fr;
  }
}
/* Narrow PANE (container, not viewport — accounts for the 240px sidebar): the
   actions are flex-shrink:0, so beside a long title in a ~784px pane they crowd
   it. Wrap the actions under the identity block. The ≤640 viewport block below
   still owns the full mobile column treatment. */
@container (max-width: 760px) {
  :is(.hero-show__header, .os-hero-show__header) { flex-wrap: wrap; }
  :is(.hero-show__identity, .os-hero-show__identity) { flex: 1 1 100%; }
  :is(.hero-show__actions, .os-hero-show__actions) { flex-wrap: wrap; }
}
@media (max-width: 640px) {
  :is(.hero-show__band, .os-hero-show__band) { margin-top: -56px; }
  :is(.hero-show__header, .os-hero-show__header) {
    flex-direction: column;
    gap: 16px;
  }
  :is(.hero-show__actions, .os-hero-show__actions) {
    width: 100%;
    flex-direction: column;
    align-items: stretch;
  }
  :is(.hero-show__action, .os-hero-show__action) { justify-content: center; }
}

/* ── empty_state ────────────────────────────────────────────────────── */
:is(.empty-state, .os-empty-state) {
  text-align: center;
  padding: 40px 16px;
}
/* Composed onto a card. `.os-card` sets `padding: 0` because it expects a
   `__body` child to own the inset — so `class="os-card os-empty-state"` on ONE
   element loses this pattern's padding entirely: both are single-class
   selectors and card.css is bundled after patterns.css, so the zero wins.
   Callers reach for that combination constantly (an empty state IS the whole
   card when a list is empty), and the failure is silent — the text simply runs
   to the card's edge. Two classes out-specify one, so this restores the inset
   regardless of bundle order. Found in arena-ops at 30 callsites across 26
   files: every settings index, most guest-portal empty states, and both
   portal gate pages. */
:is(.empty-state, .os-empty-state):is(.card, .os-card) {
  padding: 40px 16px;
}
:is(.empty-state__icon, .os-empty-state__icon) {
  color: var(--text-muted);
  margin: 0 auto 12px;
  display: block;
}
:is(.empty-state__message, .os-empty-state__message) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
}
/* When a description is present, the message reads as an emphasized title
   above the muted supporting line. Single-message callers are unaffected. */
:is(.empty-state, .os-empty-state):has(:is(.empty-state__description, .os-empty-state__description)) :is(.empty-state__message, .os-empty-state__message) {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--ink);
}
:is(.empty-state__description, .os-empty-state__description) {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 4px 0 0;
}
:is(.empty-state__actions, .os-empty-state__actions) {
  margin-top: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
:is(.empty-state__action, .os-empty-state__action) {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 16px;
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: var(--radius-sm);
  text-decoration: none;
  border: 1px solid transparent;
}
:is(.empty-state__action--primary, .os-empty-state__action--primary) {
  color: var(--accent);
  background: transparent;
  /* Inline link form when single action (matches conforming surfaces);
     becomes proper button when paired with a secondary (handled by
     `.empty-state__actions:has(.empty-state__action--secondary)`). */
}
:is(.empty-state__action--primary, .os-empty-state__action--primary):hover { text-decoration: underline; }
:is(.empty-state__actions, .os-empty-state__actions):has(:is(.empty-state__action--secondary, .os-empty-state__action--secondary)) :is(.empty-state__action--primary, .os-empty-state__action--primary) {
  color: white;
  background: var(--accent);
  border-color: var(--accent);
}
:is(.empty-state__actions, .os-empty-state__actions):has(:is(.empty-state__action--secondary, .os-empty-state__action--secondary)) :is(.empty-state__action--primary, .os-empty-state__action--primary):hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  text-decoration: none;
}
:is(.empty-state__action--secondary, .os-empty-state__action--secondary) {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--border);
}
:is(.empty-state__action--secondary, .os-empty-state__action--secondary):hover { background: var(--surface-alt); }

/* ── loading_state ──────────────────────────────────────────────────── */
:is(.loading-state, .os-loading-state) {
  padding: 12px 0;
}
:is(.loading-state__row, .os-loading-state__row) {
  height: 48px;
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
  animation: loading-state-pulse 1.6s ease-in-out infinite;
}
@keyframes loading-state-pulse {
  0% { opacity: 0.6; }
  50% { opacity: 1; }
  100% { opacity: 0.6; }
}
@media (prefers-reduced-motion: reduce) {
  :is(.loading-state__row, .os-loading-state__row) { animation: none; opacity: 0.8; }
}

/* ── stepper ──────────────────────────────────────────────────────────────
   Vertical progress rail: a dot+connector spine with a label/meta body per
   step. States — --done (confirmed dot + tinted line), --active (accent dot
   with focus-style ring), --last (drops the trailing line). Distinct from the
   wizard-layout chrome (that is page layout); this is the reusable progress
   primitive. Promoted from arena-ops (HotelOS `hos-vstep`) 2026-07-25.
   Token-only. */
.os-stepper { display: flex; gap: 12px; align-items: flex-start; }
.os-stepper__rail { display: flex; flex-direction: column; align-items: center; flex: none; }
.os-stepper__dot {
  width: 26px;
  height: 26px;
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-alt);
  color: var(--muted-2);
}
.os-stepper__line { width: 2px; height: 20px; background: var(--hairline); }
.os-stepper__body { padding-top: 3px; padding-bottom: 8px; min-width: 0; }
.os-stepper__label { font-size: var(--text-sm); color: var(--muted); }
.os-stepper__meta { font-size: var(--text-xs); color: var(--muted-2); margin-top: 2px; }
.os-stepper__meta--mono { font-family: var(--font-mono); }
.os-stepper--done .os-stepper__dot  { background: var(--confirmed-bg); color: var(--confirmed); }
.os-stepper--done .os-stepper__line { background: var(--confirmed); opacity: 0.35; }
.os-stepper--done .os-stepper__label { color: var(--ink); }
.os-stepper--active .os-stepper__dot { background: var(--accent-soft); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.os-stepper--active .os-stepper__label { color: var(--ink); font-weight: 600; }
.os-stepper--active .os-stepper__meta { color: var(--accent); }
.os-stepper--last .os-stepper__body { padding-bottom: 0; }
.os-stepper--last .os-stepper__line { display: none; }

/* ── timeline ─────────────────────────────────────────────────────────────
   Numbered node/line/content timeline: a marker column (numbered circle +
   connector) beside a content block, one per node. --board recolours the
   marker as context (not a logged item); --last drops the trailing pad. An
   optional __link makes the content a focusable row. Promoted from dive-os
   (account `dv-timeline`) 2026-07-25. Token-only. */
.os-timeline { list-style: none; margin: 0; padding: 0 16px; }
.os-timeline__node { display: flex; gap: 12px; align-items: flex-start; min-height: 48px; }
.os-timeline__marker-col { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; width: 24px; padding-top: 2px; }
.os-timeline__number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1.5px solid var(--accent-border);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  flex-shrink: 0;
}
.os-timeline__number--board { background: var(--surface-alt); border-color: var(--hairline-strong); color: var(--muted); }
.os-timeline__line { width: 1.5px; flex: 1; min-height: 16px; background: var(--hairline); margin-top: 4px; }
.os-timeline__content { flex: 1; min-width: 0; padding-bottom: 20px; }
.os-timeline__node--last .os-timeline__content { padding-bottom: 8px; }
.os-timeline__link { display: flex; flex-direction: column; gap: 2px; text-decoration: none; color: inherit; }
.os-timeline__link:hover .os-timeline__title { color: var(--accent); }
.os-timeline__link:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; border-radius: var(--radius-sm); }
.os-timeline__title { font-size: var(--text-sm); font-weight: 500; color: var(--ink); line-height: 1.4; transition: color 0.12s; }
.os-timeline__meta { font-size: var(--text-xs); color: var(--muted); }


/* ===== os_ds/responsive_tables.css ===== */
/* Mobile Responsive Tables */

/* Wrapper for horizontal scroll on mobile */
:is(.table-responsive-wrapper, .os-table-responsive-wrapper) {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* Base table improvements */
:is(.table, .os-table-responsive) {
  width: 100%;
  min-width: 100%;
}

/* Striped rows */
:is(.table-striped, .os-table-striped) tbody tr:nth-child(odd) {
  background-color: #f9fafb;
}

/* Row hover effect */
:is(.table, .os-table-responsive) tbody tr:hover {
  background-color: #f3f4f6;
  transition: background-color 0.15s ease-in-out;
}

/* Mobile: Stack table for better readability on small screens */
@media (max-width: 768px) {

  /* Hide table headers on mobile */
  :is(.table, .os-table-responsive) thead {
    display: none;
  }

  /* Make each row a card */
  :is(.table, .os-table-responsive) tbody tr {
    display: block;
    margin-bottom: 1rem;
    background-color: white;
  }

  /* Stack cells vertically */
  :is(.table, .os-table-responsive) tbody td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid #f3f4f6;
    text-align: right;
  }

  :is(.table, .os-table-responsive) tbody td:last-child {
    border-bottom: none;
  }

  /* Add labels before each cell using data attributes */
  :is(.table, .os-table-responsive) tbody td::before {
    content: attr(data-label);
    font-weight: 600;
    text-align: left;
    flex: 1;
    color: #374151;
    padding-left: 1.25rem;
  }

  /* Checkbox column - keep it compact */
  :is(.table, .os-table-responsive) tbody td:first-child {
    padding: 0.5rem 1rem;
    justify-content: center;
  }

  :is(.table, .os-table-responsive) tbody td:first-child::before {
    content: '';
  }

  /* Action buttons column */
  :is(.table, .os-table-responsive) tbody td:is(.buttons, .os-buttons) {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: flex-end;
    padding: 1rem;
    background-color: #f9fafb;
  }

  :is(.table, .os-table-responsive) tbody td:is(.buttons, .os-buttons)::before {
    content: 'Actions';
    width: 100%;
    text-align: center;
    margin-bottom: 0.5rem;
  }

  /* Make buttons full width on mobile */
  :is(.table, .os-table-responsive) tbody td:is(.buttons, .os-buttons) :is(.button-secondary, .os-button-secondary), :is(.table, .os-table-responsive) tbody td:is(.buttons, .os-buttons) :is(.button-primary, .os-button-primary) {
    flex: 1;
    min-width: 120px;
  }

}

/* Tablet and Large screens: Horizontal scroll with sticky first column */
/* Enable horizontal scrolling for tablet (md), large (lg), extra-large (xl), and 2xl screens */
@media (min-width: 769px) {

  /* Enable horizontal scrolling for the table wrapper */
  :is(.table-wrapper, .os-table-wrapper), :is(.box, .os-box)>div:has(> table), [data-controller*="action-model-select"]>div>div>div:has(> table), div:has(> table:is(.table, .os-table-responsive)) {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    width: 100%;
  }

  :is(.table, .os-table-responsive) {
    min-width: 1000px;
    width: max-content;
  }

  /* Make first column (usually name/title) sticky */
  :is(.table, .os-table-responsive) thead th:nth-child(2), :is(.table, .os-table-responsive) tbody td:nth-child(2) {
    position: sticky;
    left: 0;
    background-color: white;
    z-index: 10;
  }
}

/* Improve button spacing on all screen sizes */
:is(.table, .os-table-responsive) td:is(.buttons, .os-buttons) {
  white-space: nowrap;
}

:is(.table, .os-table-responsive) td:is(.buttons, .os-buttons)>* {
  margin-right: 0.5rem;
}

:is(.table, .os-table-responsive) td:is(.buttons, .os-buttons)>*:last-child {
  margin-right: 0;
}

/* Ensure text doesn't overflow */
:is(.table, .os-table-responsive) td {
  word-wrap: break-word;
  overflow-wrap: break-word;
}

/* Better spacing for small screens */
@media (max-width: 640px) {
  :is(.table, .os-table-responsive) tbody td {
    font-size: 0.875rem;
  }

  :is(.table, .os-table-responsive) tbody td::before {
    font-size: 0.875rem;
  }
}

/* ── .box-table parallel responsive layer ─────────────────────────────────
   The box.table cluster was migrated off `class="table"` onto the .box-table
   chrome (header_band.css). The `.table`-scoped rules above (mobile card-stack
   via data-label + desktop horizontal scroll) would otherwise be lost on those
   ~14 views. Mirror them for the .box-table WRAPPER (descendant selectors),
   token-driven. data-label attributes on the cluster rows still drive the
   mobile labels. */
@media (max-width: 768px) {
  :is(.box-table, .os-box-table) thead { display: none; }
  :is(.box-table, .os-box-table) tbody tr { display: block; margin-bottom: 1rem; background: var(--surface); }
  :is(.box-table, .os-box-table) tbody td {
    display: flex; justify-content: space-between; align-items: center;
    padding: 0.75rem 1rem; border-bottom: 1px solid var(--hairline-2); text-align: right;
  }
  :is(.box-table, .os-box-table) tbody td:last-child { border-bottom: none; }
  :is(.box-table, .os-box-table) tbody td::before {
    content: attr(data-label); font-weight: 600; text-align: left; flex: 1;
    color: var(--text-secondary); padding-left: 1.25rem;
  }
  :is(.box-table, .os-box-table) tbody td:first-child { padding: 0.5rem 1rem; justify-content: center; }
  :is(.box-table, .os-box-table) tbody td:first-child::before { content: ''; }
  :is(.box-table, .os-box-table) tbody td:is(.buttons, .os-buttons) {
    display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: flex-end;
    padding: 1rem; background: var(--canvas);
  }
  :is(.box-table, .os-box-table) tbody td:is(.buttons, .os-buttons)::before { content: 'Actions'; width: 100%; text-align: center; margin-bottom: 0.5rem; }
}
@media (max-width: 640px) {
  :is(.box-table, .os-box-table) tbody td, :is(.box-table, .os-box-table) tbody td::before { font-size: 0.875rem; }
}
/* Desktop: let wide box-tables scroll horizontally (the wrapper already has
   overflow-y; add x). No forced 1000px min-width — narrow tables size naturally. */
@media (min-width: 769px) {
  :is(.box-table, .os-box-table) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* ===== os_ds/components/accordion.css ===== */
/* ── .dv-accordion ──────────────────────────────────────────────────────────
   Disclosure accordion: a stack of collapsible panels. Each item is a
   header button that toggles its body open/closed. Tokens only — no raw
   palette, no hex. Built for the trip Return checklist (boxes → gear rows)
   but general enough for any grouped disclosure list.

   Anatomy:
     .dv-accordion            wrapper (stacks items with gap)
       .dv-accordion__item    one panel  (+ .is-open)
         .dv-accordion__head  the toggle button (full-width, ≥44px tall)
           .dv-accordion__icon   leading glyph slot
           .dv-accordion__title  heading text
           .dv-accordion__meta   right-aligned mono progress/count
           .dv-accordion__chev   trailing chevron (rotates when open)
         .dv-accordion__body  disclosed content (hidden unless .is-open)
------------------------------------------------------------------------------- */

.os-accordion {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.os-accordion__item {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs, 0 1px 2px rgba(17, 24, 39, .04));
  overflow: hidden;
}

.os-accordion__head {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 52px;
  padding: 14px 18px;
  background: none;
  border: 0;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--ink);
  transition: background .12s;
}
.os-accordion__head:hover { background: var(--surface-alt); }
.os-accordion__head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.os-accordion__item.is-open .os-accordion__head {
  border-bottom: 1px solid var(--hairline);
}

.os-accordion__icon {
  display: inline-flex;
  color: var(--muted-2);
  flex-shrink: 0;
}

.os-accordion__title {
  font-family: var(--font-display, var(--font-sans));
  font-weight: 600;
  font-size: var(--text-base, 15px);
  color: var(--ink);
}

.os-accordion__meta {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs, 12.5px);
  color: var(--muted);
  white-space: nowrap;
}

.os-accordion__chev {
  display: inline-flex;
  color: var(--muted-2);
  flex-shrink: 0;
  transition: transform .18s ease;
}
.os-accordion__item.is-open .os-accordion__chev { transform: rotate(180deg); }

.os-accordion__body { padding: 4px 18px 16px; }
.os-accordion__item:not(.is-open) .os-accordion__body { display: none; }

/* Right-aligned actions strip inside a body (e.g. "Return all"). */
.os-accordion__foot {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
}


/* ===== os_ds/components/appbar.css ===== */
/* ═══════════════════════════════════════════════════════════════════════
   os-appbar — the sticky MOBILE TOP BAR.

   The bar a phone-shaped surface (crew portal, rep portal, kiosk) pins to the
   top of its scroll container. Content scrolls UNDER it; the bar stays put and
   carries the screen's identity (a back/menu control, the title, one optional
   text action).

   NOT `.os-page-head` / `.os-page-band`. Those are the DESKTOP page-header
   family that happens to pin on phones (see docs/mobile-header.md): they own an
   <h1>, breadcrumbs, scope tabs, filter rows and a 40px action set, and they sit
   INSIDE the page column. `.os-appbar` is app chrome — it spans the viewport,
   owns the status-bar inset, and has no notion of tabs or breadcrumbs. A
   surface uses one or the other, never both stacked.

   Anatomy:
     .os-appbar                — the sticky bar (status-bar inset + row)
       .os-appbar__row         — the 56px control row
         .os-appbar__lead      — 36px leading control (back / menu / close)
         .os-appbar__title     — flexible title
         .os-appbar__action    — optional trailing text action

   Modifiers:
     .os-appbar--tint          — accent-soft ground, accent-toned separation

   THE SEPARATION RULE — read before touching the shadow.
   The 1px hairline is what separates the bar from the content beneath it. The
   `box-shadow` is a deliberately faint, tightly-negative-spread wash that only
   SOFTENS the edge below that hairline so scrolled content does not appear to
   touch it. It is not a drop shadow and must not be grown into one: if the bar
   starts to read as a floating card, the shadow is wrong, not the hairline.
   Raising the alpha or the blur here is the failure mode this note exists to
   prevent — the hairline does the separating.

   Safe area: the status-bar inset is `env(safe-area-inset-top)`, which is 0 on
   a non-notched device and on desktop, so the bar collapses to its 56px row
   with no media query. It is testable headlessly via
   `Emulation.setSafeAreaInsetsOverride` (docs/mobile-primitives.md §3) — do not
   fake it with a fixed 48px.

   Sticky, not fixed: `position: fixed` is unreliable while the iOS keyboard is
   open (docs/mobile-primitives.md §4) and would need JS to stay correct.
   `sticky` inside the scroll container needs none, so the bar is CSS-only.
   ═══════════════════════════════════════════════════════════════════════ */

.os-appbar {
  /* Local knobs so --tint (and a consumer with a different ground) can restyle
     the bar by re-pointing four values instead of redeclaring the rules. */
  --os-appbar-bg:       color-mix(in srgb, var(--surface) 92%, transparent);
  --os-appbar-hairline: var(--hairline);
  /* 13%, not 30%: the near-black ink wash at 30% read as if the plain bar floated
     (HotelOS Mobile). This matches the subtlety of the --tint variant's accent
     edge (L153); the box-shadow geometry is unchanged. */
  --os-appbar-edge:     color-mix(in srgb, var(--ink) 13%, transparent);
  --os-appbar-lead-bg:  var(--surface-alt);
  --os-appbar-ink:      var(--text-primary);

  position: sticky;
  top: 0;
  z-index: var(--z-tooltip);

  /* The status-bar inset rides on the bar's own padding, so the ROW keeps its
     honest 56px and the translucent ground extends up behind the clock. */
  padding-top: env(safe-area-inset-top);

  background: var(--os-appbar-bg);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);

  border-bottom: 1px solid var(--os-appbar-hairline);
  /* Edge softener — see THE SEPARATION RULE above. The -10px spread keeps the
     wash tucked under the hairline instead of spilling as an elevation. */
  box-shadow: 0 6px 14px -10px var(--os-appbar-edge);

  color: var(--os-appbar-ink);
}

.os-appbar__row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: 56px;
  padding-inline: var(--space-3);
}

/* Leading control — back, menu or close. Square, one control rung (36px), and
   given its own ground so it stays a discrete tap target rather than dissolving
   into the bar. */
.os-appbar__lead {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h-base);
  height: var(--control-h-base);
  border: 0;
  border-radius: var(--radius-md);
  background: var(--os-appbar-lead-bg);
  color: inherit;
  cursor: pointer;
  padding: 0;
  transition: background var(--t-fast) var(--ease-standard);
}
/* Touch-target floor: the visible control stays 36px, but the tappable area
   extends to the 44px pointer-target minimum (WCAG 2.5.5 / the DESIGN-MOBILE
   hit-area rule — the floor is the HIT AREA, not the ink) via a centred,
   invisible hit-slug. The ink is byte-identical on every consumer, so this is
   non-visual for DiveOS and HotelOS alike; only the clickable region grows. */
.os-appbar__lead::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.os-appbar__lead :is(.lucide, .os-lucide) { display: block; }
.os-appbar__lead:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* Title — takes the slack and truncates rather than wrapping the bar taller. */
.os-appbar__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.2;
  color: inherit;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Trailing action — TEXT, not a filled button. The bar carries at most one, and
   a solid button here competes with the title for the eye. */
.os-appbar__action {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: var(--control-h-base);
  padding-inline: var(--space-2);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  font-size: var(--text-sm);
  font-weight: 600;
  color: inherit;
  cursor: pointer;
}
.os-appbar__action:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* ── --tint ────────────────────────────────────────────────────────────────
   Accent ground for a bar that belongs to an accent-owned flow (a wizard leg,
   an active run). The hairline and the edge wash go accent-toned too, or a
   neutral separation over a tinted ground reads as a seam.

   The leading control fills a SHADE STRONGER than the ground (16% vs the
   ground's 8%): a control tinted to the same value as what it sits on is
   invisible, which is the bug this line prevents. */
.os-appbar--tint {
  --os-appbar-bg:       var(--accent-soft);
  --os-appbar-hairline: var(--accent-border);
  --os-appbar-edge:     color-mix(in srgb, var(--accent) 30%, transparent);
  --os-appbar-lead-bg:  color-mix(in srgb, var(--accent) 16%, transparent);
}

/* Light ground → ink controls; a coloured ground → white controls. `.os-appbar`
   inherits `--text-primary` by default, so only the inverted case is declared.
   A consumer painting the bar a solid brand colour opts in here. */
.os-appbar--onaccent {
  --os-appbar-bg:       var(--accent);
  --os-appbar-hairline: var(--accent-strong);
  --os-appbar-ink:      var(--on-accent);
  --os-appbar-lead-bg:  color-mix(in srgb, var(--on-accent) 18%, transparent);
}


/* ===== os_ds/components/assist-row.css ===== */
/* os-assist-row — assisted-input row: "Scan passport to autofill →".
 * Source: `.rd-rowline` + `.rd-itile--accent` (arena-ops reservation_details.css ~429–456)
 * and arrival.html. The canonical affordance for any AI/OCR-assisted input (passport
 * reader, receipt reader): accent tile + what it does + why + trailing action.
 * Renders as <a> or <button> — native button chrome is reset below.
 */

.os-assist-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-5);
  border: 0;
  border-top: 1px solid var(--hairline-2);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--t-fast);
}

.os-assist-row:hover,
.os-assist-row:focus-visible {
  background: var(--accent-softer);
}

.os-assist-row__icon {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: var(--radius-md);
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
}

.os-assist-row__body {
  min-width: 0;
}

.os-assist-row__title {
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 600;
  color: var(--ink);
}

.os-assist-row__sub {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted);
  margin-top: 1px;
}

.os-assist-row__action {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  white-space: nowrap;
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  color: var(--accent);
}


/* ===== os_ds/components/avatar.css ===== */
/* ── dv-avatar ─────────────────────────────────────────────────────────────
   Initials disc for guests + staff where no photo exists. Formalises the
   inline pattern previously shown only in the showcase (accent-soft fill,
   accent ink). Sizes scale with context: list row (sm) → hero header (lg).
   Token-only. Blessed 2026-07-05 (Trip Roster build).
   ────────────────────────────────────────────────────────────────────────── */
.os-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: 0.01em;
  user-select: none;
}
.os-avatar--xs { width: 28px; height: 28px; font-size: var(--text-2xs); }
.os-avatar--sm { width: 32px; height: 32px; font-size: var(--text-xs); }
.os-avatar--lg { width: 56px; height: 56px; font-size: var(--text-xl); }

/* Neutral fallback (no initials) */
.os-avatar--muted { background: var(--gray-100); color: var(--muted); }


/* ===== os_ds/components/bodyframe.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   bodyframe.css — `.os-bodyframe`, the frame around a page's BODY content.

   WHY THIS EXISTS. On a page with a tinted hero or a header band, the content
   beneath it was floating loose on the canvas: the band ended and the fields
   simply began, with nothing saying where the readable page was. Consumers kept
   solving it by making every section a card, which produces a column of boxes
   and then a card-inside-a-card the moment a section needs its own surface.

   The frame is the answer: ONE surface holding the body, with the chrome
   OUTSIDE it. Hero, page header, tab/party switchers and a pinned action bar
   all stay out; what goes in is the thing the page is actually for.

   IT IS NOT `.os-card`. A card is a piece of content with its own head and body
   slots at `--radius-lg`; this is page-level geometry at `--radius-xl` that
   holds other components, and it deliberately has no head of its own — the page
   header above it already did that job.

   SECTIONS INSIDE IT MUST FLATTEN. A filled, bordered section inside a filled,
   bordered frame is the card-in-a-card this exists to stop, so the frame takes
   care of it: a nested `.os-form-section` loses its surface, border and shadow
   and becomes a hairline-divided block. One filled box.

     div.os-bodyframe
       section.os-form-section   (flattened automatically)
       section.os-form-section
───────────────────────────────────────────────────────────────────── */

.os-bodyframe {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-sm);
  overflow: hidden;   /* clips a nested section's square corners to the radius */
}

/* Flatten the sections the frame holds — see the note above. `background: none`
   rather than `--surface` so the frame's own ground shows through unchanged. */
.os-bodyframe > .os-form-section {
  background: none;
  border-inline: 0;
  border-bottom: 0;
  border-radius: 0;
  box-shadow: none;
  margin-bottom: 0;
}

/* The divider is the boundary between sections, so the first one has none. */
.os-bodyframe > .os-form-section + .os-form-section {
  border-top: 1px solid var(--hairline);
}


/* ===== os_ds/components/bottomnav.css ===== */
/* ───────────────────────────────────────────────────────────────────
   bottomnav — fixed mobile bottom tab bar (2-5 tabs).
   The mobile counterpart to .os-sidebar. Frosted ground, a home-indicator
   safe-area inset, and an accent-soft pill behind the ACTIVE tab's icon.
   Promoted from HotelOS Mobile (.hom-tabbar); token-only.

   Posture: absolute by default, so the bar pins inside a 100dvh phone shell
   (a scroll container) rather than the viewport. A consumer whose shell IS
   the viewport can set position: fixed on .os-bottomnav.
   ─────────────────────────────────────────────────────────────────── */

.os-bottomnav {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* 84px = ~56px tab content + a 28px home-indicator inset. */
  height: 84px;
  padding-top: 2px;
  padding-bottom: env(safe-area-inset-bottom, 28px);
  display: flex;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
  border-top: 1px solid var(--hairline);
  z-index: var(--z-panel);
}

.os-bottomnav__tab {
  flex: 1;
  min-height: 48px;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  text-decoration: none;
  color: var(--muted);
  font-size: 10.5px;
  font-weight: 600;
}

.os-bottomnav__tab--active { color: var(--accent); font-weight: 650; }
.os-bottomnav__tab--disabled { opacity: 0.38; pointer-events: none; }

/* The active pill sits BEHIND the icon only. top:2px + 30px height keeps its
   bottom at ~32px, clear of the label baseline below — a taller pill or one
   anchored to the tab's vertical center overlaps the label (the overlap bug
   from the HotelOS build). Do not raise the height or re-anchor it. */
.os-bottomnav__tab--active::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 50%;
  transform: translateX(-50%);
  width: 52px;
  height: 30px;
  border-radius: 9999px;
  background: var(--accent-soft);
}


/* ===== os_ds/components/breadcrumb.css ===== */
/* ── Breadcrumb (.dv-breadcrumb) ───────────────────────────────────────────
   The canonical in-body breadcrumb for Settings-accessed pages
   (Settings › Section › Record). Chevron-separated crumbs; the last crumb is
   the current (non-link) page. Standalone component — margin-free so the host
   (hero_show / page_header) owns vertical spacing. Promoted from the legacy
   .hero-show__breadcrumb / .hero-show__crumb* rules in patterns.css. */

.os-breadcrumb {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-sm);
}
.os-breadcrumb__crumb { color: var(--muted); text-decoration: none; }
.os-breadcrumb__crumb:hover { color: var(--ink); }
.os-breadcrumb__crumb--current { color: var(--ink); font-weight: 600; }
.os-breadcrumb__sep { display: inline-flex; color: var(--muted-2); }

/* Phones: the full trail is noise — the last crumb is the current page, which
   just repeats the <h1> beneath it. Collapse to a single `‹ Parent` back-link
   (the immediate parent, tagged `--parent` by the partial). Chevron is a CSS
   corner so no extra markup. Desktop keeps the full trail. */
@media (max-width: 640px) {
  .os-breadcrumb__sep,
  .os-breadcrumb__crumb:not(.os-breadcrumb__crumb--parent) { display: none; }
  .os-breadcrumb__crumb--parent {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--muted);
    font-weight: 500;
  }
  .os-breadcrumb__crumb--parent::before {
    content: "";
    width: 7px;
    height: 7px;
    border-left: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg);
    flex-shrink: 0;
  }
}


/* ===== os_ds/components/bulkbar.css ===== */
/* ═══════════════════════════════════════════════════════════════════════
   dv-bulkbar — sticky multi-select action bar.

   Appears when one or more rows in a list/table are ticked; carries the
   selection count + bulk actions (assign, mark, clear). Accent-filled so it
   reads as an active operating mode, not a passive toolbar.

   Anatomy:
     .dv-bulkbar
       .dv-bulkbar__count     — "N selected"
       .dv-bulkbar__action    — a bulk action button (repeatable)
       .dv-bulkbar__clear     — dismiss selection (pushed to the right)
   ═══════════════════════════════════════════════════════════════════════ */
.os-bulkbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  margin: 12px 0;
  border-radius: var(--radius-lg);
  background: var(--accent);
  color: var(--surface);
  box-shadow: var(--shadow-md);
}
.os-bulkbar__count {
  font-weight: 600;
  font-size: var(--text-xs);
  padding-right: 4px;
}
.os-bulkbar__action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  min-height: 38px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(255, 255, 255, .28);
  background: rgba(255, 255, 255, .12);
  color: var(--surface);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
}
.os-bulkbar__action:hover { background: rgba(255, 255, 255, .22); }
.os-bulkbar__action :is(.lucide, .os-lucide) { opacity: .9; }
.os-bulkbar__clear {
  margin-left: auto;
  padding: 6px 10px;
  background: transparent;
  border: 0;
  color: rgba(255, 255, 255, .85);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
}
.os-bulkbar__clear:hover { color: var(--surface); text-decoration: underline; }


/* ===== os_ds/components/buttons.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   Shared component layer — button primitives.

   Consolidates the primary-action button that was previously repeated as an
   identical inline utility string across seven index/header views:

     inline-flex items-center gap-1.5 h-9 px-3.5 text-sm font-medium
     text-white bg-accent rounded-btn hover:bg-accent-strong
     transition-colors no-underline

   (New Trip · Trip · Add diver · Add · Add vessel · New booking).

   PARITY NOTE — these values are matched BYTE-FOR-BYTE to the Tailwind output
   of the inline string above, NOT to WL's `.dv-btn` spec. WL's `.dv-btn` uses
   16px horizontal padding + 13px font; DM's shipped buttons use px-3.5 (14px) +
   text-sm (14px). To guarantee ZERO visual change we keep DM's exact metrics
   under the WL class name. If/when the two are unified, change here once.

   Tailwind → CSS mapping used:
     inline-flex      → display:inline-flex
     items-center     → align-items:center
     gap-1.5          → gap:0.375rem (6px)
     h-9              → height:2.25rem (36px)
     px-3.5           → padding-left/right:0.875rem (14px)
     text-sm          → font-size:0.875rem; line-height:1.25rem
     font-medium      → font-weight:500
     rounded-btn      → border-radius:6px (var(--radius-btn) / borderRadius.btn)
     transition-colors→ transition: color/bg/border 150ms cubic-bezier(.4,0,.2,1)
     no-underline     → text-decoration-line:none

   Token-driven; consumes the semantic aliases from tokens.css. @imported from
   application.css right after components/header_band.css.
   ───────────────────────────────────────────────────────────────────── */

/* ── Base button ──────────────────────────────────────────────────────
   Layout + typography shared by every variant. Color/background come from
   the modifier so the base alone is never used directly. */
.os-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center; /* centre when stretched full-width */
  gap: 0.375rem;
  /* Sized by the CONTROL TRIO, not literals. At the default density these
     resolve to exactly the shipped 36px / 14px / 14px, so nothing moves;
     inside an .os-density--* scope the button follows the surface. Every
     size modifier below re-declares the trio on itself, which pins it
     against any enclosing scope (see tokens/density.css). */
  height: var(--control-h);
  padding-left: var(--control-pad-x);
  padding-right: var(--control-pad-x);
  font-size: var(--control-fs);
  line-height: 1.25rem;
  font-weight: 500;
  border-radius: 6px;
  /* Own the border instead of inheriting the UA's `2px outset` on <button>.
     Variants that want a border set it themselves, each a full `border:
     1px solid`: --secondary, --ghost, --danger, --confirmed. That list is
     enumerated from the source, not remembered — the first draft of this
     comment named a `--outline` variant that has never existed. No rule
     gem-wide sets border-color or border-width alone on .dv-btn, so
     zeroing the shorthand here cannot strand a half-declared border.

     NOTE: this comment deliberately contains no curly braces. An earlier draft
     spelled a CSS rule out inline, and the stray closing brace broke three
     separate block parsers in one session — every one of them stopped reading
     the rule early and reported the wrong declarations. Same hazard class as a
     star followed by a slash inside comment prose, which once closed a comment
     early, destroyed a :root block and killed 21 tokens app-wide.
     Describe CSS in comments; do not spell it.

     This is NOT redundant with a reset. DiveOS gets a zero button border-width
     free from Tailwind Preflight, so this component looked correct there for its
     whole life. arena-ops has no Tailwind, and every one of its 61
     `<button class="dv-btn">` rendered with a black outset border — a defect
     visible on the guest portal's "Save my details" and invisible to the whole
     test suite, because nothing computes style on real DOM.

     Same failure shape as `turbo-frame[hidden]` (v0.22.1): a component that
     silently depends on the consumer's reset is a component that behaves
     differently per consumer. The base must be self-sufficient. */
  border: 0;
  /* Same reasoning as the border, and found the same way — by computing style
     on a real <button> rather than reading the CSS. A bare `.dv-btn` in
     arena-ops rendered `rgb(239, 239, 239)` on black: the UA's ButtonFace
     fill. Preflight sets a transparent background-color on button; arena's
     base.css has no `button` rule at all.

     An older comment here claimed the base is "never used directly because the
     background comes from the modifier". That is not true of real markup —
     five live arena buttons carry `.dv-btn` with no colour variant, including
     one half of a segmented control in front_office/new_reservation.html.erb
     whose other half has `--secondary`, so the two tabs render differently.

     A base that is only correct when a modifier is also present should either
     declare its own neutral ground or not be usable alone. This declares it. */
  background-color: transparent;
  color: inherit;
  text-decoration-line: none;
  cursor: pointer;
  white-space: nowrap;
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, box-shadow, transform;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
/* Keyboard focus ring — focus-visible only, so mouse clicks don't ring. */
.os-btn:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
/* Pressed feedback — shared across every variant. */
.os-btn:active {
  transform: translateY(1px);
}
.os-btn:disabled, .os-btn[disabled] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}
/* aria-disabled twin — same dimmed look, but the button stays focusable so
   assistive tech can reach it and hear why it's inert (turnaround bulk bars
   disable until a selection exists; the Stimulus guard blocks the submit). */
.os-btn[aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
}

/* ── Primary (accent fill) ────────────────────────────────────────────
   Replaces: bg-accent text-white hover:bg-accent-strong. */
/* An <a class="dv-btn"> must not inherit link-hover styling (underline +
   link color) from global `a:hover` rules — those out-specify the variant's
   base `color` on hover. Lock decoration off on every interactive state, and
   re-assert each variant's text color on :hover below. */
.os-btn:hover, .os-btn:focus, .os-btn:focus-visible, .os-btn:active { text-decoration-line: none; }

.os-btn--primary {
  color: #fff;
  background-color: var(--accent);
}
.os-btn--primary:hover {
  color: #fff;
  background-color: var(--accent-strong);
}

/* ── Large size ───────────────────────────────────────────────────────
   Taller, heavier weight. For prominent full-width CTAs (e.g. the sidebar
   "New Dive Plan"). Pair with a layout class for width/block behaviour. */
.os-btn--lg {
  --control-h:     var(--control-h-lg);      /* 42px */
  --control-pad-x: var(--control-pad-x-lg);  /* 16px */
  --control-fs:    var(--control-fs-lg);     /* 14px */
  font-weight: 600;
}

/* ── Field-height size ────────────────────────────────────────────────
   46px tall — the canonical form-control height (matches .dv-field /
   .diveos-input on a filter row). Use for submit/clear buttons that sit
   inline with selects + date pickers so every control aligns on one
   baseline. Height-only; weight + 14px font stay the base size.

   Since v1.6.0 this is the PER-BUTTON escape hatch. If a whole form region
   should sit on the field baseline, wrap it in `.os-density--field` once
   instead of adding this modifier to each control. */
.os-btn--field {
  --control-h: var(--control-h-field);
}

/* ── Touch size — 48pt minimum target ─────────────────────────────────
   Practical UI Ch.7 + Fitts's Law: primary / touch-facing actions should be
   at least 48pt tall so they're fast and easy to hit. The base .dv-btn (36px)
   is right for dense DESKTOP admin rows; reach for .dv-btn--touch on the
   PRIMARY CTA of public + mobile flows (booking, waiver, checkout). */
.os-btn--touch {
  --control-h:     var(--control-h-touch);      /* 48px */
  --control-pad-x: var(--control-pad-x-touch);  /* 20px */
  font-weight: 600;
}

/* ── Secondary (surface + hairline outline) ───────────────────────────
   Outline button: surface fill, hairline border, ink text, hover warms to
   canvas. WL `.dv-btn--secondary`. Used for "View public" / "Copy" on the
   booking-detail page.

   NOTE: the booking-detail "View public" button previously carried its own
   inline string (text-[13px], rounded-md). It is now unified to the canonical
   `.dv-btn` metrics (14px font, 6px radius). The radius is identical (rounded-md
   and rounded-btn both compute to 6px); only the font shifts 13px → 14px. This
   1px change is accepted in favour of a single shared button size (consistency
   over preserving a one-off). */
.os-btn--secondary {
  background-color: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--ink);
}
.os-btn--secondary:hover {
  color: var(--ink);
  background-color: var(--canvas);
}

/* ── Ghost (chromeless) ───────────────────────────────────────────────
   No fill, no border — an accent-text action that reads as a button but sits
   flat in a tinted container (callout trailing action, wizard "Back"). Hover
   tints with the accent-soft wash. Was referenced (callout, wizard footer)
   before it existed, so those rendered as bare links — now a real variant. */
.os-btn--ghost {
  background-color: transparent;
  border: 1px solid transparent;
  color: var(--accent);
}
.os-btn--ghost:hover {
  color: var(--accent-strong);
  background-color: var(--accent-soft);
}

/* ── View toggle (segmented control) ──────────────────────────────────
   A small inset segmented control for switching a surface between views
   (Bookings: Kanban / Table / Calendar). Options are <a> links, so they reset
   BT's global a:hover underline. Active = raised surface chip + accent text.
   Replaces the per-page utility-soup version (raw text-[12px], hover underline,
   no shared identity). */
.os-view-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--canvas);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
}
.os-view-toggle__opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Density via vertical padding (no declared height — see
     .os-segmented__option). (36 − 24) / 2 = 6px at base density. */
  padding: max(6px, calc((var(--control-h) - 24px) / 2)) 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease;
}
.os-view-toggle__opt:hover { color: var(--ink); background: var(--surface-alt); text-decoration: none; }
.os-view-toggle__opt--active, .os-view-toggle__opt--active:hover {
  background: var(--surface);
  color: var(--accent);
  box-shadow: var(--shadow-sm);
}

/* ── Danger (outlined cancelled) ──────────────────────────────────────
   Transparent fill, cancelled-tinted border + text, hover fills with the
   cancelled-bg wash. WL `.dv-btn--danger`. Used for "Cancel booking".

   NOTE: the inline original used bg-surface (opaque) rather than transparent.
   Over the page's surface background these render identically; transparent is
   the WL-canonical token treatment and is adopted here. */
.os-btn--danger {
  background-color: transparent;
  border: 1px solid var(--cancelled-border);
  color: var(--cancelled);
}
.os-btn--danger:hover {
  color: var(--cancelled);
  background-color: var(--cancelled-bg);
}

/* ── Icon-only (square) ───────────────────────────────────────────────
   Square button, no horizontal padding, centred glyph. WL `.dv-btn--icon`.
   Used for the Ledger date-navigation arrows.

   NOTE: the Ledger arrows were previously `.ledger-nav-arrow` — a 32px square
   with a hairline border and muted-ink glyph. The shared `.dv-btn--icon` is a
   36px square (matching the canonical `.dv-btn` height) with a muted glyph.
   To preserve the Ledger's 32px sizing AND its hairline outline, the arrows
   use `.dv-btn dv-btn--secondary dv-btn--icon dv-btn--sm` (secondary supplies
   the surface + hairline + ink, sm drops it to 32px). This keeps the existing
   look while consuming only shared classes. */
.os-btn--icon {
  /* Square: width tracks whatever height is in force, so an icon button is
     36px at base density and 48px in an .os-density--touch scope without a
     second modifier. Previously a 2.25rem literal that could not follow. */
  width: var(--control-h);
  padding-left: 0;
  padding-right: 0;
  justify-content: center;
  color: var(--muted);
}
.os-btn--icon:hover {
  background-color: var(--state-hover);
}

/* ── Small size modifier (h-8) ────────────────────────────────────────
   Denser button for inline/contextual actions. WL `.dv-btn--sm`:
   height 32px, padding 0 12px, font 12px. Used for the booking-detail
   "Assign" (primary sm) and "Copy" (secondary sm) buttons, and — combined
   with `--icon` — the Ledger nav arrows (32px square). */
.os-btn--sm {
  --control-h:     var(--control-h-sm);      /* 32px / h-8 */
  --control-pad-x: var(--control-pad-x-sm);  /* 12px / px-3 */
  --control-fs:    var(--control-fs-sm);     /* 12px */
}
/* --sm is declared AFTER --icon and has equal specificity, so its padding
   would otherwise win and un-square the icon button. Re-zero it here. Width
   needs no repeat: --icon reads var(--control-h), which --sm has re-pointed
   on this same element. */
.os-btn--sm.os-btn--icon {
  padding-left: 0;
  padding-right: 0;
}
/* Mini button — for an inline action that sits BESIDE a status badge and must
   align to its height, not the 32px --sm (e.g. waiver roster "Send link" next
   to the "Not signed" pill). 22px tall ≈ the .dv-badge--square height. */
.os-btn--xs {
  --control-h:     var(--control-h-xs);      /* 22px */
  --control-pad-x: var(--control-pad-x-xs);  /* 8px */
  --control-fs:    var(--control-fs-xs);     /* 10px */
  gap: 4px;
}
.os-btn--xs.os-btn--icon { padding-left: 0; padding-right: 0; }

/* ── Confirmed (green secondary) ──────────────────────────────────────
   Exactly the --secondary button (surface fill, 1px outline, base type), only
   recoloured to the confirmed hue: green-700 border + text. Hover fills with
   the confirmed-bg wash. Used for positive transactions — check a diver in,
   mark a diver back aboard (Trip Run v4). */
.os-btn--confirmed {
  background-color: var(--surface);
  border: 1px solid var(--confirmed);
  color: var(--confirmed);
}
.os-btn--confirmed:hover {
  color: var(--confirmed);
  background-color: var(--confirmed-bg);
}


/* ===== os_ds/components/callout.css ===== */
/* ── .dv-callout ──────────────────────────────────────────────────────────
   Tinted status/info notice panel — the hand-rolled bg-*-bg panels promoted
   to a canonical component. NOT a card (no card shell); a flagged message.
   Layout: leading icon + content (optional eyebrow + body) + optional action.
   Tones key to the status palette triplets. Token-only; light-only.
   ───────────────────────────────────────────────────────────────────────── */

.os-callout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface-alt);
  color: var(--ink);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.os-callout__icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: var(--space-0-5);
}

.os-callout__icon svg, .os-callout__icon [data-lucide] {
  width: 16px;
  height: 16px;
}

.os-callout__content {
  flex: 1 1 auto;
  min-width: 0;
}

.os-callout__eyebrow {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: var(--space-1);
}

.os-callout__title {
  font-weight: 600;
  color: var(--ink);
}

.os-callout__body {
  color: var(--text-secondary);
}

.os-callout__title + .os-callout__body {
  margin-top: var(--space-0-5);
}

.os-callout__action {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  /* Centre the trailing action against the message instead of top-aligning it
     (the callout itself is flex-start so the icon hugs the first body line). A
     32px button beside a single-line title looked dropped without this. */
  align-self: center;
  margin-left: auto;
}

/* ── Tones ───────────────────────────────────────────────────────────────── */

.os-callout--info {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--ink);
}
.os-callout--info .os-callout__icon, .os-callout--info .os-callout__eyebrow {
  color: var(--accent);
}

.os-callout--briefing {
  background: var(--briefing-bg);
  border-color: var(--briefing-border);
  color: var(--briefing);
}
.os-callout--briefing .os-callout__icon, .os-callout--briefing .os-callout__eyebrow, .os-callout--briefing .os-callout__title {
  color: var(--briefing);
}

.os-callout--confirmed {
  background: var(--confirmed-bg);
  border-color: var(--confirmed-border);
  color: var(--confirmed);
}
.os-callout--confirmed .os-callout__icon, .os-callout--confirmed .os-callout__eyebrow, .os-callout--confirmed .os-callout__title {
  color: var(--confirmed);
}

.os-callout--cancelled {
  background: var(--cancelled-bg);
  border-color: var(--cancelled-border);
  color: var(--cancelled);
}
.os-callout--cancelled .os-callout__icon, .os-callout--cancelled .os-callout__eyebrow, .os-callout--cancelled .os-callout__title {
  color: var(--cancelled);
}

.os-callout--inwater {
  background: var(--inwater-bg);
  border-color: var(--inwater-border);
  color: var(--inwater);
}
.os-callout--inwater .os-callout__icon, .os-callout--inwater .os-callout__eyebrow, .os-callout--inwater .os-callout__title {
  color: var(--inwater);
}


/* ===== os_ds/components/card.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   DiveOS info card — .dv-card

   Promoted from the Booking Details "Forked card shell" (the page-scoped
   `.bk-detail .card` in account/bookings.css), which the product owner blessed
   as the canonical info-card shell: white surface, 1px hairline, radius-lg,
   NO resting shadow, and an uppercase eyebrow head that flows straight into the
   body (no divider). Reproduced here verbatim as an unscoped component so any
   surface can use it.

   Interaction states (--interactive / --selected) are modeled on the design-
   system handoff's `.dos-card` (Design/templates/rails-handoff): interactive
   cards lift on hover; selected cards take an accent border.

   Spacing note: the 22px vertical padding is intentionally OFF the 8pt grid —
   it's preserved to match the blessed reference pixel-for-pixel. Horizontal
   padding and head/actions gaps use the --space-* scale. If the 22px is later
   reconciled to 24px (--space-6), change it here once.
   Token-driven; @imported from application.css after components/booking-v2.css.
   ───────────────────────────────────────────────────────────────────── */

.os-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  padding: 0;
}
/* Opt out of the radius clip for cards that host an absolutely-positioned
   popover (e.g. a calendar_input datepicker, .dv-pop z-index:30). The base
   overflow:hidden would clip the popover to the card edge. Only safe on cards
   WITHOUT a nested .dv-table-wrap, which relies on the clip for its radius. */
.os-card--overflow-visible { overflow: visible; }
/* Table-wrapper card (heading head + a flush .dv-table-wrap body via bare:true):
   the card owns the frame, so the nested table-wrap drops its own border/radius
   — no box-in-box. The card's overflow:hidden clips the table to the radius. */
.os-card > .os-table-wrap {
  border: 0;
  border-radius: 0;
  transition: box-shadow var(--t-fast) var(--ease-standard),
              border-color var(--t-fast) var(--ease-standard),
              background-color var(--t-fast) var(--ease-standard);
}

/* Head — uppercase eyebrow label (left) + optional actions (right). Top-only
   padding so the head flows into the body with no divider. */
.os-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px var(--space-6) 0;
}
.os-card__title {
  display: flex;
  align-items: center;
  gap: 7px;                 /* optical icon gap; off-grid by intent */
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}
.os-card__title i, .os-card__title svg { flex-shrink: 0; color: var(--muted-2); }
/* Display-title head — the 15px General Sans heading (matches
   shared/patterns/_card_title), for cards whose head is a prominent section
   heading rather than the 11px uppercase eyebrow. Pair with --divided when the
   head also carries an action. */
.os-card__title--display {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  font-family: var(--font-display);
}
.os-card__actions { display: flex; align-items: center; gap: var(--space-2); }

.os-card__body { padding: 22px var(--space-6); }

/* Foot — trailing action row; the mirror of __head. Bottom-only padding so the
   body flows into it with no divider, exactly as the head flows into the body.
   Actions sit right, primary rightmost (--split when a destructive or secondary
   action anchors left). Pair with --divided when the card needs the hairline
   band; the divided rule below mirrors the head's.

   This is the canonical home for a card's actions. `.os-card__actions` is the
   HEAD's action slot (a bare flex row with no padding or divider) — placed
   after the body it renders as a naked row butting the content, which is what
   consumers were hand-rolling card footers to work around. */
.os-card__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: 0 var(--space-6) 22px;
}
.os-card__foot--split { justify-content: space-between; }

/* ── Variants ────────────────────────────────────────────────────────
   --raised  : soft elevation (the bk2-card treatment) for cards that float
               above the canvas rather than sit flush in a column.
   --divided : head sits in its own band with a hairline divider, instead of
               flowing into the body. Use when the head carries an action row
               or the body is dense. */
.os-card--raised { box-shadow: var(--shadow-sm); }
/* --float: the card overlaps a tinted page hero and must lift off it. The ONLY
   other place the overlay tier is used besides the sticky action footer — see
   the two-elevation rule. Deliberately not a third shadow: `--shadow-float` was
   proposed and withdrawn after a pixel diff put it 0.54/765 from this. */
.os-card--float { box-shadow: var(--elevation-overlay); }
.os-card--divided .os-card__head {
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--hairline);
}
.os-card--divided .os-card__foot {
  padding-top: var(--space-4);
  border-top: 1px solid var(--hairline);
}

/* ── Interaction states (from handoff .dos-card) ─────────────────────────
   Apply --interactive only when the whole card is a click target. */
.os-card--interactive { cursor: pointer; }
.os-card--interactive:hover {
  box-shadow: var(--shadow-hover);
  background-color: var(--canvas);
}
.os-card--interactive:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.os-card--selected { border-color: var(--accent); }

/* --danger — a card whose contents are IRREVERSIBLE and consequential to
   someone other than the operator (release the seats and notify every guest;
   delete an account). Distinct from an error (which reports what already went
   wrong) and from a warning: this holds an action about to be taken. Recolours
   the card's OWN border + surface via the cancelled status pair — one signal,
   no extra left stroke, no second knob. The icon stays the callsite's job
   (domain vocabulary: triangle-alert here, ship-off elsewhere) and button
   variants stay the callsite's (`.os-btn--danger` already exists). Pair with
   `.os-card__note` for the blast-radius line the operator is owed. */
.os-card--danger {
  border-color: var(--status-cancelled-border);
  background: var(--status-cancelled-bg);
}
/* Title + note read as one block, so the body sits tighter under the head. */
.os-card--danger .os-card__body { padding-top: var(--space-4); }

/* __note — the muted explanatory line a consequential card owes its operator
   ("both actions release the seats and notify every guest on this manifest").
   Sits between head/body and the actions; small, muted, its own bottom margin.
   Not danger-only, but this is where it earns its keep. */
.os-card__note {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--muted);
}


/* ===== os_ds/components/chart-card.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   DiveOS chart card — .dv-chart-card

   Canonical panel for hand-rolled Chartkick chart containers (the
   `bg-surface border border-hairline rounded-lg shadow-card p-6` + an
   `<h3 class="text-lg ...">` pattern dotted around dashboards). White
   surface, 1px hairline, radius-lg, padding via the --space scale, and a
   head (title + optional actions) that flows into a body holding the
   chart canvas.

   Unlike .dv-card (eyebrow head, flush body), the chart card keeps a
   prominent --text-lg display title and a generous body min-height so the
   chart canvas has room before Chartkick measures it.

   Token-driven; @imported from application.css after components/card.css.
   ───────────────────────────────────────────────────────────────────── */

.os-chart-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}

/* --raised : soft elevation for cards floating above the canvas. */
.os-chart-card--raised { box-shadow: var(--shadow-sm); }

/* Head — title (left) + optional actions (right). */
.os-chart-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.os-chart-card__title {
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  font-family: var(--font-display);
}

.os-chart-card__sub {
  margin-top: 2px;
  font-size: var(--text-xs);
  color: var(--muted);
}

.os-chart-card__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Body — holds the chart canvas. min-height keeps the panel from
   collapsing before Chartkick renders / when data is empty. */
.os-chart-card__body {
  min-height: 300px;
}


/* ===== os_ds/components/chip.css ===== */
/* ── dv-chip (entity chip) ─────────────────────────────────────────────────
   A named, dismissible entity token — a selected person/tag rendered as a pill
   with optional avatar + inline remove control. Distinct from the legacy
   package-builder toggle chip (.dv-chip.is-on); an entity chip carries the
   --entity modifier so the two never collide. Token-only.
   Blessed 2026-07-05 (Trip Roster build).
   ────────────────────────────────────────────────────────────────────────── */
.os-chip--entity {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Right padding is tight (6px) because the ✕ remove control fills that side.
     A NON-dismissible chip (no ✕) has no such filler, so its text would sit
     only 6px from the pill's curved edge — the `:not(:has())` rule below gives
     it comfortable, symmetric end padding. */
  padding: 5px 6px 5px 8px;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  max-width: 100%;
}
.os-chip--entity:not(:has(.os-chip__x)) { padding-right: 12px; }
.os-chip--entity .os-chip__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.15;
}
.os-chip__name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.os-chip__meta {
  font-size: var(--text-2xs);
  color: var(--muted-2);
  white-space: nowrap;
}
.os-chip__x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border: 0;
  background: none;
  border-radius: var(--radius-pill);
  color: var(--muted-2);
  cursor: pointer;
  transition: background .12s, color .12s;
}
.os-chip__x:hover { background: var(--cancelled-bg); color: var(--cancelled); }
.os-chip__x :is(.lucide, .os-lucide) { display: block; }

/* ── .os-chip--sm — compact single-line dismissible pill ────────────────────
   A lighter alternative to --entity for simple one-line tags (dietary flags,
   filters, selected options) where the name/meta stack of the entity chip
   reads as cramped. Single line, tighter padding, a smaller ✕. Pairs with the
   same .os-chip__x remove control. Token-only.
   Promoted from arena-ops (HotelOS) 2026-07-25. */
.os-chip--sm {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px 2px 10px;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  max-width: 100%;
}
.os-chip--sm:not(:has(.os-chip__x)) { padding-right: 10px; }
.os-chip--sm .os-chip__x { width: 16px; height: 16px; }


/* ===== os_ds/components/choicecard.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   choicecard.css — `.os-choicecard` + `.os-choicegrid`, the icon-over-label
   choice tile.

   WHY THIS EXISTS. It is the answer to a segmented control whose labels
   truncate. `.os-segmented` lays its options on ONE horizontal track, so the
   moment a set has six members, or one member is called "Snorkelling gear",
   the labels ellipsis and the control stops being readable at a glance — the
   user has to read to choose, which is the one thing a segmented control is
   supposed to spare them. The fix is not a longer track: it is to make the
   choice a PICTURE. An icon carries the meaning, the label confirms it, and
   the set reflows onto as many rows as it needs.

   Use `.os-segmented` for 2–4 short, text-only, mutually-exclusive options
   that must read as one control; use `.os-choicecard` when the set is larger,
   the labels are longer, or the options have icons worth showing.

   MARKUP. The tile is a REAL `<button>` — it acts, it does not navigate — so
   the full UA chrome reset lives on it (see below). Checked state is the
   `.is-on` state class, matching the state-class convention the rest of the
   library uses, and the button must carry `aria-pressed` so the state is not
   colour-only:

     div.os-choicegrid
       button.os-choicecard.is-on  type=button aria-pressed="true"
         svg                                       (the icon helper's output)
         span.os-choicecard__l     "Diving"
       button.os-choicecard        type=button aria-pressed="false"
         …

   For a radio-group reading (one of N, form-submittable) wrap each tile in a
   label with a visually-hidden input instead and drive `.is-on` off `:checked`
   in the consuming view — the class carries no element assumption beyond the
   reset.

   SIZING AND `--control-h`. A choicecard is a control, but it is deliberately
   NOT one control-height tall: it stacks an icon over a label, so a single
   `height: var(--control-h)` would crush it. It reads the control trio as a
   RATIO instead — `min-height: calc(var(--control-h) * 1.5)` — which is 54px
   at the 36px base (the tile's natural height: icon + gap + label + padding)
   and grows with `.os-density--touch` exactly as its neighbours do, with no
   modifier. The padding is left to the spacing scale rather than derived,
   because the min-height is what governs; the padding only sets the floor for
   an unusually long, wrapping label.
   ───────────────────────────────────────────────────────────────────── */

/* ── The grid ─────────────────────────────────────────────────────────
   auto-fit + minmax means the set decides its own column count from the
   space it is given: five tiles in a wide rail, two per row in a drawer, no
   breakpoint anywhere. The floor is exposed as a component-local custom
   property so a consumer with longer labels can raise it without redeclaring
   the grid. */
.os-choicegrid {
  --os-choicegrid-min: 80px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--os-choicegrid-min), 1fr));
  gap: var(--space-2);
}

/* ── The tile ─────────────────────────────────────────────────────────
   UA chrome reset in full. Rendered as a button, which is correct for a
   control that toggles rather than navigates — and v1.6.2 shipped a fix for
   exactly this defect on `.os-segmented__option`, which set colour and
   padding but never border, background, appearance or font, so the browser's
   outset border and buttonface fill showed through and a group of options
   read as mismatched boxes. Enumerated rather than remembered: appearance,
   border, background, font, margin, radius, tap-highlight. */
.os-choicecard {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
  /* Containing block for the visually-hidden control a consumer nests here —
     the label form of this card wraps its own `position: absolute` input. See
     the long note on `.os-option-card`: unowned, that input anchors to the
     initial containing block, escapes every overflow clip on the way up and
     inflates the document's scroll height. Harmless on the bare-input form of
     this card, which has no positioned children. */
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: calc(var(--control-h) * 1.5);
  padding: var(--space-2);
  /* 1.5px, not 1px: the border IS the selected state here (there is no fill
     strong enough to carry it at this size), and a 1px hairline reads as
     noise rather than a choice when it turns accent. */
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-xl);
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-standard),
              background-color var(--t-fast) var(--ease-standard),
              color var(--t-fast) var(--ease-standard),
              transform var(--t-fast) var(--ease-standard),
              box-shadow var(--t-fast) var(--ease-standard);
}

.os-choicecard svg {
  color: var(--muted-2);
  transition: color var(--t-fast) var(--ease-standard);
}

.os-choicecard:hover {
  border-color: var(--muted-2);
  color: var(--ink);
  transform: translateY(-1px);
}

/* A real button needs a real keyboard affordance. The gem's focus ring, not
   a hand-rolled outline, so a product re-pointing --accent-rgb (arena-ops
   does) moves this ring with it. */
.os-choicecard:focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 2px;
  box-shadow: var(--shadow-focus);
}

/* Honour a reduced-motion preference: the lift is decoration, the colour
   change is the information. */
@media (prefers-reduced-motion: reduce) {
  .os-choicecard { transition: none; }
  .os-choicecard:hover { transform: none; }
}

.os-choicecard__l {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 500;
}

/* ── Checked ──────────────────────────────────────────────────────────
   Border + fill + a soft halo, so the state survives at a glance across a
   grid of eight. `.is-on` is a state class, so it outranks :hover by source
   order — a selected tile must not lose its accent under the pointer. */
.os-choicecard.is-on,
.os-choicecard.is-on:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-strong);
  box-shadow: 0 0 0 3px var(--accent-softer);
}

.os-choicecard.is-on svg {
  color: var(--accent);
}

/* ── Density ──────────────────────────────────────────────────────────
   The one modifier. A tighter tile for a drawer or a rail — everything else
   (icon, label, states) is unchanged. */
.os-choicecard--sm {
  min-height: calc(var(--control-h) * 1.25);
  padding: var(--space-1);
  gap: var(--space-1);
  border-radius: var(--radius-lg);
}

/* A choicecard can be unavailable (a service not offered on this date), and
   must grey out rather than vanish — same rule as `.os-qty__btn`. */
.os-choicecard:disabled,
.os-choicecard[aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}


/* ===== os_ds/components/confirm_dialog.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   Confirm dialog — DiveOS design-system component (promoted 2026-06-20).

   The single styled replacement for the browser's native confirm() popup.
   Wired globally via Turbo.setConfirmMethod (support/confirm_dialog.js), so
   every `data-turbo-confirm="…"` across the app renders this dialog instead
   of the OS chrome. Built on the native <dialog> element (top-layer + free
   backdrop + Escape handling), mirroring the .arena-modal pattern in modal.css.

   Token-only. The OK button inherits destructive styling when the triggering
   control is destructive (see the JS); otherwise it is the primary accent.
   ───────────────────────────────────────────────────────────────────── */

/* Do NOT set `display` on the base — the UA `dialog:not([open]){display:none}`
   must stay in effect so the closed dialog never blocks page interaction. */
dialog.os-confirm {
  padding: 0;
  border: 0;
  background: transparent;
  max-width: none;
  max-height: none;
  overflow: visible;
}
dialog.os-confirm[open] {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
dialog.os-confirm::backdrop {
  background-color: rgb(15 23 42 / 0.45);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.os-confirm__panel {
  width: 100%;
  max-width: 420px;
  margin: 16px;
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.os-confirm__title {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-md);
  letter-spacing: -0.01em;
  color: var(--ink);
}
.os-confirm__msg {
  margin: 0 0 22px;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--muted);
}
.os-confirm__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}


/* ===== os_ds/components/countdown.css ===== */
/* os-countdown — countdown chip: boxed mono value + plain label ("05 · days to arrival").
 * Source: `.countdown/.cbox/.clab` in arrival-rich.html (the only mockup carrying it).
 * The value box is the only emphasis; the label stays sentence-case running text so the
 * chip can sit inline under a hero title without competing with it.
 */

.os-countdown {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.os-countdown__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--text-xl);
  line-height: var(--leading-xl);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--accent-border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-card);
}

.os-countdown__label {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  color: var(--text-secondary);
}

.os-countdown__label b,
.os-countdown__label strong {
  color: var(--ink);
  font-weight: 700;
}


/* ===== os_ds/components/databand.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   databand.css — `.os-databand`, a tinted in-flow group header for a list
   whose groups live INSIDE one container.

   WHY THIS EXISTS, AND HOW IT DIFFERS FROM `.os-daterail`. Both mark the
   groups of a grouped list; they answer different layouts and are not
   interchangeable.

     .os-daterail  — the group marker lives in a LEFT COLUMN beside the rows
                     and sticks while they scroll. Right when the list is the
                     page and the groups are long: a dive schedule, an
                     arrivals board.
     .os-databand  — the group marker is a BAND ACROSS the container, in
                     normal flow. Right when the whole grouped list is itself
                     one document — a sale, a folio, a manifest — where a
                     left column would compete with the document's own left
                     edge, and where each group is a handful of rows rather
                     than a screenful.

   Using the rail in the second case was the observed mistake: the rail's
   sticky column re-indents the content of every group, so a document with a
   header band above it acquires two different left edges and stops reading
   as one object.

   THE TINT IS THE POINT. `--surface-alt` plus a hairline is what makes a
   band read as the LID of a container rather than as a heading floating
   above unrelated rows. A group header that is merely bold text sits at the
   same depth as the rows and the reader has to infer the grouping; a tinted
   band is a boundary they see without reading it.

   ANATOMY. Icon, label, sub-label, then the trailing controls.

     div.os-databand
       span.os-databand__ico      (icon, optional)
       span.os-databand__d        "11 Aug 2026"
       span.os-databand__w        "Tuesday"                (optional)
       span.os-databand__count    "1 seat · $45.00"        (optional)
       button.os-databand__x      (dismiss, optional)

   `__count` is monospace and pill-shaped: it carries figures, and several
   bands stacked in one container turn those figures into a column. It takes
   `margin-left: auto`, so everything after it hugs the right edge.

   `__x` inherits the CANCELLED status colours on hover rather than a generic
   red, because removing a group is destructive and the products already
   teach that colour as "this un-books something".
───────────────────────────────────────────────────────────────────── */

.os-databand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  background: var(--surface-alt);
  border-bottom: 1px solid var(--hairline);
}

/* Consecutive bands mean an EMPTY group between them. It still needs its own
   top edge or the two bands fuse into one thick stripe. */
.os-databand + .os-databand {
  border-top: 1px solid var(--hairline);
}

.os-databand__ico {
  display: flex;
  flex: none;
  color: var(--muted-2);
}

.os-databand__d {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
}

.os-databand__w {
  font-size: var(--text-sm);
  color: var(--muted);
}

.os-databand__count {
  margin-left: auto;
  flex: none;
  padding: var(--space-0-5) var(--space-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* Also takes margin-left:auto so the dismiss still sits at the right edge
   when the band carries no count. */
.os-databand__x {
  appearance: none;
  margin-left: auto;
  width: 26px;
  height: 26px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted-2);
  cursor: pointer;
  transition: background 120ms var(--ease-standard),
              color 120ms var(--ease-standard);
}

/* When a count is present it has already consumed the auto margin; the
   dismiss then sits directly beside it. */
.os-databand__count + .os-databand__x {
  margin-left: 0;
}

.os-databand__x:hover {
  background: var(--status-cancelled-bg);
  color: var(--status-cancelled-fg);
}

.os-databand__x:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}


/* ===== os_ds/components/daterail.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   daterail.css — `.os-daterail`, the sticky day marker for a list grouped
   by day.

   WHY THIS EXISTS. Every operational list in both products is grouped by day
   — a dive schedule, a transfer board, an arrivals list, a statement. The
   grouping was repeatedly expressed as a full-width heading row between
   groups, which costs a whole band of vertical rhythm per day and, worse,
   scrolls away: three screens into a long day the reader no longer knows
   which day they are looking at. The rail moves the marker into a left
   column and makes it STICKY, so the day stays on screen for exactly as long
   as its rows do, and the list itself keeps one uninterrupted rhythm.

   ANATOMY. Big number, then the calendar context under it, then an optional
   summary. `__d` is a display-font numeral at heading size because it is the
   thing the eye returns to; `__m` and `__w` step down in size and colour so
   the block reads top-down without a second glance; `__sum` is MONOSPACE
   because it carries counts and times, and a column of counts that does not
   line up is harder to scan than no column at all.

     div.os-daterail
       div.os-daterail__d    "14"
       div.os-daterail__m    "March 2026"
       div.os-daterail__w    "Saturday"
       div.os-daterail__sum  "6 runs" / "42 pax"          (optional)

   Pair it with a two-column layout (grid or flex) whose second column holds
   the day's rows; the rail carries `align-self: start` so it does not stretch
   to the group's height, which is what makes `position: sticky` bite.

   THE STICKY OFFSET is a component-local custom property with a default, not
   a literal, because the correct value is whatever the CONSUMER's fixed page
   chrome is tall — a workpage header, a filter bar, both. Override it by
   redeclaring --os-daterail-top on the list's own selector — for example a
   value of 116px where a header band and a filter bar stack — not in the
   component.
───────────────────────────────────────────────────────────────────── */

.os-daterail {
  /* Default clears a single sticky header band with a little breathing room.
     Consumers with taller chrome raise it; a consumer with none sets 0. */
  --os-daterail-top: 76px;
  position: sticky;
  top: var(--os-daterail-top);
  /* Without this the rail stretches to the group's full height and sticky has
     nothing to slide within. */
  align-self: start;
}

.os-daterail__d {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.os-daterail__m {
  margin-top: var(--space-0-5);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  color: var(--muted);
}

/* Separated from the month by a wider gap than the month is from the number:
   the number + month are one reading ("14 March 2026"), the weekday is a
   second, softer one. */
.os-daterail__w {
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted-2);
}

/* Counts and times. Monospace + a loose line-height so several summary lines
   stack as a scannable column rather than a paragraph. */
.os-daterail__sum {
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  line-height: 1.7;
  color: var(--muted);
}


/* ===== os_ds/components/desclist.css ===== */
/* ─── Description list (.dv-desclist) ────────────────────────────────────
   Formalized key/value description-list component. Promotes the earlier
   `.dv-kv` pattern into a proper <dl> with named variants. Default is the
   STACKED multi-column grid (muted label on TOP, ink value BELOW) the
   info_card bodies rely on — the eye scans straight down, no full-width
   eye travel, no per-row dividers. Token-driven, light-only.

   API
     .dv-desclist                      the <dl>: responsive auto-fill grid
     .dv-desclist__row                 a <div> wrapping one dt + dd
     .dv-desclist__term                the <dt> (label: 12px, muted)
     .dv-desclist__desc                the <dd> (value: 14px, ink)
   Variants (on .dv-desclist)
     --inline      term left / desc right on one line (settings layout)
     --striped     alternating row tints + hairline dividers
   Modifier (on .dv-desclist__row)
     --full        row spans all grid columns (long values)

   Supersedes the retired `.dv-kv` pattern — Waiver show now renders this
   component; `.dv-kv*` and the `kv_row` partial no longer exist. */

.os-desclist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 18px 28px;
  margin: 0;
}

.os-desclist__row {
  min-width: 0;
}
.os-desclist__row--full {
  grid-column: 1 / -1;
}

.os-desclist__term {
  display: block;
  color: var(--text-muted);
  font-size: var(--text-xs);
  line-height: 1.4;
  margin-bottom: 3px;
}

.os-desclist__desc {
  display: block;
  color: var(--ink);
  font-weight: 500;
  font-size: var(--text-sm);
  line-height: 1.45;
  margin: 0;
  word-break: break-word;
}
.os-desclist__desc a {
  color: var(--accent);
}

/* ── --inline: term left, desc right, one line per row (settings layout) ── */
.os-desclist--inline {
  display: block;
}
.os-desclist--inline .os-desclist__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 8px 0;
}
.os-desclist--inline .os-desclist__row--full {
  grid-column: auto;
}
.os-desclist--inline .os-desclist__term {
  margin-bottom: 0;
  flex: 0 0 auto;
}
.os-desclist--inline .os-desclist__desc {
  text-align: right;
  flex: 1 1 auto;
  min-width: 0;
}

/* ── --striped: alternating tints + hairline dividers (best with --inline) ── */
.os-desclist--striped {
  display: block;
}
.os-desclist--striped .os-desclist__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--hairline);
}
.os-desclist--striped .os-desclist__row:first-child {
  border-top: 1px solid var(--hairline);
}
.os-desclist--striped .os-desclist__row:nth-child(odd) {
  background: var(--surface-alt);
}
.os-desclist--striped .os-desclist__row--full {
  grid-column: auto;
}
.os-desclist--striped .os-desclist__term {
  margin-bottom: 0;
  flex: 0 0 auto;
}
.os-desclist--striped .os-desclist__desc {
  text-align: right;
  flex: 1 1 auto;
  min-width: 0;
}

/* ═══ v1.4.0 additions — read-only summary variants ═══════════════════════
   Promoted with the guest-arrival set. --mono for figure/code values, --panel
   for the tinted read-only block used by the summary rail (A1) and the
   full-width summary banner (A2). Both compose with --inline / --striped. */
/* ── --mono: figures/codes in monospace with tabular alignment ── */
.os-desclist--mono .os-desclist__desc {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ── --panel: tinted read-only block with hairline row dividers ── */
 /* USAGE: --panel IS the surface. Put it directly in the card (or standalone) —
    do NOT nest it inside `.os-card__body`, whose padding makes every row read as
    a chip floating in a card instead of one passive block. Reported from the
    arrival-wizard rail, where exactly that inversion shipped. */
.os-desclist--panel {
  background: var(--surface-alt);
  border-radius: var(--radius-lg);
  gap: 0;
  overflow: hidden;
}

.os-desclist--panel .os-desclist__row {
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--hairline);
}

/* First row of each grid column has no rule above it. */
.os-desclist--panel .os-desclist__row:first-child {
  border-top: none;
}


/* ===== os_ds/components/drawer.css ===== */
/* ═══════════════════════════════════════════════════════════════════════
   os-drawer — desktop side panel.

   A modal surface anchored to the RIGHT edge, full viewport height, that
   slides in horizontally. The drawer is where a record is triaged without
   leaving the list behind it: the list stays visible under the scrim, the
   drawer carries the detail, and the __foot carries the commit.

   SIBLING OF .os-sheet, NOT A VARIANT OF IT. The sheet is the mobile bottom
   sheet — it rises from the bottom edge, caps at 90svh so the page stays
   partly visible above it, wears a drag grip, and stretches its footer
   buttons full-width because a thumb needs a 48px target. Every one of those
   is wrong for a desktop panel, so nothing is shared but the vocabulary.
   Before this component existed the system had no desktop drawer at all and
   consumers hand-rolled one (arena-ops minted `.hos-odrawer-head`/`-body`) —
   and, minting only the parts they needed, shipped it with no footer, so
   drawer actions had nowhere canonical to sit.

   Anatomy:
     .os-drawer                — fixed scrim (toggle .is-open)
       .os-drawer__panel       — the panel surface
         .os-drawer__head      — title band (hairline divider)
         .os-drawer__body      — scroll region
         .os-drawer__foot      — sticky action row

   Sizes (on .os-drawer__panel):
     --sm 360px · (default) 440px · --lg 560px · --xl 720px

   STACKING CAVEAT: never nest a drawer/sheet inside a `position: sticky`
   ancestor. `sticky` establishes a stacking context unconditionally, so the
   drawer's z-index becomes local to that context and cannot rise above a
   sibling sticky header — the drawer renders correctly but is painted UNDER
   the header regardless of how high its z-index is. Mount the drawer at the
   page/body level (its scrim is `position: fixed` for exactly this reason),
   not inside a sticky rail or sticky table header.

   Token-driven; picked up by the bundle from components/*.css.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Behaviour contract ──────────────────────────────────────────────────
   PURE CSS — no Stimulus controller, deliberately, matching `.os-sheet`
   (which also ships none). The consumer owns the open/close state and just
   toggles `.is-open` on the root; the gem does not own another product's
   modal lifecycle, and in a Hotwire app the drawer is usually driven by a
   turbo-frame the app already controls. That leaves focus management with
   the consumer too: the drawer does not trap focus. Give `__panel`
   `role="dialog" aria-modal="true"` and a `tabindex="-1"` to focus on open;
   the panel's own focus outline is suppressed below because that programmatic
   focus should not paint a ring.
   ─────────────────────────────────────────────────────────────────────── */

.os-drawer {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  justify-content: flex-end;   /* right-anchored — the sheet's flex-end is vertical */
  /* The scrim tracks `--ink` rather than restating the sheet's literal
     rgba(17,24,39,.45): a product that repoints its neutrals (HotelOS points
     them at slate) gets a scrim in its own family, and no raw color lands in
     a component file. 45% matches the sheet's dim exactly at the gem default,
     where --ink resolves to --gray-900 (#111827 = rgb(17,24,39)). */
  background: color-mix(in srgb, var(--ink) 45%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-med) var(--ease-standard);
}
.os-drawer.is-open {
  opacity: 1;
  pointer-events: auto;
}

.os-drawer__panel {
  width: 440px;
  max-width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  /* Left corners only. The right edge is flush against the viewport, so
     rounding it would leave a sliver of scrim down the outside — the same
     reason the sheet rounds only its top two corners. */
  border-radius: var(--radius-xl) 0 0 var(--radius-xl);
  box-shadow: var(--shadow-panel);
  transform: translateX(100%);
  /* --t-slow is 240ms — the sheet's .24s, restated as the token. */
  transition: transform var(--t-slow) var(--ease-out-quart);
}
.os-drawer.is-open .os-drawer__panel { transform: translateX(0); }

/* ── Sizes ───────────────────────────────────────────────────────────────
   Widths are literal px, not `--space-*`: these are panel measures (how much
   reading column the content gets), not spacing steps, and the scale has no
   width tokens to reuse. 440 is the default because it holds a label+value
   detail list and a two-button footer without wrapping. */
.os-drawer__panel--sm { width: 360px; }
.os-drawer__panel--lg { width: 560px; }
.os-drawer__panel--xl { width: 720px; }

/* Head — title (left) + optional close/action (right), laid out like
   `.os-card__head`, NOT like `.os-sheet__head`. The sheet's head is `display:
   block` because a bottom sheet leads with a centred grip and title; a desktop
   drawer's head almost always carries a title and a close affordance on one
   row. Left as a block, every consumer wraps its own flex row inside the head —
   precisely the hand-rolled drift this component exists to prevent. */
.os-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--hairline);
  flex-shrink: 0;
}
.os-drawer__head .os-eyebrow { color: var(--muted-2); }

.os-drawer__body {
  padding: var(--space-5);
  overflow-y: auto;
  flex: 1;
  /* A scroller must also be a containing block. This body scrolls inside the
     `position: fixed` `.os-drawer__panel`, so an absolutely positioned
     descendant (every visually-hidden radio is one) otherwise resolves against
     that FIXED wrapper instead of this scroller: it does not scroll with the
     content, and focusing it makes the browser scroll-into-view the fixed
     ancestor, yanking the whole drawer off-screen. `.side-panel__body` in
     patterns.css hit exactly this and was fixed there; this sibling and
     `.os-sheet__body` were missed at the time. */
  position: relative;
}
.os-drawer__body > .os-eyebrow { display: block; color: var(--muted-2); margin-bottom: 4px; }

/* ── Footer ──────────────────────────────────────────────────────────────
   Footer semantics follow `.os-card__foot` (v1.5.0), NOT `.os-sheet__foot`:
   actions right, primary rightmost, buttons at their intrinsic width. The
   sheet's `flex:1; min-height:48px` full-bleed buttons are a thumb-target
   rule for a phone; on a pointer surface a stretched "Cancel" reads as
   equally weighted with "Save", which is the opposite of the intended
   hierarchy. `--split` anchors a secondary or destructive action left, again
   mirroring `.os-card__foot--split`.
   Unlike the card's footer this one is a real band: it is pinned outside the
   scroll region with its own hairline, because the body scrolls under it. */
.os-drawer__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5) calc(var(--space-4) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--hairline);
  background: var(--surface);
  flex-shrink: 0;
}
.os-drawer__foot--split { justify-content: space-between; }

/* ── Narrow viewports ────────────────────────────────────────────────────
   The drawer STAYS A DRAWER below 640px — it goes full-bleed and drops its
   radius, but keeps sliding in from the right. It does not morph into
   `.os-sheet` behaviour, deliberately: a sheet caps at 90svh so the page
   shows through above it, which only works for a short focused task (pick a
   value, log a number). A drawer carries a whole record; capped to 90svh it
   would just be a shorter drawer with a wasted strip of scrim, and the grip
   would promise a drag gesture no CSS here implements. A surface that wants
   sheet behaviour on a phone should render `.os-sheet` under a media query —
   the two are siblings precisely so a consumer can choose.
   ─────────────────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .os-drawer__panel,
  .os-drawer__panel--sm,
  .os-drawer__panel--lg,
  .os-drawer__panel--xl {
    width: 100%;
    border-radius: 0;
  }
}

/* Programmatic focus-on-open must not paint a ring; keyboard focus inside
   the drawer is untouched and still uses --shadow-focus via the controls. */
.os-drawer__panel:focus { outline: none; }

@media (prefers-reduced-motion: reduce) {
  .os-drawer,
  .os-drawer__panel { transition: none; }
}


/* ===== os_ds/components/dv_datepicker.css ===== */
/* ── dv-datepicker ─────────────────────────────────────────────────────────
   Custom design-system date picker. Vanilla-JS calendar replacing the
   daterangepicker (jQuery) vendor lib for `fields--date` fields. Progressive
   enhancement: the gem's `.date-input` markup stays (hidden field = submit
   source of truth + display field), and this UI is layered on the display
   field's wrapper. Token-only.

   Ported from tmp/form-elements-preview.html (.cal / .cals / .cal__*). The
   .dv-field / .dv-pop trigger + popover shells are shared with dv_select.css;
   only datepicker-specific rules live here.
   ──────────────────────────────────────────────────────────────────────── */

/* Wrapper turns relative so the popover can anchor to the trigger. */
.os-datepicker { position: relative; }

/* The native gem inputs (display + hidden) stay in the DOM but are visually
   hidden. The display input remains the value source the controller writes to;
   we mirror its value into .dv-field__val. */
.os-datepicker__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Trigger — reuses .dv-field look from dv_select.css. The calendar icon sits
   on the LEFT of the value (matches approved preview row). */
.os-datepicker .os-select-field { justify-content: flex-start; }
.os-datepicker .os-select-field__val.is-placeholder { color: var(--text-muted); }

/* ── Calendar popover body ── */
/* #291 — components/popover.css (the Trip v4 roster port) is @imported AFTER
   this file and sets a generic `.dv-pop { width: 320px; overflow: hidden }`,
   which at equal specificity would win and force the two-month calendar (2×260px
   = 540px) to wrap + clip. Qualify with `.dv-pop` so this variant's width wins
   regardless of import order, and keep overflow visible so nothing is clipped. */
.os-pop.os-pop--cal {
  padding: 12px;
  width: max-content;
  min-width: auto;
  /* dv_select.css `.dv-pop` also caps max-width at min(24rem,92vw)=384px and
     max-height at 320px; both bleed onto this shared class. 384px is narrower
     than the two months (540px), so with flex nowrap they'd shrink to ~half
     size — the "cramped / design gone" look. Clear both caps. */
  max-width: none;
  max-height: none;
  overflow: visible;
}
/* Scoped to the popover — `.cal` / `.cals` are generic enough to collide with
   unrelated markup (e.g. the booking ticketband's `<span class="cal">`), so they
   must never leak as bare global selectors. */
/* #291 — nowrap so the two months always sit side by side. With flex-wrap:wrap
   a wrapping flex container resolves `width:max-content` to ~one month wide, so
   the second month wrapped below and overflowed the popover. nowrap makes
   max-content sum both months, and the wizard is wide enough to show them. */
.os-pop--cal :is(.cals, .os-cals) { display: flex; gap: 20px; flex-wrap: nowrap; }
.os-pop--cal :is(.cal, .os-cal) { width: 260px; padding: 6px; }
:is(.cal__head, .os-cal__head) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 6px 10px;
}
:is(.cal__title, .os-cal__title) { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }
:is(.cal__nav, .os-cal__nav) {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  border: none;
  background: transparent;
}
:is(.cal__nav, .os-cal__nav):hover { background: var(--surface-alt); color: var(--text-primary); }
:is(.cal__nav, .os-cal__nav):disabled, :is(.cal__nav, .os-cal__nav).is-hidden { visibility: hidden; }

:is(.cal__grid, .os-cal__grid) { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
:is(.cal__dow, .os-cal__dow) {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-muted);
  text-align: center;
  padding: 4px 0;
}
:is(.cal__day, .os-cal__day) {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-primary);
  user-select: none;
}
:is(.cal__day, .os-cal__day).is-empty { cursor: default; }
:is(.cal__day, .os-cal__day):not(.is-empty):hover { background: var(--surface-alt); }
:is(.cal__day, .os-cal__day).is-muted { color: var(--border-strong); }
/* before today — lighter, but STILL selectable (not disabled) */
:is(.cal__day, .os-cal__day).is-past { color: #9CA3AF; }
:is(.cal__day, .os-cal__day).is-today { font-weight: 700; color: var(--accent); }
:is(.cal__day, .os-cal__day).is-sel { background: var(--accent); color: #fff; font-weight: 600; }
:is(.cal__day, .os-cal__day).is-range { background: var(--accent-light); color: var(--accent-strong); border-radius: 0; }
:is(.cal__day, .os-cal__day).is-range:is(.r-start, .os-r-start) { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
:is(.cal__day, .os-cal__day).is-range:is(.r-end, .os-r-end) { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
/* keyboard focus cursor */
:is(.cal__day, .os-cal__day).is-focus { box-shadow: inset 0 0 0 2px var(--accent); }

/* keyboard focus cursor on the selected day shouldn't double up the ring */
:is(.cal__day, .os-cal__day).is-sel.is-focus { box-shadow: inset 0 0 0 2px #fff; }

/* ── Time row (only for include-time fields) ──
   Ported from tmp/form-elements-preview.html (.dv-time / .dv-seg / .dv-ampm).
   Reused standalone by fields--time (dv_timepicker via @import of this file). */
:is(.cal__time, .os-cal__time) {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 6px 2px;
  margin-top: 8px;
  border-top: 1px solid var(--border);
}
:is(.cal__time-label, .os-cal__time-label) { font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); }

/* ── .dv-time component (HH:MM segment + AM/PM toggle) ── */
.os-time { display: inline-flex; align-items: center; gap: 10px; width: max-content; }
.os-seg {
  flex: 0 0 auto;        /* don't stretch/shrink inside a form flex/grid cell */
  display: inline-flex;
  align-items: center;
  width: max-content;
  height: 38px;
  padding: 0 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color .12s, box-shadow .12s;
}
.os-seg:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
.os-seg input {
  flex: 0 0 auto;        /* never collapse below content width */
  width: 32px;
  min-width: 32px;
  box-sizing: content-box;
  padding: 0;
  text-align: center;
  border: none;
  outline: none;
  font-family: inherit;
  font-size: var(--text-md);
  height: 100%;
  color: var(--text-primary);
  background: transparent;
  -moz-appearance: textfield;
}
.os-seg input::-webkit-outer-spin-button, .os-seg input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.os-seg__c { color: var(--text-muted); font-size: var(--text-md); }
.os-ampm {
  display: inline-flex;
  height: 38px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.os-ampm button {
  border: none;
  background: var(--surface);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  padding: 0 16px;
  cursor: pointer;
  color: var(--text-muted);
  transition: background .12s, color .12s;
}
.os-ampm button + button { border-left: 1px solid var(--border-strong); }
.os-ampm button:is(.on, .os-on) { background: var(--accent); color: #fff; }
.os-ampm button:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-ring);
}

/* Hidden native time input — stays the submission source of truth. */
.os-time__native {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Footer (clear) ── */
:is(.cal__foot, .os-cal__foot) {
  display: flex;
  justify-content: flex-end;
  padding: 8px 6px 2px;
  margin-top: 8px;
  border-top: 1px solid var(--border);
}
:is(.cal__clear, .os-cal__clear) {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-muted);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
}
:is(.cal__clear, .os-cal__clear):hover { background: var(--surface-alt); color: var(--text-primary); }


/* ===== os_ds/components/dv_fileupload.css ===== */
/* ── dv-fileupload ──────────────────────────────────────────────────────────
   Custom design-system file upload. Progressive enhancement of a native
   <input type="file">: the native input stays in the DOM (source of truth for
   submit / direct-upload) and is visually hidden; this drop zone + file rows
   are layered on top and kept in sync by the fields--file-field controller.
   Ported from tmp/form-elements-preview.html (.dv-upload / .dv-file). Token-only.
   ────────────────────────────────────────────────────────────────────────── */

/* Drop zone — click target + drag-and-drop surface. Default size is centered;
   --sm / --md modifiers are left-aligned, horizontal, and progressively
   smaller for use in tighter form layouts. */
.os-upload {
  position: relative;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--canvas);
  min-height: 104px;
  padding: 18px;
  cursor: pointer;
  transition: border-color .12s, background .12s;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 8px;
}
.os-upload:hover, .os-upload.is-dragover {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.os-upload:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
}

.os-upload__icon {
  color: var(--accent);
  background: var(--accent-light);
  border-radius: var(--radius-sm);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.os-upload__icon :is(.lucide, .os-lucide) { display: block; }

.os-upload__t {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}
.os-upload__t b { color: var(--accent); font-weight: 600; }

.os-upload__h {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
}

/* --md — horizontal, left-aligned, no hint growth beyond the row height. */
.os-upload--md {
  min-height: 68px;
  padding: 12px 16px;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  text-align: left;
  gap: 12px;
}
.os-upload--md .os-upload__icon {
  width: 38px;
  height: 38px;
}
.os-upload--md .os-upload__h { margin-top: 2px; }

/* --sm — compact single row, icon + inline label, no hint line. */
.os-upload--sm {
  min-height: 44px;
  padding: 0 14px;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  text-align: left;
  gap: 8px;
}
.os-upload--sm .os-upload__icon {
  width: 26px;
  height: 26px;
}
.os-upload--sm .os-upload__t {
  font-size: var(--text-xs);
}
.os-upload--sm .os-upload__h {
  display: none;
}

/* Uploaded / to-upload file row. */
.os-file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  margin-top: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.os-file__ic { color: var(--accent); display: flex; flex-shrink: 0; }
.os-file__ic :is(.lucide, .os-lucide) { display: block; }

/* Image preview thumbnail — shown in place of the generic file icon when the
   picked/persisted file is an image, so the diver can confirm the right photo
   is selected (not just its filename). */
.os-file__thumb {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-alt);
  display: block;
}

.os-file__nm {
  font-size: var(--text-sm);
  flex: 1;
  min-width: 0;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.os-file__sz {
  font-size: var(--text-xs);
  color: var(--text-muted);
  flex-shrink: 0;
}

/* Action affordances on a row (download link, remove/cancel buttons). */
.os-file__act {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  flex-shrink: 0;
  border: none;
  background: transparent;
  transition: color .12s, background .12s;
}
.os-file__act:hover {
  color: var(--accent-strong);
  background: var(--surface-alt);
}
.os-file__act :is(.lucide, .os-lucide) { display: block; }

/* When a persisted file is flagged for removal, dim the row. */
.os-file.is-removing {
  opacity: .55;
}
.os-file.is-removing .os-file__nm {
  text-decoration: line-through;
}

/* Upload progress bar (preserved from the gem markup). */
.os-upload-progress {
  margin-top: 8px;
  overflow: hidden;
  font-size: var(--text-xs);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  position: relative;
}
.os-upload-progress__bar {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  white-space: nowrap;
  overflow: hidden;
  background: var(--accent);
  border-radius: var(--radius-sm);
}
.os-upload-progress__label {
  position: relative;
  text-align: center;
  color: var(--text-secondary);
  padding: 2px 0;
}


/* ===== os_ds/components/dv_select.css ===== */
/* ── dv-select ─────────────────────────────────────────────────────────────
   Custom design-system dropdown. Progressive enhancement of a native <select>:
   the native element stays in the DOM (source of truth for submit/validation)
   and is visually hidden; this UI is layered on top and kept in sync.
   Ported from tmp/form-elements-preview.html (.dv-field / .dv-pop / .dv-opt /
   .dv-search). Token-only.
   ────────────────────────────────────────────────────────────────────────── */

/* Visually-hidden native select — stays focusable + submittable (NOT display:none) */
.os-select__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.os-select { position: relative; }

/* ── Touch / coarse-pointer fallback ──────────────────────────────────────
   On touch devices the controller (dv_select_controller connect()) skips the
   custom .dv-pop popup and keeps the native <select> interactive so a tap opens
   the OS picker. The native select is layered transparently OVER the styled
   .dv-field trigger (which is visual-only here), so the field shows the design-
   system look while the tap target is the real native control. */
.os-select--native > select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}

/* Trigger */
.os-select-field {
  width: 100%;
  /* A select is a FIELD control: its floor is the 46px field baseline, never
     the 36px button base. So it takes the LARGER of the surface density and
     that floor — 46px at base/compact/comfortable density (unchanged from the
     literal it replaces), 48px inside .os-density--touch. Expressed as max()
     rather than a pinned token because "never shorter than a field, but grow
     with the surface" is the actual rule, and a pinned value could not grow.
     Was a bare 46px literal, one of four independent copies of that number. */
  min-height: max(var(--control-h), var(--control-h-field));
  padding: 10px 12px;
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--text-primary);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  transition: border-color .12s, box-shadow .12s;
  user-select: none;
}
.os-select-field:hover { border-color: var(--accent); }
.os-select-field.is-open, .os-select-field:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
  outline: none;
}
.os-select-field__val { flex: 1; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.os-select-field__val.is-placeholder { color: var(--text-muted); }
.os-select-field__icon { color: var(--text-muted); flex-shrink: 0; display: inline-flex; }

/* Compact size — inline selects in dense contexts (e.g. the Today to-schedule
   card headers), smaller than the 46px filter-row default. A real component
   variant, not a per-surface height hack. */
.os-select--sm .os-select-field {
  /* 34px is OFF the control scale — it sits between --sm (32) and base (36)
     and matches nothing else in the system. It is the drift this contract
     exists to stop, kept at its shipped value here because correcting it
     would move a live control. A future minor should land it on 32px. */
  min-height: 34px;
  padding: 6px 10px;
  font-size: var(--text-xs);
}
.os-select--sm .os-select-field__icon svg { width: 14px; height: 14px; }

/* Menu — the searchable custom-select dropdown built by dv_select_controller.js.
   NOTE: this class was historically named `.os-pop`, which COLLIDED with the
   separate multiselect/filter popover in popover.css (also `.os-pop`) at equal
   specificity. In the flattened os_ds.css bundle popover.css is emitted later,
   so its `overflow: hidden` won and this menu lost its scroll (long option lists
   overflowed the viewport uncut). Renamed to the `.os-select__menu` family so the
   two components no longer fight. Do NOT rename back to `.os-pop`. */
.os-select__menu {
  position: absolute;
  z-index: 30;
  margin-top: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 28px rgba(0, 0, 0, .10);
  padding: 6px;
  /* Never narrower than a legible floor: a compact (--sm) or otherwise narrow
     trigger must still open a menu wide enough that short labels ("No grouping",
     "All statuses") don't wrap. Capped so a genuinely long label wraps
     gracefully instead of forcing a huge menu. */
  min-width: max(100%, 12rem);
  max-width: min(24rem, 92vw);
  max-height: 320px;
  overflow-y: auto;
}
/* Rich two-line rows (diver→session picker) need room so site/guide don't
   truncate to "H · G.." — widen the popup past the (often narrow) trigger. */
.os-select__menu:has(.os-opt--rich) {
  min-width: min(340px, 92vw);
}

/* Option */
.os-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  font-size: var(--text-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-primary);
}
.os-opt:hover, .os-opt.is-active { background: var(--surface-alt); }
.os-opt[aria-selected="true"] {
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 500;
}
.os-opt__check { margin-left: auto; color: var(--accent); opacity: 0; display: inline-flex; flex-shrink: 0; }
.os-opt[aria-selected="true"] .os-opt__check { opacity: 1; }
.os-opt.is-disabled { color: var(--text-muted); cursor: not-allowed; opacity: .6; }

/* Rich two-line rows (diver → dive-session picker, #199). The row markup is
   supplied via <option data-rich-html> and rendered inside .dv-opt__rich.
   Used by both the crew/roster picker and the Today "to schedule" diver
   card's trip picker (ToScheduleHelper#trip_option_html) — one mechanism,
   not two: this superseded diver-card-only .dv-opt__main/.dv-opt__sub 2-line
   variant, which duplicated the same "which session?" disambiguation. */
.os-opt--rich { align-items: flex-start; padding: 9px 10px; }
.os-opt__rich { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.os-opt__rich .os-opt-line {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.os-opt__rich .os-opt-line--head { justify-content: space-between; }
.os-opt__rich .os-opt-title { display: flex; align-items: center; gap: 6px; min-width: 0; }
.os-opt__rich .os-opt-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  color: var(--text-muted);
}
.os-opt__rich .os-opt-meta :is(.truncate, .os-truncate), .os-opt__rich .os-opt-title :is(.truncate, .os-truncate) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.os-opt__rich [data-lucide], .os-opt__rich svg { width: 13px; height: 13px; flex-shrink: 0; }
.os-opt--rich .os-opt__check { align-self: center; }

/* Search header (component-scoped element).
   Named `.dv-select__search` (not the generic `.dv-search`, which is a separate
   search field owned by package_builder.css) so the two don't collide — that
   collision previously stacked package_builder's focus ring on this dropdown. */
.os-select__search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  margin-bottom: 4px;
  border: 1px solid transparent;
  border-bottom-color: var(--border);
  border-radius: var(--radius-sm);
  transition: border-color .12s, box-shadow .12s;
}
/* ONE focus indicator: a single ring around the whole search box. */
.os-select__search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
.os-select__search svg { color: var(--text-muted); flex-shrink: 0; }
.os-select__search input {
  border: none;
  outline: none;
  font-family: inherit;
  font-size: var(--text-sm);
  width: 100%;
  color: var(--text-primary);
  background: transparent;
}
/* The <input> is a bare type="text", so @tailwindcss/forms (Bullet Train
   default) paints its own border + ring on :focus. The container already shows
   focus, so suppress the input's ring — one clean indicator, not the BT box. */
.os-select__search input:focus, .os-select__search input:focus-visible {
  border: 0;
  outline: none;
  box-shadow: none;
  --tw-ring-shadow: 0 0 transparent;
  --tw-ring-offset-shadow: 0 0 transparent;
}

/* Empty state for search */
.os-select__menu-empty {
  padding: 10px;
  font-size: var(--text-sm);
  color: var(--text-muted);
  text-align: center;
}


/* ===== os_ds/components/dv_toast.css ===== */
/* ───────────────────────────────────────────────────────────────────
   dv_toast — transient confirmation toasts (operator / light theme)
   Approved design: Option A, top-right stack (tmp/toast-preview.html).
   Floats over content, stacks newest-on-top, auto-dismisses after 4s
   with a progress bar, manual close. Token-only.
   Spawned by toast_controller.js (flash → toast bridge).
   ─────────────────────────────────────────────────────────────────── */

:is(.toast-stack, .os-toast-stack) {
  position: fixed;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 360px;
  max-width: calc(100vw - 32px);
  pointer-events: none;
}
:is(.toast-stack, .os-toast-stack):is(.tr, .os-tr) { top: 16px; right: 16px; }

/* Mobile bottom-center placement — floats above a bottom tab bar / dock, where
   a top-right 360px slab would cover the header context. --os-toast-btm is a
   consumer knob: set it to clear your bottom chrome (e.g. 96px over an 84px bar). */
:is(.toast-stack, .os-toast-stack):is(.bc, .os-bc) {
  top: auto;
  right: auto;
  left: 50%;
  bottom: var(--os-toast-btm, 24px);
  transform: translateX(-50%);
  align-items: center;
  width: max-content;
  max-width: calc(100vw - 32px);
}
/* Click-through: a bottom-center toast overlaps the primary CTA underneath it,
   so the card must not eat the tap — only the dismiss control stays interactive.
   (Errors are sticky, so a caller relying on the CTA is never blocked by a timer.) */
:is(.toast-stack, .os-toast-stack):is(.bc, .os-bc) :is(.toast, .os-toast) { pointer-events: none; }
:is(.toast-stack, .os-toast-stack):is(.bc, .os-bc) :is(.toast__x, .os-toast__x) { pointer-events: auto; }

/* Off-screen assertive live region: mirrors error toasts so a screen reader
   announces them urgently (the visual stack is polite). Visually hidden, never
   a second visual surface. Created at boot by toast_controller.js. */
:is(.toast-live, .os-toast-live) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

:is(.toast, .os-toast) {
  pointer-events: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--text-muted);
  border-radius: var(--radius-md);
  box-shadow: 0 10px 30px rgba(17, 24, 39, 0.16), 0 2px 6px rgba(17, 24, 39, 0.08);
  padding: 12px 14px;
  display: flex;
  gap: 11px;
  align-items: flex-start;
  animation: os-toast-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
  position: relative;
  overflow: hidden;
}
:is(.toast, .os-toast):is(.out, .os-out) { animation: os-toast-out 0.18s ease forwards; }

@keyframes os-toast-in {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: none; }
}
@keyframes os-toast-out {
  to {
    opacity: 0;
    transform: translateX(16px);
    height: 0;
    padding-top: 0;
    padding-bottom: 0;
    margin-top: -10px;
  }
}

:is(.toast__ic, .os-toast__ic) { flex-shrink: 0; margin-top: 1px; }
:is(.toast__ic, .os-toast__ic) svg { display: block; }
:is(.toast__body, .os-toast__body) { flex: 1; min-width: 0; }
:is(.toast__title, .os-toast__title) { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }
:is(.toast__msg, .os-toast__msg) { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 1px; }

:is(.toast__x, .os-toast__x) {
  flex-shrink: 0;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px;
  margin: -2px -2px 0 0;
  border-radius: 4px;
  background: none;
  border: 0;
  line-height: 0;
}
:is(.toast__x, .os-toast__x) svg { display: block; }
:is(.toast__x, .os-toast__x):hover { background: var(--surface-alt, var(--gray-100)); color: var(--text-primary); }

:is(.toast__bar, .os-toast__bar) {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  background: currentColor;
  opacity: 0.4;
  animation: os-toast-shrink 4s linear forwards;
}
@keyframes os-toast-shrink {
  from { width: 100%; }
  to   { width: 0; }
}

/* status accents — generic notification semantics from tokens.css */
:is(.toast, .os-toast):is(.success, .os-success) { background: var(--green-100); border-color: var(--success-border); border-left-color: var(--success); }
:is(.toast, .os-toast):is(.success, .os-success) :is(.toast__ic, .os-toast__ic), :is(.toast, .os-toast):is(.success, .os-success) :is(.toast__bar, .os-toast__bar) { color: var(--success); }

:is(.toast, .os-toast):is(.error, .os-error) { background: var(--rose-100); border-color: var(--danger-border); border-left-color: var(--danger); }
:is(.toast, .os-toast):is(.error, .os-error) :is(.toast__ic, .os-toast__ic), :is(.toast, .os-toast):is(.error, .os-error) :is(.toast__bar, .os-toast__bar) { color: var(--danger); }

:is(.toast, .os-toast):is(.warning, .os-warning) { background: var(--amber-100); border-color: var(--warning-border); border-left-color: var(--warning); }
:is(.toast, .os-toast):is(.warning, .os-warning) :is(.toast__ic, .os-toast__ic), :is(.toast, .os-toast):is(.warning, .os-warning) :is(.toast__bar, .os-toast__bar) { color: var(--warning); }

:is(.toast, .os-toast):is(.info, .os-info) { background: var(--sky-100); border-color: var(--info-border); border-left-color: var(--info); }
:is(.toast, .os-toast):is(.info, .os-info) :is(.toast__ic, .os-toast__ic), :is(.toast, .os-toast):is(.info, .os-info) :is(.toast__bar, .os-toast__bar) { color: var(--info); }

@media (prefers-reduced-motion: reduce) {
  :is(.toast, .os-toast), :is(.toast, .os-toast):is(.out, .os-out) { animation-duration: 0.01ms; }
  :is(.toast__bar, .os-toast__bar) { animation: none; width: 0; }
}


/* ===== os_ds/components/fab.css ===== */
/* ═══════════════════════════════════════════════════════════════════════
   os-fab — floating action button.

   ONE action, pinned bottom-right, for the frequent ADD on a list that can run
   long. It exists because a header "+ New" scrolls away: on a roster the user
   scrolls for a minute before deciding to add a row, and the FAB is the only
   affordance still on screen at that point.

   Use it for exactly one action per screen, and only the screen's dominant one.
   Two FABs, or a FAB for a secondary action, is a misuse — that is a sticky
   action bar (`.os-sticky-bar`), which holds a row of them.

   Anatomy:
     .os-fab                   — the button (icon only)

   Bottom offset: `--os-fab-bottom` is the gap above whatever the surface pins
   to its bottom edge. Default is the safe-area inset plus a 16px breath, so a
   screen with no bottom bar needs no configuration. A screen WITH a bottom bar
   (tab bar, sticky action bar) sets the variable to that bar's height plus the
   gap — the FAB cannot measure it, and guessing a fixed offset is how a FAB
   ends up half-buried under a tab bar on one surface and floating oddly high on
   another.

   `position: fixed` is unreliable while the iOS keyboard is open
   (docs/mobile-primitives.md §4). That is acceptable here and only here: a FAB
   belongs on a scrolling LIST, which has no focused text input. Do not reach for
   this component on a form.
   ═══════════════════════════════════════════════════════════════════════ */

.os-fab {
  --os-fab-size:   56px;
  --os-fab-bottom: calc(env(safe-area-inset-bottom) + var(--space-4));
  --os-fab-inline: var(--space-4);

  position: fixed;
  right: max(var(--os-fab-inline), env(safe-area-inset-right));
  bottom: var(--os-fab-bottom);
  /* Above the page and any bottom bar, below drawers/sheets/modals — a FAB must
     never float over an open sheet's scrim. */
  z-index: var(--z-dropdown);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--os-fab-size);
  height: var(--os-fab-size);
  padding: 0;
  border: 0;
  /* Not a pill. A fully round FAB reads as a different family from the rest of
     the system's controls; the softened square keeps it recognisably os-ds. */
  border-radius: var(--radius-2xl);

  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
  box-shadow: var(--elevation-overlay);
  transition: background var(--t-fast) var(--ease-standard),
              box-shadow var(--t-fast) var(--ease-standard);
}

.os-fab :is(.lucide, .os-lucide) { display: block; }

.os-fab:hover { background: var(--accent-strong); }

.os-fab:focus-visible {
  outline: none;
  box-shadow: var(--elevation-overlay), var(--shadow-focus);
}

.os-fab:disabled {
  opacity: .5;
  cursor: not-allowed;
}


/* ===== os_ds/components/forms.css ===== */
/* forms.css — the shared FORM substrate, promoted into the gem (was
   `diveos_forms.css`, duplicated BYTE-FOR-BYTE in arena-ops and dive-os).
   Third promotion after header_band: label/input/select/textarea/hint/error,
   field + form-actions, check/radio, and the legacy .diveos-button family.

   Class names are kept AS-IS (`.diveos-*`) so the promotion is a pure move —
   every form callsite in both consumers keeps working. A `.dv-*` alias pass is
   a separate, later step; renaming here would break two apps at once.

   Consumers: delete your local `diveos_forms.css` and drop its
   `stylesheet_link_tag` once you are on this version. */

/* DiveOS form substrate — utility classes the ejected shared/fields/*
   partials (T6) and DiveosFormBuilder (T5) apply so 75+ forms inherit
   consistent styling without per-call class lists.
   Per docs/archive/UI-MIGRATION-PLAN.md PR1 task T4 + design-review D2 (error state)
   + D9 (focus rings + touch targets). */

/* Vertical rhythm between stacked fields — without this they collapse to
   margin:0 and read as a cramped wall (legacy-redesign QA finding). The
   label→input gap stays tight (6px); the field→field gap breathes (22px). */
:is(.form-section__body, .os-form-section__body) > * + * { margin-top: 22px; }
.os-field + .os-field { margin-top: 22px; }

.os-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 6px;
}
.os-label--required::after {
  content: " *";
  color: var(--error);
}

/* v4 input model per the repo spec: gray-50 bg, gray-300 border with a
   gray-400 hover step, accent-derived focus border + 10% focus ring. */
.os-input, .os-field-select, .os-textarea {
  display: block;
  width: 100%;
  /* Height is PINNED to the button scale so an input and the button beside it
     line up (a control row is the common case: "Record… [Save]", search +
     submit, a filter + apply). Before this, an input was implicitly ~40px from
     padding while .os-btn is a fixed 36px, so every inline pairing sat a few
     px off — visible app-wide. --sm matches .os-btn--sm. box-sizing keeps the
     border inside the declared height.

     Since v1.6.0 that pinning is literal: the input reads the SAME
     --control-h token .os-btn does, so the two cannot drift apart again and
     both follow an .os-density--* scope together. Padding stays a literal
     pair — an input's 12px inset is tighter than a button's 14px by design
     (the caret needs less shoulder than a centred label), so it is not the
     same value and must not be tokenised as if it were. */
  box-sizing: border-box;
  min-height: var(--control-h);   /* 36px at base density — .os-btn */
  padding: 10px 12px;
  font-family: var(--font-sans);
  font-size: var(--control-fs);
  color: var(--text-primary);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  transition: border-color 100ms ease, box-shadow 100ms ease;
}
/* Small control row — pairs with .os-btn--sm (32px). */
.os-input--sm, .os-field-select--sm {
  --control-h:  var(--control-h-sm);    /* 32px — .os-btn--sm */
  --control-fs: var(--control-fs-sm);   /* 12px */
  padding: 6px 10px;
}
/* (A textarea is multi-line — its own min-height: 96px below overrides the
   control height set above.) */
.os-input:hover, .os-field-select:hover, .os-textarea:hover {
  border-color: var(--input-border-hover);
}
.os-input:focus, .os-field-select:focus, .os-textarea:focus {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
.os-input:disabled, .os-field-select:disabled, .os-textarea:disabled {
  background: var(--surface-alt);
  color: var(--text-muted);
  cursor: not-allowed;
}
.os-input::placeholder, .os-textarea::placeholder {
  color: var(--input-placeholder);
}
.os-textarea {
  min-height: 96px;
  resize: vertical;
}

/* Error state — applied by ejected field partials (T6) when the field
   has validation errors. Design-review D2: red border + inline message
   below. The message itself uses .diveos-error. */
/* ══════════════════════════════════════════════════════════════
   DATE / TIME INPUTS — painted by DEFAULT (v1.13.0)
   ══════════════════════════════════════════════════════════════
   Same reasoning as the checkbox in v1.10.0: the default has to be the one you
   want. `<input type="date" class="os-input">` inherited the PLATFORM's calendar
   affordance — on Chrome a small grey glyph in the UA's own weight and colour,
   on Safari a different one — sitting inside a field drawn from our tokens. Two
   vocabularies in one control, and every product hit it because typing the
   obvious thing is what produced it.

   The glyph is now a MASK tinted with `--muted-2`, so it takes the theme's
   colour (and the accent on hover), matches the 1.75 stroke weight the icon set
   uses, and carries no hard-coded colour of its own — the mask is geometry, the
   colour is a token. The native picker still opens on click: this restyles the
   indicator, it does not replace the control.

   Firefox exposes no such pseudo-element and renders no indicator, so it
   degrades to a plain field — the same as before this rule.

   NO MARKUP CHANGE anywhere; every existing date input is repainted as-is. */
.os-input[type="date"],
.os-input[type="time"],
.os-input[type="datetime-local"],
.os-input[type="month"],
.os-input[type="week"] {
  /* iOS refuses to fill the field and centres the value instead unless the UA
     appearance is reset — the value then sits mid-field while the placeholder
     of the input beside it sits at the top. */
  -webkit-appearance: none;
  appearance: none;
}

.os-input[type="date"]::-webkit-calendar-picker-indicator,
.os-input[type="time"]::-webkit-calendar-picker-indicator,
.os-input[type="datetime-local"]::-webkit-calendar-picker-indicator,
.os-input[type="month"]::-webkit-calendar-picker-indicator,
.os-input[type="week"]::-webkit-calendar-picker-indicator {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  padding: 0;
  cursor: pointer;
  opacity: 1;
  background-color: var(--muted-2);
  -webkit-mask: var(--os-cal-glyph) center / 18px 18px no-repeat;
  mask: var(--os-cal-glyph) center / 18px 18px no-repeat;
  transition: background-color 120ms var(--ease-standard);
}

.os-input:hover::-webkit-calendar-picker-indicator { background-color: var(--muted); }
.os-input:focus::-webkit-calendar-picker-indicator { background-color: var(--accent); }

/* The calendar geometry, as a mask. `black` is the mask's alpha channel, not a
   colour the user ever sees — the visible colour is the `background-color`
   token above. Declared as a custom property so the five type selectors above
   share one copy of the payload. */
:root {
  --os-cal-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M16 2v4'/%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
}

/* ══════════════════════════════════════════════════════════════
   SELECT — painted with NO JavaScript (v1.17.0)
   ══════════════════════════════════════════════════════════════
   `.os-field-select` used to leave `appearance: auto`, so the painted select
   existed ONLY behind the `os-select` Stimulus controller. Every bundler-less
   consumer — and every deliberately no-JS surface, like a guest portal that
   runs on plain form posts — got the platform's chevron sitting inside a field
   drawn entirely from our tokens. Same defect the checkbox had in v1.10.0 and
   the date input in v1.13.0, and the same fix: the DEFAULT has to be ours.

   This is the closed state only. `appearance: none` does not touch how the
   option list opens, so mobile still gets the native picker it should have —
   there is no need to gate this by pointer type, and doing so would leave
   touch-capable laptops on the UA chevron.

   THE TINT IS BAKED, and that is a real limitation, not an oversight: a
   `background-image` data-URI cannot read a custom property, and `currentColor`
   does not resolve inside one. The calendar indicator escapes this by being a
   pseudo-element we can `mask` (so it takes `--muted-2`); a `<select>` has no
   such pseudo-element to hang a mask on without masking its text too. The whole
   URL is therefore exposed as `--os-select-chevron` so a product can replace
   the glyph wholesale — geometry and tint together — rather than being stuck
   with ours. The default is `--slate-500` / `--gray-500`, the value `--muted-2`
   resolves to in both shipped themes.

   A consumer that DOES run the `os-select` controller is unaffected: it wraps
   the native select and paints its own trigger, which sits above this. */
:root {
  --os-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.os-field-select {
  -webkit-appearance: none;
  appearance: none;
  background-image: var(--os-select-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 16px 16px;
  /* Room for the glyph plus its gutter: 12 (inset) + 16 (glyph) + 10 (gap).
     Without it a long option value runs under the chevron — which is exactly
     how this shipped before, with the UA arrow jammed against the text. */
  padding-right: 38px;
}

/* Multiple/size selects render a list box, not a closed control — a chevron on
   a list box is a lie. */
.os-field-select[multiple], .os-field-select[size]:not([size="1"]) {
  background-image: none;
  padding-right: 12px;
}

.os-input.is-invalid, .os-field-select.is-invalid, .os-textarea.is-invalid {
  border-color: var(--error);
}
.os-input.is-invalid:focus, .os-field-select.is-invalid:focus, .os-textarea.is-invalid:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--error) 10%, transparent);
}

.os-hint {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 6px;
}
.os-field-error {
  font-size: var(--text-xs);
  color: var(--error);
  margin-top: 6px;
}

/* ── Affixed field — a currency symbol or unit INSIDE the control ─────
   WHY THIS EXISTS. Every money input in the system had the same problem: the
   dollar sign was rendered as a separate label beside the box, so the pair
   read as two things and the number lost its meaning the moment the eye left
   the label. DiveOS solved it locally in its booking wizard by hand-building
   a bordered flex box with the symbol inside it; arena-ops, porting that POS
   flow, needed the same thing at every charge line and had nothing to reach
   for. The affix belongs INSIDE the control's border, before the value — it
   is part of the control, not an adornment next to it.

   This EXTENDS the field anatomy rather than replacing it. Label, hint and
   error are unchanged; only the control itself is wrapped.

     div.os-field
       label.os-label
       div.os-field--affix          the control box — owns border and height
         span.os-field__affix       the symbol
         input.os-input             the value
       p.os-hint

   TRAILING AFFIXES fall out of DOM order for free: put .os-field__affix
   AFTER the input and it renders as a unit suffix — kg, %, /night. Nothing
   below is position-specific except the divider rules, which key off
   adjacency rather than a modifier, so both sides work without a variant.

   ONE GOTCHA, worth stating because the failure is silent. The control box
   carries .os-field--affix ALONE, never `os-field os-field--affix`. The base
   class participates in the 22px field-stacking rule above, so pairing them
   would put inter-field margin inside a single field. The modifier is
   therefore self-contained: it redeclares the box styling instead of
   inheriting it. */
.os-field--affix {
  display: flex;
  align-items: stretch;
  box-sizing: border-box;
  width: 100%;
  /* The affix box takes over the border, background and height the input
     normally draws, so the two read as one control. Values are the input's,
     token for token, and the height comes from the SAME --control-h an
     .os-input reads — an affixed field and a plain one in the same row line
     up, and both follow an .os-density--* scope together. */
  min-height: var(--control-h);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  transition: border-color 100ms ease, box-shadow 100ms ease;
  /* Keeps the affix's tint off the rounded corners. */
  overflow: hidden;
}
.os-field--affix:hover {
  border-color: var(--input-border-hover);
}
/* The ring moves to the WHOLE box: the input inside has no visible edge left
   to ring, and ringing an invisible child would draw a rectangle floating
   inside the control. Not :focus-visible — a text field rings on any focus,
   which is what .os-input already does. */
.os-field--affix:focus-within {
  border-color: var(--input-border-focus);
  box-shadow: var(--shadow-focus);
}
.os-field--affix.is-invalid {
  border-color: var(--error);
}
.os-field--affix.is-invalid:focus-within {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--error) 10%, transparent);
}

/* The symbol. Monospace and muted so it stays subordinate to the number it
   qualifies, and non-selectable so dragging across the value to copy it does
   not carry a stray dollar sign into the clipboard. */
.os-field__affix {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding-inline: 12px;
  font-family: var(--font-mono);
  font-size: var(--control-fs);
  color: var(--text-muted);
  background: var(--surface-alt);
  user-select: none;
  white-space: nowrap;
}
/* A hairline where the affix meets the value, on whichever side it sits.
   Adjacency, not a modifier — see the trailing note above. */
.os-field__affix + * { border-inline-start: 1px solid var(--input-border); }
* + .os-field__affix  { border-inline-start: 1px solid var(--input-border); }

/* A quieter affix for the money case, where a boxed, ruled `$` is heavier
   than the symbol deserves: no fill, no divider, just the glyph tucked
   against the number. This is the DiveOS price-field reading. */
.os-field--affix-flush .os-field__affix {
  background: transparent;
  padding-inline-end: 0;
  border: 0;
}
.os-field--affix-flush .os-field__affix + * { border-inline-start: 0; }
.os-field--affix-flush * + .os-field__affix { border-inline-start: 0; }
.os-field--affix-flush .os-field__affix + .os-input { padding-inline-start: 4px; }

/* The input surrenders its own chrome to the box around it. Descendant
   selector rather than a required extra class: the wrapper is what makes it
   affixed, so the consumer should not have to say so twice. */
.os-field--affix .os-input, .os-field--affix .os-field-select {
  min-height: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  flex: 1 1 auto;
  min-width: 0;
}
.os-field--affix .os-input:hover, .os-field--affix .os-field-select:hover {
  border-color: transparent;
}
.os-field--affix .os-input:focus, .os-field--affix .os-field-select:focus {
  outline: none;
  border-color: transparent;
  box-shadow: none;
}
/* A money value reads right-aligned and tabular so decimal points stack down
   a column of charges. Opt-in — an affixed field is not always money. */
.os-field--affix-numeric .os-input {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.os-field--affix-numeric .os-input::-webkit-outer-spin-button,
.os-field--affix-numeric .os-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.os-field--affix-numeric .os-input[type="number"] {
  appearance: textfield;
  -moz-appearance: textfield;
}

/* Size modifiers redeclare the trio on themselves, pinning them against an
   enclosing density scope — the v1.6.0 back-compat rule. */
.os-field--affix-sm {
  --control-h:  var(--control-h-sm);
  --control-fs: var(--control-fs-sm);
}
.os-field--affix-sm .os-input { padding: 6px 10px; }
/* Intrinsic width, for an affixed field sitting inline in a line-item row
   rather than filling a form column. */
.os-field--affix-inline {
  display: inline-flex;
  width: auto;
}

/* ══════════════════════════════════════════════════════════════
   CHECKBOX / RADIO — painted by DEFAULT (v1.10.0)
   ══════════════════════════════════════════════════════════════
   WHAT CHANGED AND WHY. `.os-checkbox` used to be the NATIVE control with
   `accent-color` set — deliberately, "no custom-painted control". In practice
   that made the default the wrong one. A product that writes the obvious thing,
   `<input type="checkbox" class="os-checkbox">`, got the PLATFORM's checkbox:
   macOS renders a rounded-square with its own tick weight, Windows a hard
   square, and neither matches the 1.5px hairline + `--radius-sm` vocabulary
   every other control in the library is drawn with. The painted component
   existed (`.os-field-check`), but it needs a two-element structure —
   `__input` + a sibling `__box` — so nobody reached for it on a bare input,
   and both products shipped surfaces with platform checkboxes sitting inside
   DS tiles. The default has to be the one you want.

   So `.os-checkbox` / `.os-radio` now paint THEMSELVES, single-element: the
   input is its own box, the tick is a `::after` pseudo-element. **No markup
   change anywhere** — every existing call-site is repainted as-is, which is
   why this is a minor bump and not a major.

   THE ESCAPE HATCH is `--native`: add it to hand a control back to the
   platform (an OS-integrated form, a print sheet, a surface where the user
   expects their own widget). That is the modifier the old default became.

   `.os-field-check` is unchanged and still correct when the box must sit
   beside multi-line label text with its own alignment — it owns the
   composition; these own the control. */
.os-checkbox, .os-radio {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 18px;
  height: 18px;
  margin-top: 3px; /* optical alignment to label cap-height when parent is flex-start */
  padding: 0;
  flex-shrink: 0;
  cursor: pointer;
  border: 1.5px solid var(--border-strong);
  background: var(--surface);
  transition: background-color 120ms var(--ease-standard),
              border-color 120ms var(--ease-standard);
}
.os-checkbox { border-radius: var(--radius-sm); }
.os-radio    { border-radius: var(--radius-pill); }

.os-checkbox:hover:not(:disabled), .os-radio:hover:not(:disabled) {
  border-color: var(--accent);
}

.os-checkbox:checked, .os-radio:checked,
.os-checkbox:indeterminate {
  background: var(--accent);
  border-color: var(--accent);
}

/* The tick is DRAWN, not an image: a rotated corner of a 2-sided box. That
   keeps it token-free (no data-URI carrying a hard-coded stroke colour, which
   is also what the compliance scanner would flag) and lets it inherit the
   sizing above if the control is scaled. */
.os-checkbox::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 46%;
  width: 4px;
  height: 8px;
  border: solid var(--surface);
  border-width: 0 2px 2px 0;
  transform: translate(-50%, -50%) rotate(45deg) scale(0.6);
  opacity: 0;
  transition: opacity 120ms var(--ease-standard), transform 120ms var(--ease-standard);
}
.os-checkbox:checked::after {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(45deg) scale(1);
}

/* Indeterminate is a BAR, not a tick — "some of the children", which a
   half-drawn check cannot say. It replaces the tick rather than joining it. */
.os-checkbox:indeterminate::after {
  opacity: 1;
  width: 9px;
  height: 0;
  border-width: 0 0 2px 0;
  transform: translate(-50%, -50%);
}

.os-radio::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 120ms var(--ease-standard), transform 120ms var(--ease-standard);
}
.os-radio:checked::after { opacity: 1; transform: scale(1); }

.os-checkbox:focus-visible, .os-radio:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--shadow-focus);
}

.os-checkbox:disabled, .os-radio:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* ── Opt back out to the platform control ─────────────────────────────────
   This is exactly what `.os-checkbox` was before v1.10.0. Everything the
   painted version declares is reset here, so the modifier is complete on its
   own and does not depend on declaration order. */
.os-checkbox--native, .os-radio--native {
  appearance: auto;
  -webkit-appearance: auto;
  border: 0;
  background: none;
  border-radius: 0;
  accent-color: var(--accent);
}
.os-checkbox--native::after, .os-radio--native::after { content: none; }
.os-checkbox-label, .os-radio-label {
  display: inline-flex;
  align-items: flex-start;
  gap: 8px;
  font-size: var(--text-sm);
  color: var(--text-primary);
  cursor: pointer;
  min-height: 44px;
}

/* ══════════════════════════════════════════════════════════════
   CANONICAL CHECKBOX COMPONENT  (.diveos-check)
   Painted square box + Lucide check icon. Native input visually
   hidden but fully accessible (focus, keyboard, screen readers).
   Aligns the box to the label's first line.
   ══════════════════════════════════════════════════════════════ */
.os-field-check {
  display: inline-flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-primary);
  line-height: 1.45;
}
.os-field-check__input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  margin: 0;
}
.os-field-check__box {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.os-field-check__box svg {
  width: 12px;
  height: 12px;
  opacity: 0;
  transform: scale(.6);
  transition: opacity 120ms ease, transform 120ms ease;
}
.os-field-check__input:checked + .os-field-check__box {
  background: var(--accent);
  border-color: var(--accent);
}
.os-field-check__input:checked + .os-field-check__box svg {
  opacity: 1;
  transform: scale(1);
}
.os-field-check__input:indeterminate + .os-field-check__box {
  background: var(--accent);
  border-color: var(--accent);
}
.os-field-check__input:indeterminate + .os-field-check__box::after {
  content: "";
  width: 9px;
  height: 1.5px;
  background: #fff;
  border-radius: 1px;
}
.os-field-check__input:focus-visible + .os-field-check__box {
  outline: 2px solid var(--accent-strong);
  outline-offset: 2px;
}
.os-field-check:hover .os-field-check__input:not(:checked) + .os-field-check__box {
  border-color: var(--accent);
}
.os-field-check__input:disabled + .os-field-check__box {
  background: var(--surface-alt);
  border-color: var(--border);
}
.os-field-check__input:disabled ~ .os-field-check__text {
  color: var(--text-muted);
}
.os-field-check:has(.os-field-check__input:disabled) {
  cursor: not-allowed;
}
.os-field-check__text {
  display: block;
}
.os-field-check__text :is(.t, .os-t) {
  font-weight: 500;
  color: var(--text-primary);
}
.os-field-check__text :is(.d, .os-d) {
  display: block;
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
  font-weight: 400;
}

/* Buttons — primary/secondary/ghost/destructive. Used by the FormBuilder
   submit helper + everywhere a form needs an action.
   Matches the inline patterns the conforming surfaces ship today. */
.os-field-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: var(--control-h);   /* 36px at base density — tracks .os-btn */
  padding: 0 16px;            /* literal: this family ships 16px, not the button's 14px */
  font-family: var(--font-sans);
  font-size: var(--control-fs);
  font-weight: 500;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease;
}
.os-field-button:focus-visible {
  outline: 2px solid var(--accent-strong);
  outline-offset: 2px;
}
.os-field-button:disabled, .os-field-button[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}
.os-field-button--primary {
  color: white;
  background: var(--accent);
  border-color: var(--accent);
}
.os-field-button--primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}
.os-field-button--secondary {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--border);
}
.os-field-button--secondary:hover {
  background: var(--surface-alt);
}
.os-field-button--ghost {
  color: var(--text-muted);
  background: transparent;
}
.os-field-button--ghost:hover {
  color: var(--text-primary);
}
/* Destructive — used for "Remove diver" etc. NOT for plain Cancel
   (Cancel uses ghost). Red text on neutral background; full-red bg
   reserved for confirmation modals. */
.os-field-button--destructive {
  color: #991B1B;
  background: var(--surface);
  border-color: var(--border);
}
.os-field-button--destructive:hover {
  background: #FEF2F2;
  border-color: #FECACA;
}

/* Size modifiers re-declare the trio on themselves, which pins them against
   any enclosing .os-density--* scope (tokens/density.css). NOTE --lg here is
   44px, NOT the 42px of .os-btn--lg — an existing divergence between the two
   button families. Left as-is on purpose: unifying them would resize shipped
   buttons, which a token contract must not do. Logged for a future minor. */
.os-field-button--sm { --control-h: var(--control-h-sm); padding: 0 12px; font-size: var(--text-sm); }
.os-field-button--lg { height: 44px; padding: 0 20px; font-size: var(--text-sm); }

/* Action bar that sits below the last form-section. Right-aligned by
   default — primary button trails so the eye lands there last. Stacks
   full-width on narrow screens so taps stay reachable. */
.os-form-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 24px;
}
.os-form-actions__leading {
  margin-right: auto;
}
@media (max-width: 640px) {
  .os-form-actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  .os-form-actions .os-field-button { width: 100%; }
  .os-form-actions__leading { margin-right: 0; }
}

/* Touch targets on mobile per DESIGN.md a11y section. Bump small
   buttons + inputs to 44px so they hit the WCAG AAA threshold without
   requiring callers to remember per-screen overrides. */
@media (max-width: 640px) {
  .os-field-button { min-height: 44px; height: auto; padding: 11px 18px; }
  .os-input, .os-field-select, .os-textarea {
    min-height: 44px;
    padding: 12px 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .os-input, .os-field-select, .os-textarea, .os-field-button {
    transition: none;
  }
}

/* Phase 8 PR5 T7 — fitting-room size pill picker. Sits above each
   slot dropdown in the fitting drawer. Tapping a pill calls the
   fitting-size-filter Stimulus controller to filter the dropdown to
   items in that size. --active styles the currently selected pill. */
:is(.fitting-size-pill, .os-fitting-size-pill) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  min-height: 28px;
  padding: 2px 10px;
  font: 500 12px/1 var(--font-sans, system-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 100ms ease, color 100ms ease, border-color 100ms ease;
}
:is(.fitting-size-pill, .os-fitting-size-pill):hover {
  background: var(--canvas);
}
:is(.fitting-size-pill, .os-fitting-size-pill):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
:is(.fitting-size-pill--active, .os-fitting-size-pill--active) {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

/* Tablet hit-target compliance: bump pill height to 36px on small
   viewports so the fitting room is usable on a 10" tablet without
   stylus-precision taps. */
@media (max-width: 640px) {
  :is(.fitting-size-pill, .os-fitting-size-pill) { min-height: 36px; padding: 4px 14px; font-size: var(--text-sm); }
}

/* ── select2 (super_select) → DiveOS field substrate ──────────────────────
   Bullet Train's theme super_select.css styles select2's chrome with the BT
   palette (base-300 border, primary-500 accent). select2 hides the native
   <select> and renders its OWN box, so .diveos-select on the hidden element
   never shows — the BT-looking box IS this chrome. Restyle it ONCE here to
   match .diveos-input; every super_select field (27 views) inherits. Loaded
   after the theme; !important beats super_select's !important rules. */
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--single, .os-select2-selection--single), :is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--multiple, .os-select2-selection--multiple) {
  min-height: 40px !important;
  padding: 0 !important;
  background: var(--input-bg) !important;
  border: 1px solid var(--input-border) !important;
  border-radius: var(--radius-sm) !important;
  box-shadow: none !important;
  transition: border-color 100ms ease, box-shadow 100ms ease !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--single, .os-select2-selection--single):hover, :is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--multiple, .os-select2-selection--multiple):hover {
  border-color: var(--input-border-hover) !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--single, .os-select2-selection--single) :is(.select2-selection__rendered, .os-select2-selection__rendered) {
  padding: 0 12px !important;
  line-height: 38px !important;
  color: var(--text-primary) !important;
  font-size: var(--text-sm) !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--single, .os-select2-selection--single) :is(.select2-selection__arrow, .os-select2-selection__arrow) {
  height: 38px !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection__placeholder, .os-select2-selection__placeholder) {
  color: var(--input-placeholder) !important;
}
:is(.select2-container--default, .os-select2-container--default):is(.select2-container--open, .os-select2-container--open) :is(.select2-selection--single, .os-select2-selection--single), :is(.select2-container--default, .os-select2-container--default):is(.select2-container--open, .os-select2-container--open) :is(.select2-selection--multiple, .os-select2-selection--multiple), :is(.select2-container--default, .os-select2-container--default):is(.select2-container--focus, .os-select2-container--focus) :is(.select2-selection--single, .os-select2-selection--single), :is(.select2-container--default, .os-select2-container--default):is(.select2-container--focus, .os-select2-container--focus) :is(.select2-selection--multiple, .os-select2-selection--multiple) {
  border-color: var(--input-border-focus) !important;
  box-shadow: 0 0 0 3px var(--focus-ring) !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-dropdown, .os-select2-dropdown) {
  background: var(--surface) !important;
  border: 1px solid var(--input-border) !important;
  border-radius: var(--radius-md) !important;
  box-shadow: var(--shadow-md) !important;
  overflow: hidden !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-results__option, .os-select2-results__option) {
  color: var(--text-primary) !important;
  font-size: var(--text-sm) !important;
  padding: 8px 12px !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-results__option--highlighted, .os-select2-results__option--highlighted), :is(.select2-container--default, .os-select2-container--default) :is(.select2-results__option--highlighted, .os-select2-results__option--highlighted)[aria-selected] {
  background: var(--accent) !important;
  color: #fff !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-results__option, .os-select2-results__option)[aria-selected="true"] {
  background: var(--accent-soft, var(--surface-alt)) !important;
  color: var(--text-primary) !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-search--dropdown, .os-select2-search--dropdown) :is(.select2-search__field, .os-select2-search__field) {
  border: 1px solid var(--input-border) !important;
  border-radius: var(--radius-sm) !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-search--dropdown, .os-select2-search--dropdown) :is(.select2-search__field, .os-select2-search__field):focus {
  border-color: var(--input-border-focus) !important;
  box-shadow: 0 0 0 3px var(--focus-ring) !important;
  outline: none !important;
}
:is(.select2-container--default, .os-select2-container--default) :is(.select2-selection--multiple, .os-select2-selection--multiple) :is(.select2-selection__choice, .os-select2-selection__choice) {
  background: var(--accent-soft, var(--surface-alt)) !important;
  border: 1px solid var(--hairline) !important;
  border-radius: var(--radius-sm) !important;
  color: var(--text-primary) !important;
  margin-top: 7px !important;
}

/* ── Bullet Train .button family → DiveOS buttons ─────────────────────────
   The arena theme's .button / .button-secondary / .button-alternative /
   .button-red classes (BT palette: primary-500, base-300/500) back the file-
   upload buttons (active_storage "Upload New Image"), the buttons-toggle
   field, and any BT-rendered action control. Restyle the family ONCE here so
   every instance matches .diveos-button, no per-view edits. Loaded after the
   theme; ties on specificity, wins on source order. */
:is(.button, .os-button), :is(.button-secondary, .os-button-secondary), :is(.button-alternative, .os-button-alternative) {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: var(--radius-sm);
  box-shadow: none;
  transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease;
}
/* primary */
:is(.button, .os-button) {
  color: #fff;
  background: var(--accent);
  border: 1px solid var(--accent);
}
:is(.button, .os-button):hover {
  color: #fff;
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}
/* secondary / alternative (outlined) — the file-upload button look */
:is(.button-secondary, .os-button-secondary), :is(.button-alternative, .os-button-alternative) {
  color: var(--text-primary);
  background: var(--surface);
  border: 1px solid var(--border);
}
:is(.button-secondary, .os-button-secondary):hover, :is(.button-alternative, .os-button-alternative):hover {
  color: var(--text-primary);
  background: var(--surface-alt);
  border-color: var(--border);
}
/* danger */
:is(.button, .os-button):is(.button-red, .os-button-red), :is(.button-red, .os-button-red) {
  color: #fff;
  background: var(--red-600);
  border-color: var(--red-600);
}
:is(.button, .os-button):is(.button-red, .os-button-red):hover, :is(.button-red, .os-button-red):hover {
  color: #fff;
  background: var(--red-700);
  border-color: var(--red-700);
}

/* color-swatch buttons (color picker `.button-color`). Neutral DS shell
   matching the .button-alternative look; the swatch fill comes from the
   inline background-color style. Selected state (the BT `ring-2`) reads
   through --accent so the active swatch picks up the DS accent ring. */
:is(.button-color, .os-button-color) {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: none;
}
:is(.button-color, .os-button-color):is(.ring-2, .os-ring-2), :is(.button-color, .os-button-color):focus-visible {
  --tw-ring-color: var(--accent);
  outline: none;
}

/* ── File-upload widget chrome (file_field + active_storage_image) ─────────
   The ejected upload partials render a progress bar (`bg-primary-500`) and a
   thumbnail/progress panel (`bg-base-100`) straight from the BT palette.
   Retoken both centrally — the partials keep their markup, these win on
   source order. */
:is(.file-field, .os-file-field) [data-fields--file-field-target="progressBar"] {
  background: var(--accent);
}
:is(.file-field, .os-file-field) :is(.bg-base-100, .os-bg-base-100) {
  background: var(--surface);
}


/* ===== os_ds/components/gate-bar.css ===== */
/* os-gate-bar — batch completion gate: group status on the left, actions grouped right,
 * primary rightmost. The primary stays disabled until every member of the group is done.
 * Source: `.partybar/.pbnum/.pbsub/.pbcta` in reg-card-hybrid.html.
 * Composes `.os-btn` / `.os-btn--primary` for its actions; the disabled state below is a
 * coherent, non-shouting "not yet" (dimmed + not-allowed), never a red error.
 */

.os-gate-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
}

.os-gate-bar__body {
  min-width: 0;
}

.os-gate-bar__title {
  font-size: var(--text-md);
  line-height: var(--leading-md);
  font-weight: 680;
  color: var(--ink);
}

.os-gate-bar__sub {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted);
  margin-top: 2px;
}

.os-gate-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: none;
}

/* Gate not yet satisfied. */
.os-gate-bar__actions [disabled],
.os-gate-bar__actions [aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  box-shadow: none;
}

@media (max-width: 720px) {
  .os-gate-bar {
    flex-direction: column;
    align-items: stretch;
  }

  .os-gate-bar__actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }
}


/* ===== os_ds/components/header_band.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   Shared component layer — header band + toolbar primitives.

   Consolidates two patterns that were previously repeated as inline class
   strings / duplicated page-scoped CSS across the app:

   1. `.dv-page-band` — the full-bleed white header band that sits at the top
      of (and between sections of) every index/detail page. Previously every
      band was written inline as
        `bg-surface border-b border-hairline dv-full-bleed …`
      on the wrapper div. This sheet folds the three shared invariants
      (surface background, bottom hairline, full-bleed horizontal breakout)
      into one named class.

      IMPORTANT — parity note: the *vertical* padding of each band is NOT
      uniform across pages (head bands use `-mt-10 pt-8 pb-6`, stat strips use
      `py-[22px]` on Today but `py-[18px]` on Dives, toolbars use `py-2` or
      `py-3`, and the bookings tab nav supplies its own padding via
      `.page-tabs`). To guarantee ZERO visual change, `.dv-page-band` owns ONLY
      the shared invariants; callers KEEP their existing per-band padding /
      margin Tailwind utilities. No `--head` / `--stats` padding modifiers are
      defined because that would force a single padding value and shift pixels
      on pages that use a different one.

   2. `.dv-toolbar`, `.dv-divider`, `.chip` (+ `.is-on` / `.count`) — toolbar
      filter primitives that were duplicated byte-for-byte in
      `account/divers.css` and `account/team.css`. Promoted here so both pages
      (and any future page) consume one canonical definition.

   Token-driven; consumes the semantic aliases from tokens.css. Loaded after
   patterns.css (see application.css).

   Full-bleed is the shell contract: the consuming shell defines --bleed-w /
   --bleed-mx / --bleed-pad on its content column (and makes the pane a query
   container). Each is read WITH a fallback, so a consumer that defines none
   degrades to a plain, non-bleeding band instead of a broken layout.
   ───────────────────────────────────────────────────────────────────── */

/* ── Page band (full-bleed white header band) ─────────────────────────
   Shared invariants only. Callers add their own vertical padding/margin
   utilities (e.g. `-mt-10 pt-8 pb-6`, `py-[18px]`, `py-[22px]`, `py-2`,
   `py-3`) to preserve each band's exact rhythm.

   The horizontal breakout is baked in: it extends the band to the content-pane
   edges via --bleed-*, then re-insets its content to the centered column. So
   callers add nothing alongside `.dv-page-band`. */
.os-page-band {
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
  width: var(--bleed-w, auto);                 /* full-bleed background to the pane edges */
  margin-inline: var(--bleed-mx, 0);
  padding-inline: var(--bleed-pad, 0);      /* inner content aligns to the tier column */
}

/* ── Page-band tab nav (band 2) ───────────────────────────────────────
   The second .dv-page-band on a detail page: a full-bleed sticky tab row.
   Promoted (unscoped) from account/bookings.css so every detail header
   (booking, dive trip, …) shares ONE tab component. `.dv-tabnav` pins the
   band under band 1; `.page-tabs`/`.page-tab`/`.page-tab-count` style the
   underline tabs + badge counts. */
.os-tabnav { position: sticky; top: 0; z-index: 20; }
:is(.page-tabs, .os-page-tabs) {
  display: flex;
  gap: 2px;
  flex-shrink: 0;
  /* Narrow screens: scroll the tab row horizontally rather than wrap/clip
     ("Run →" onto two lines, "Return" off the edge). No-op when they fit. */
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
:is(.page-tabs, .os-page-tabs)::-webkit-scrollbar { display: none; }
:is(.page-tab, .os-page-tab) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 12px 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--muted);
  border: none;
  background: none;
  cursor: pointer;
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--t-fast, 0.15s) ease;
}
:is(.page-tab, .os-page-tab):hover { color: var(--ink); }
/* Keyboard focus ring — tabs are interactive (<a>/<button>); without this,
   keyboard users get no visible focus. `--shadow-focus` is ours (tokens.css);
   `--radius-xs` is NOT a token os-ds owns, so it carries a fallback — otherwise
   the ring renders square in any consumer that doesn't happen to define it. */
:is(.page-tab, .os-page-tab):focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-xs, 4px);
}
:is(.page-tab, .os-page-tab).is-active, :is(.page-tab, .os-page-tab)[aria-selected="true"] {
  color: var(--accent) !important;
  border-bottom-color: var(--accent);
  font-weight: 600;
}
:is(.page-tab-count, .os-page-tab-count) {
  font-size: var(--text-2xs);
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--canvas);
  border: 1px solid var(--hairline);
  color: var(--muted-2);
}
:is(.page-tab, .os-page-tab).is-active :is(.page-tab-count, .os-page-tab-count), :is(.page-tab, .os-page-tab)[aria-selected="true"] :is(.page-tab-count, .os-page-tab-count) {
  color: var(--accent);
  border-color: var(--accent-border);
}
/* A thin separator between in-page tabs and a nav-away tab (e.g. Run opens
   the run flow on its own surface). */
:is(.page-tab-div, .os-page-tab-div) {
  align-self: center;
  width: 1px;
  height: 18px;
  margin: 0 6px;
  background: var(--hairline);
  flex-shrink: 0;
}

/* ── Toolbar (filter chips + search) ──────────────────────────────────
   Promoted from account/divers.css + account/team.css (identical copies). */
/* ── Page-band typography — canonical h1, h2, sub-copy ───────────────
   Every page header h1 inside .dv-page-band inherits the WL-exact style
   automatically. Views just write <h1>Title</h1> — no Tailwind utilities
   needed on the element. Same for the muted sub-copy <p>. */
.os-page-band h1 {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.025em;
  color: var(--ink);
}

.os-page-band h1 + p, .os-page-band :is(.page-sub-copy, .os-page-sub-copy) {
  font-size: var(--text-sm);
  color: var(--muted);
  margin-top: 4px;
}

.os-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Vertical separator between chip groups inside a toolbar. */
.os-toolbar .os-divider {
  width: 1px;
  height: 22px;
  background: var(--hairline);
  margin: 0 4px;
  flex-shrink: 0;
}

/* ── Chip (filter pill) ───────────────────────────────────────────────
   Promoted from account/divers.css + account/team.css (identical copies). */
:is(.chip, .os-chip) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 28px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--hairline);
  background: var(--surface);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background 150ms ease-out, border-color 150ms ease-out;
}
:is(.chip, .os-chip):hover { background: var(--canvas); }
:is(.chip, .os-chip).is-on {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--accent);
}
:is(.chip, .os-chip) :is(.count, .os-count) {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
  margin-left: 2px;
}
:is(.chip, .os-chip).is-on :is(.count, .os-count) { color: var(--accent); }

/* ─────────────────────────────────────────────────────────────────────
   Shared index-page primitives — page header, search box, table wrap.

   Previously duplicated byte-for-byte in account/divers.css and
   account/team.css (and consumed by account/gear.css + account/fleet.css).
   Promoted here so the canonical definition lives in one place and the next
   `thead th`-style correction only has to happen once. Loaded before all
   account/* page sheets, so page-scoped variants (e.g. team grouped-row
   rules) still cascade on top. ZERO visual change vs the duplicated copies.
   ───────────────────────────────────────────────────────────────────── */

/* ── Search box ───────────────────────────────────────────────────── */
:is(.search-wrap, .os-search-wrap) {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 32px;
}
:is(.search-wrap, .os-search-wrap) svg, :is(.search-wrap, .os-search-wrap) [data-lucide] {
  position: absolute;
  left: 10px;
  width: 14px;
  height: 14px;
  color: var(--muted-2);
  pointer-events: none;
}
:is(.search-wrap, .os-search-wrap) input {
  width: 100%;
  height: 32px;
  padding: 0 10px 0 30px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  background: var(--surface);
  font-size: var(--text-xs);
  color: var(--ink);
}
:is(.search-wrap, .os-search-wrap) input::placeholder { color: var(--muted-2); }
:is(.search-wrap, .os-search-wrap) input:focus {
  outline: none;
  border-color: var(--hairline-strong);
}

/* ── Table wrap ───────────────────────────────────────────────────── */
.os-table-wrap {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  /* BL-1a — clip vertically (keeps the rounded-corner mask), scroll horizontally
     at every breakpoint so no table clips/squeezes on narrow viewports. The
     border-radius still clips because overflow is non-visible on both axes. */
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}
/* 14px body-cell floor. Set on the WRAPPER so it's INHERITED by cells — any cell
   that declares its own size (.dv-table__sub → 12px, Tailwind text-xs, thead th)
   overrides inheritance regardless of specificity, but a bare/super-scaffolded
   <td> with no sizing class inherits 14px instead of leaking to the 16px root.
   Complements the .dv-table__* classes (components/table.css); this catches the
   tables that don't use them (BT super-scaffold, box.table, hand-rolled cells). */
.os-table-wrap, :is(.box-table, .os-box-table) { font-size: var(--text-sm); }
.os-table-wrap table {
  min-width: 100%;
  border-collapse: collapse;
}
.os-table-wrap thead tr {
  background: var(--canvas);
  border-bottom: 1px solid var(--hairline);
}
.os-table-wrap thead th {
  /* Header cells own vertical rhythm just like body `td` below (14px ≈ one step
     tighter than the 16px body row, matching the opt-in `.os-table__th`). Without
     it the header row collapses to its ~10px text line and reads far shorter than
     every body row. */
  padding-top: 14px;
  padding-bottom: 14px;
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray-600);  /* WL table-header color (#4B5563); was --muted (gray-500), one step too light */
}
.os-table-wrap tbody tr { border-bottom: 1px solid var(--hairline-2); }
.os-table-wrap tbody tr:last-child { border-bottom: none; }
.os-table-wrap tbody tr:hover { background: var(--sky-50); }
/* Row/cell links (often dv_link_to names) shouldn't underline on hover inside
   a table — the row hover tint is the affordance; underline reads as noise. */
.os-table-wrap td a:hover, :is(.box-table, .os-box-table) td a:hover { text-decoration: none; }
/* Default cell rhythm — list-table rows are 1rem (16px = --space-4) top+bottom.
   This OVERRIDES per-cell py-* utilities (specificity 0,1,1 > a plain .py-3),
   so the row height is uniform across every list table from the component, not
   from scattered per-cell classes. Dense embedded sub-tables opt out with
   .dv-table--compact (8px). */
.os-table-wrap td { padding-top: var(--space-4); padding-bottom: var(--space-4); }
.os-table-wrap.os-table--compact td, .os-table--compact .os-table-wrap td { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.os-table-wrap td:first-child, .os-table-wrap th:first-child { padding-left: 16px; }
.os-table-wrap td:last-child, .os-table-wrap th:last-child { padding-right: 16px; }
/* NOTE (shell-agnostic): a consuming app whose table framework wraps index
   tables and leaks column insets onto .dv-table-wrap (e.g. Bullet Train's
   `.bulk-actions` "identity column" padding) should reset those in its OWN
   stylesheet — that's a product-framework concern, not part of the shared
   component. The gem stays free of framework-specific ancestor selectors. */

/* Reorderable tables (drag-handle first column). The leading handle cell is a
   narrow grip, not real content — the first-child 16px inset would push the
   whole table in past the canonical 12px (px-3) gutter and, on the header,
   misalign the labels against their data columns. Pin both the handle <td> and
   its matching leading <th> to 12px so the table's left content lines up at the
   canonical gutter, header above row. */
.os-table-wrap:has(tbody [data-sortable-target="handle"]) th:first-child, .os-table-wrap:has(tbody [data-sortable-target="handle"]) td:first-child { padding-left: var(--space-3); }

/* ── Box table ────────────────────────────────────────────────────────
   The shared themes/light/_box partial already supplies a bordered
   surface card. So `box.table` tables get the canonical .dv-table-wrap
   LOOK (canvas header band, uppercase 10px th, 12px td rhythm, hairline
   rows, hover surface-alt) WITHOUT a second outer border/radius. Mirrors
   the thead/td/hover rules above; intentionally omits the .dv-table-wrap
   outer border + radius (the box owns that chrome). */
/* BL-1a — box.table wrappers scroll horizontally too (the responsive_tables.css
   layer that used to supply this was deleted as dead card-stack CSS). */
:is(.box-table, .os-box-table) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
:is(.box-table, .os-box-table) table { min-width: 100%; border-collapse: collapse; }
:is(.box-table, .os-box-table) thead tr {
  background: var(--canvas);
  border-bottom: 1px solid var(--hairline);
}
:is(.box-table, .os-box-table) thead th {
  /* Header rhythm — mirrors `.os-table-wrap thead th`; see the note there. */
  padding-top: 14px;
  padding-bottom: 14px;
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray-600);
}
:is(.box-table, .os-box-table) tbody tr { border-bottom: 1px solid var(--hairline-2); }
:is(.box-table, .os-box-table) tbody tr:last-child { border-bottom: none; }
:is(.box-table, .os-box-table) tbody tr:hover { background: var(--sky-50); }
:is(.box-table, .os-box-table) td { padding-top: var(--space-4); padding-bottom: var(--space-4); }
:is(.box-table, .os-box-table) td:first-child, :is(.box-table, .os-box-table) th:first-child { padding-left: 16px; }
:is(.box-table, .os-box-table) td:last-child, :is(.box-table, .os-box-table) th:last-child { padding-right: 16px; }

/* ════════════════════════════════════════════════════════════════════════
   Unified list/index page header (WL handoff — "header-applied-boards").
   ONE general header: row 1 .dv-page-head (title + actions) · row 2
   .dv-page-tools (scope tabs + facet chips + trailing). Replaces the old
   multi-band .dv-page-band stack. Reuses .dv-btn*, .search-wrap, .chip.
   Scope tabs = mutually-exclusive segmentation (one .is-on); chips = facets.
   ════════════════════════════════════════════════════════════════════════ */
/* Row 1 — title bar. White, full-bleed to the pane edges (same calc as
   .dv-full-bleed / .dv-page-band — keep in sync), bleeding up through
   the pane's top padding (via --shell-pad-top) so the white starts at the pane top. */
.os-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
  margin-top: calc(-1 * var(--shell-pad-top, 40px));
  width: var(--bleed-w, auto);
  margin-inline: var(--bleed-mx, 0);
  padding: 32px var(--bleed-pad, 0) 18px;
}
/* Mobile (<1023px): compact single-row header + auto-hiding page tools.
   SHELL-AGNOSTIC nav clearance: a shell that renders a FIXED mobile nav toggle
   (a hamburger overlapping the top-left) sets `--dv-nav-gutter` (horizontal
   inset, e.g. 64px) and `--dv-nav-gutter-top` (vertical clearance for a
   breadcrumb sitting under the toggle, e.g. 72px). Both default to 0, so a shell
   with no fixed toggle gets no phantom gutter — the gem assumes no shell. */
@media (max-width: 1023px) {
  /* List header (row 1): content clears the shell nav toggle via the gutter. */
  .os-page-head { padding-top: 16px; padding-left: var(--dv-nav-gutter, 0); }

  /* Detail headers carry a breadcrumb as their first line — extra top clearance
     so breadcrumb + title stack under the toggle (0 when no shell toggle). */
  .os-page-band { padding-top: var(--dv-nav-gutter-top, 0); }
  .os-page-band ~ .os-page-band { padding-top: var(--space-2, 8px); }
  nav.os-page-band { padding-top: var(--space-3, 12px); padding-left: var(--dv-nav-gutter, 0); }

  /* Page tools (row 2): at the page top the row sits below the header, clear of
     the toggle. The `page-tools` controller adds `--pinned` (applies the nav
     gutter) only while STUCK at the top, and `--hidden` to slide it out of view
     on scroll-down. Transform-based so hiding never reflows the page. */
  .os-page-tools {
    transition: transform .24s var(--ease-out-quart, ease), padding-left .12s ease;
  }
  .os-page-tools.os-page-tools--hidden { transform: translateY(-110%); }
  .os-page-tools.os-page-tools--pinned { padding-left: var(--dv-nav-gutter, 0); }
}
.os-page-head h1, h1.os-page-head__title {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.025em;
  color: var(--ink);
  margin: 0;
}
.os-page-head__sub { font-size: var(--text-sm); color: var(--muted); margin-top: 4px; }

/* ── Head lead + title row ────────────────────────────────────────────
   The band is title-cluster (left) vs actions (right). `__lead` is that left
   cluster; `__titlerow` puts the h1 and its IDENTITY CHIPS (VIP, returning,
   lifecycle status) on ONE line.

   Both were previously undefined, so a consumer that used them got the browser
   default: the block-level h1 took the full width and pushed every chip onto
   its own line under the name. Additive — no existing markup in the DiveOS
   consumer references either class, so this changes nothing there. */
.os-page-head__lead {
  flex: 1 1 auto;
  min-width: 0;                        /* let a long name truncate, not overflow */
}
.os-page-head__titlerow {
  display: flex;
  align-items: center;                 /* chips optically centre on the title */
  flex-wrap: wrap;                     /* many chips wrap as a group, not per-chip */
  gap: 10px;
  min-width: 0;
}
.os-page-head__actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
/* Optional breadcrumb inside the head band (Settings/catalogue pages). It sits
   at the top of the title block, above the <h1>. The breadcrumb component is
   margin-free, so the band owns this gap — keeps the breadcrumb IN the header
   band (no stray wrapper div per page). */
.os-page-head .os-breadcrumb { margin-bottom: 10px; }

/* Below the two-column comfort width the actions (search · view toggle · CTA)
   can't fit beside the title and — being flex-shrink:0 — bleed off the right
   edge. Wrap them under the title as a full-width row: the search grows to fill,
   the toggle + CTA wrap beneath it. Desktop keeps the title/actions on one line.

   CONTAINER query (not viewport): the header lives in the content pane, so
   it must react to the PANE width (viewport minus the 240px sidebar), not the
   window. A viewport query left a dead band — e.g. at a 1024px window the pane
   is only ~784px, narrow enough to crush the sub/description and bleed the
   actions off the edge, yet a `max-width: 880px` window query never fired. The
   container query fires at pane ≤880 in every sidebar state (shown/collapsed). */
@container (max-width: 880px) {
  .os-page-head { flex-wrap: wrap; row-gap: 16px; }
  .os-page-head > :first-child { flex: 1 1 100%; }
  .os-page-head__actions {
    width: 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
  }
  /* A search input grows to fill the row (it already carries flex-grow); a lone
     CTA button stays its natural width — never stretch a button full-bleed. */
  .os-page-head__actions > :is(.relative, .os-relative):first-child, .os-page-head__actions > form:first-child { flex: 1 1 240px; max-width: none; }
  .os-page-head__actions > :is(.relative, .os-relative):first-child :is(.max-w-sm, .os-max-w-sm), .os-page-head__actions :is(.search-wrap, .os-search-wrap) { max-width: none; }
}

/* Collapsible search wrapper. Desktop/tablet: the field shows inline (grows to
   its max-w-sm) and the toggle button is hidden; the phone rules below flip it. */
.os-hsearch { display: flex; align-items: center; flex-grow: 1; min-width: 0; }
.os-hsearch__field { flex-grow: 1; min-width: 0; }
/* Clear the leading search icon. A product input's padding shorthand can override
   a utility `pl-8`, so pad the field input here (higher specificity). */
.os-hsearch__field input { padding-left: 2rem; }
.os-hsearch__toggle { display: none; }

/* PHONES ONLY (≤640): compact single row — title + icon actions. The subtitle
   is dropped and each action button collapses to its icon (the bare-text label
   is zeroed via font-size:0; the Lucide SVG keeps its explicit width/height, so
   the glyph survives). Scoped to phones on purpose: the hamburger appears at
   ≤1023, but the TABLET range (641–1023) has room for the full header, so it
   keeps its subtitle + full-text actions (via the container-query stack above).
   Wide controls (search, view toggle) still wrap. */
@media (max-width: 640px) {
  /* Top-align so the title's FIRST line rides the hamburger band (below) and a
     wrapped 2nd line flows beneath it — instead of the whole title cluster being
     centred as a unit, which drops the title ~7px below the fixed toggle. */
  .os-page-head { flex-wrap: wrap; align-items: flex-start; row-gap: 8px; column-gap: 8px; }
  .os-page-head > :first-child { flex: 1 1 auto; min-width: 0; }
  /* The shell hamburger is a 44px touch target pinned at top:12 (optical centre
     y=34). Drop the title line and the icon actions into a matching 44px band so
     both sit level with the toggle, not with the taller header box. */
  .os-page-head h1, h1.os-page-head__title { min-height: 44px; display: flex; align-items: center; }
  .os-page-head__sub { display: none; }
  .os-page-head__actions {
    width: auto;
    flex: 0 0 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 6px;
    min-width: 0;
    min-height: 44px;                  /* ride the hamburger band, level with title */
    align-items: center;
  }
  /* Collapsible search: flatten any wrapper (search form / filter div) around
     the .dv-hsearch so it participates in the actions flex directly, then show a
     40px search-icon button and hide the field until the search-collapse
     controller opens it. When open, the actions take a full row so the field
     can breathe. Generic `:has(.dv-hsearch)` — not tied to any one controller. */
  .os-page-head__actions form:has(.os-hsearch), .os-page-head__actions > div:has(.os-hsearch) { display: contents; }
  .os-hsearch { flex: 0 0 auto; }
  .os-hsearch__toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; flex-shrink: 0;
    border: 1px solid var(--hairline); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--muted); cursor: pointer;
  }
  .os-hsearch__field { display: none; }
  .os-hsearch.is-open { flex: 1 1 100%; }
  .os-hsearch.is-open .os-hsearch__toggle { display: none; }
  .os-hsearch.is-open .os-hsearch__field { display: block; }
  .os-page-head__actions:has(.os-hsearch.is-open) { flex: 1 1 100%; justify-content: flex-start; }
  /* Fallback for pages whose header search isn't the collapsible .dv-hsearch yet
     (e.g. Leads): give the search form its own full-width row so it doesn't
     overflow — the pre-collapse behaviour. */
  .os-page-head__actions:has(input):not(:has(.os-hsearch)) { flex: 1 1 100%; justify-content: flex-start; }
  .os-page-head__actions > form:has(input) { flex: 1 1 100%; min-width: 0; }
  .os-page-head__actions input { min-width: 0; }
  .os-page-head__actions .os-btn {
    gap: 0;
    width: 40px;
    min-width: 40px;
    padding-left: 0;
    padding-right: 0;
    justify-content: center;
    font-size: 0;
  }
  .os-page-head__actions .os-btn :is(.lucide, .os-lucide), .os-page-head__actions .os-btn > svg { width: 18px; height: 18px; font-size: initial; }
  /* The date stepper (< >) matches the 40px action-button height so the whole
     action cluster reads as one row (it shipped at 34/36px, shorter than the
     buttons). */
  .os-page-head__actions .os-pager { height: 40px; }
  .os-page-head__actions .os-pager :is(.os-pager__step, .os-pager__date) { height: 40px; }
  /* The view toggle collapses to icon-only segments (each option carries a
     Lucide icon). */
  .os-page-head__actions :is(.segmented__option, .os-segmented__option) span { display: none; }
  .os-page-head__actions :is(.segmented__option, .os-segmented__option) { padding-left: 11px; padding-right: 11px; }
}

/* Row 2 — scope / tools bar. White, full-bleed (matches row 1).
   Sticky: the scope/tools row pins to the top on scroll (the title row above
   scrolls away under it), so the filters stay reachable down a long list. */
.os-page-tools {
  display: flex;
  align-items: center;
  /* Wrap as a unit below ~1440px: the anchor-stat + scope-tabs + trailing note
     can exceed the pane width (Ledger has 6 money tabs). Without this the row
     stays one line and the scope-tabs wrap internally, leaving a stray tab on
     line 2 while the note floats top-right. row-gap keeps wrapped rows legible. */
  flex-wrap: wrap;
  row-gap: 10px;
  gap: 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
  width: var(--bleed-w, auto);
  margin-inline: var(--bleed-mx, 0);
  padding: 12px var(--bleed-pad, 0);
  position: sticky;
  top: 0;
  z-index: 20;
}
/* Body gap — consistent 2rem between a page header and the content below,
   ACROSS ALL ARCHETYPES (#75). The list header is two full-bleed rows:
   row-1 `.dv-page-head`, optional row-2 `.dv-page-tools`; content follows
   whichever is last, the two header rows stay flush. Show/edit/form pages use
   `.page-header` (form/settings header) or a bare `.dv-page-band` (e.g.
   booking show) — those got no top gap before, so the first body element
   stacked flush against the header. The selectors below give every archetype
   the same 2rem gap. padding-top: 0 neutralises `pt-*` on content wrappers so
   the gap is exactly 2rem everywhere.

   Note: `.hero-show__band` (entity show pages) owns its own margin-bottom:2rem
   in patterns.css, so it is intentionally NOT matched here.

   We key the show/edit/form gap on `.page-header` (the form/settings header,
   which is `.dv-page-band.page-header`), NOT on a bare `.dv-page-band`. A bare
   band (e.g. booking show) is followed by a control-row WRAPPER — the
   `tab-panel` controller div that holds the band-2 tab nav — and that wrapper
   must stay flush; its internal nav→panel spacing lives in bookings.css. So a
   bare-`.dv-page-band` selector wrongly pushed the tab nav down 2rem (#75
   regression) and is deliberately absent.

   The `.dv-page-head` row excludes `form:has(> .dv-page-tools)` because a list's
   filter bar is often wrapped in a search `<form>` (e.g. Team) — without the
   exclusion the form (and the sticky tools bar inside it) gets a stray 2rem gap
   instead of sitting flush under the title. */
/* Underline tabs inside a toolbar band — a `.page-tabs` group sits alongside
   vertically-centered controls (search / filters / view toggle) in the same
   `.dv-page-tools` row, yet its active-tab underline must land flush on the
   band's BOTTOM hairline, not float at the centered mid-line. Stretch the group
   to the full band height (negative block margins cancel the band's 12px
   vertical padding) and drop the tabs' own vertical padding so the 2px
   border-bottom sits on the band edge. This promotes a pattern products were
   re-hacking locally (e.g. HotelOS `.fo-cstrip .fo-tabs`); a standalone
   `.dv-tabnav` band needs none of it (no centered siblings). */
.os-page-tools > :is(.page-tabs, .os-page-tabs) {
  align-self: stretch;
  flex-wrap: nowrap;
  flex-shrink: 0;
  margin-block: -12px;
}
.os-page-tools > :is(.page-tabs, .os-page-tabs) > :is(.page-tab, .os-page-tab) {
  align-items: center;
  padding-block: 0;
  margin-bottom: 0;
}

.os-page-tools + :not(.os-page-tools):not(:has(.os-page-tools)), .os-page-head + :not(.os-page-tools):not(.os-page-head):not(:has(.os-page-tools)), :is(.page-header, .os-page-header) + :not(.os-page-tools):not(.os-page-head):not(.os-page-band):not(:has(.os-page-tools)) { margin-top: 2rem; padding-top: 0; }

/* `<turbo-frame>` is an unknown element, so it defaults to `display: inline` —
   and vertical margin does nothing on an inline box. Any page whose first body
   element is a frame (a drawer, a lazy panel, a destructive confirm) therefore
   MATCHED the body-gap rule above and still rendered flush against the header.
   It looked like the gap rule was broken; the gap was landing on a box that
   cannot take it. Seen in both consuming apps.

   The `[hidden]` guard below is NOT optional. `[hidden] { display: none }` lives
   in the USER-AGENT stylesheet, and cascade origin is resolved before
   specificity — so this author-origin type selector beats it, and
   `<turbo-frame hidden>` RENDERS. That is the standard turbo-rails idiom for a
   lazy modal/drawer shell, so the rule above silently arms a landmine in every
   consumer without it.

   It also fails ASYMMETRICALLY, which is worse than failing outright: a Tailwind
   consumer gets Preflight's own `[hidden] { display: none }` at (0,1,0), which
   re-wins and masks the bug, while a consumer without Preflight shows it. Same
   markup, two behaviours, gem as the cause. Verified live in the HotelOS
   consumer: a hidden <turbo-frame> computed `display: block` and rendered, while
   a hidden <div> and <span> correctly computed `none`.

   Restore the guard rather than scoping the rule: it is already shipped and
   ~20 frames across both apps now depend on being block, so narrowing it would
   be a behaviour change wearing a bugfix's clothes. */
turbo-frame {
  display: block;
}
turbo-frame[hidden] {
  display: none;
}

.os-spacer { flex: 1 1 auto; }
.os-vrule { width: 1px; height: 22px; background: var(--hairline); flex-shrink: 0; }

/* Tools select — token-clean native <select> for .dv-page-tools filter rows.
   Native control kept (submit/keyboard); appearance stripped and a Lucide
   chevron overlaid. Matches .search-wrap height/typography. */
.os-tools-select { position: relative; display: inline-flex; align-items: center; }
.os-tools-select select {
  appearance: none;
  -webkit-appearance: none;
  height: 32px;
  min-width: 10rem;
  padding: 0 30px 0 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  background: var(--surface);
  font-size: var(--text-xs);
  color: var(--ink);
  cursor: pointer;
}
.os-tools-select select:hover { border-color: var(--hairline-strong); }
.os-tools-select select:focus { outline: none; border-color: var(--hairline-strong); }
.os-tools-select__chev {
  position: absolute;
  right: 10px;
  color: var(--muted-2);
  pointer-events: none;
}
.os-page-tools__note {
  font-size: var(--text-xs);
  color: var(--muted-2);
  font-family: var(--font-mono, ui-monospace, monospace);
  white-space: nowrap;
}
.os-toolbtn {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: 8px;
  color: var(--muted); cursor: pointer; text-decoration: none;
}
.os-toolbtn:hover { background: var(--canvas); color: var(--ink); }

/* Scope tabs — count/amount-forward, mutually-exclusive (one .is-on) */
.os-scope-tabs { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
/* Ledger tools row (the only one with an anchor-stat + trailing note) restacks
   below ~1400px instead of leaving orphan rows: the scope-tabs take their own
   full-width line, and the "N transactions" note pulls UP next to "Today net"
   on the first row (spacer hidden). Wide layout is unchanged (one line, note
   pushed right by the spacer). Scoped via :has() so other tools rows (divers /
   team facet bars) are untouched. */
@media (max-width: 1400px) {
  .os-page-tools:has(.os-anchor-stat) > .os-scope-tabs { flex: 0 0 100%; order: 3; }
  .os-page-tools:has(.os-anchor-stat) > .os-spacer { display: none; }
  .os-page-tools:has(.os-anchor-stat) > .os-page-tools__note { order: 1; }
}
/* Number-forward pill: mono count + label on one line, in a fully-rounded
   hairline pill. Active = clean white chip with a soft lift + accent number
   (was an accent-soft fill). Matches the mobile filter-sheet pills 1:1. */
.os-scope-tab {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 6px 13px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--hairline);
  cursor: pointer; font: inherit; text-decoration: none; white-space: nowrap;
}
.os-scope-tab:hover { background: var(--canvas); }
.os-scope-tab__n { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--text-md); font-weight: 600; color: var(--ink); letter-spacing: -0.01em; }
.os-scope-tab__l { font-size: var(--text-sm); color: var(--muted); }
.os-scope-tab.is-on { background: var(--surface); border-color: var(--accent-border, rgba(3,105,161,0.18)); box-shadow: 0 1px 2px rgba(15,23,42,0.08); }
.os-scope-tab.is-on .os-scope-tab__n { color: var(--accent); }
.os-scope-tab.is-on .os-scope-tab__l { color: var(--ink); }
.os-scope-tab__n--neg  { color: var(--error, #c13028); }
.os-scope-tab__n--zero { color: var(--muted-2); }

/* Scope-filter hide utility — toggled by the `scope-filter` Stimulus
   controller to show/hide items by the active tab's data-scope. !important
   beats element display rules (grid/flex items) so any item type hides. */
.os-hide { display: none !important; }

/* #107 — search-filter and cert-filter own their own hide classes so they
   compose with scope-filter (.dv-hide) instead of clobbering one shared class.
   A row is visible only when NONE of the three filters wants it hidden. */
:is(.search-hide, .os-search-hide), :is(.cert-hide, .os-cert-hide) { display: none !important; }

/* Filter control — row-2 of .dv-page-tools (date pickers, toggles, facets).
   Fixed 32px so controls align with each other + the scope tabs by construction.
   Use --pill for toggles/facets; .is-on is the accent-soft active state.
   (Canonical in WL design-system tokens.css#filter-control.) */
.os-filter-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 12px;
  font-size: var(--text-xs); font-weight: 500;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--ink);
  cursor: pointer; white-space: nowrap; text-decoration: none;
  transition: border-color var(--t-fast) ease, background var(--t-fast) ease, color var(--t-fast) ease;
}
.os-filter-btn:hover { border-color: var(--hairline-strong); }
.os-filter-btn svg, .os-filter-btn [data-lucide] { color: var(--muted-2); flex-shrink: 0; }
.os-filter-btn__chev { margin-left: 2px; }
.os-filter-btn--pill { border-radius: 999px; color: var(--muted); }
.os-filter-btn--pill:hover { color: var(--ink); }
.os-filter-btn.is-on { background: var(--accent-soft); border-color: var(--accent-border); color: var(--accent); }
.os-filter-btn.is-on svg, .os-filter-btn.is-on [data-lucide] { color: var(--accent); }

/* ── Filter bar (full-control row) ────────────────────────────────────
   A wrapping row of full-size form controls (search input, selects, date
   pickers, a checkbox toggle, submit + clear buttons). Every direct control is
   normalised to the canonical 46px field height so the row aligns on one
   baseline by construction — no per-control height utilities. Pair the buttons
   with `.dv-btn--field` (also 46px). Shell/framework-agnostic: normalises by
   ELEMENT + gem field classes (`.dv-select`, `.dv-field`), not any product form
   namespace — a consumer's own inputs/selects placed in the bar are covered. */
.os-filter-bar :is(input:not([type="checkbox"]):not([type="radio"]), select, .os-select, .os-select-field) { height: 46px; }
/* Checkbox toggle styled as a bordered control so it reads as a peer of the
   inputs in the row (box + label inside a 46px hairline pill). Apply `.dv-check`
   to the toggle wrapper. */
.os-filter-bar .os-check {
  height: 46px;
  align-items: center;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--t-fast) ease;
}
.os-filter-bar .os-check:hover { background: var(--canvas); }

/* Ledger title-level date nav (.dv-pager) + today tag */
.os-pager { display: inline-flex; align-items: center; border: 1px solid var(--hairline); border-radius: 10px; overflow: hidden; }
.os-pager__step {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; cursor: pointer; color: var(--muted); text-decoration: none;
}
.os-pager__step:hover { background: var(--canvas); color: var(--ink); }
.os-pager__date {
  display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px;
  background: transparent; border: none; border-left: 1px solid var(--hairline); border-right: 1px solid var(--hairline);
  font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--accent); cursor: default;
}
.os-today-tag {
  display: inline-flex; align-items: center; height: 28px; padding: 0 12px;
  border-radius: 999px; background: var(--accent-soft); border: 1px solid var(--accent-border, rgba(3,105,161,0.18));
  font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent);
  cursor: pointer; text-decoration: none;
}

/* Ledger anchor stat (leads the scope row) */
.os-anchor-stat { display: flex; align-items: baseline; gap: 8px; flex-shrink: 0; }
.os-anchor-stat__label { font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2); }
/* Money anchor (Ledger "Today net") — Geist Mono tabular per the numerics rule,
   and sign-driven color: ink at zero, green only when positive, rose when
   negative (was hardcoded green, so $0.00 read as a gain). */
.os-anchor-stat__value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--text-xl); font-weight: 600; color: var(--ink); letter-spacing: -0.02em; }
.os-anchor-stat__value--pos { color: var(--confirmed); }
.os-anchor-stat__value--neg { color: var(--error, #c13028); }

/* ═══════════════════════════════════════════════════════════════════════
   Mobile list header (≤640) — inline, no collapse. The row-2 tools bar stays
   visible as one horizontal-scroll strip (a bottom-sheet collapse was tried and
   removed: a fixed overlay fought the app's board views, transformed shell
   containers and Turbo lifecycle, and a filter you can't see is a filter you
   can't use). The slim title bar (row 1) pins on scroll.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 640px) {
  /* The whole slim title bar sticks to the top on scroll, so the title, search
     and actions stay reachable down a long list.

     z-index kept LOW on purpose: a sticky (positioned) element already paints
     above the in-flow list without any z-index, so `5` is only here to order it
     against other positioned content. Critically it must stay BELOW the shell's
     fixed mobile nav toggle (the hamburger the shell overlays top-left) so the
     pinned white band never paints over it — shells put that toggle above
     content, and 5 leaves ample room under it. */
  .os-page-head {
    position: sticky;
    top: 0;
    z-index: 5;
    /* Tighten the desktop 32/18 padding to a symmetric phone rhythm. This both
       shrinks the bulky bar to the compact height the mocks show AND centres the
       title/actions cluster at ~32px from the pinned top — the vertical centre a
       shell hamburger sits at — so the toggle and the header row line up. */
    padding-top: 12px;
    padding-bottom: 12px;
    align-items: center;
  }

  /* Smaller pinned title — the desktop display size reads as oversized on a
     phone bar; step it down to the 2xl step (the mock size). */
  .os-page-head h1, h1.os-page-head__title { font-size: var(--text-2xl); }

  /* Equal-height action controls — search, the view toggle and the primary CTA
     all land on ONE 40px height so the cluster reads as a tidy row (the
     segmented control shipped taller than the icon buttons). */
  .os-page-head__actions :is(.os-btn, .os-btn--primary, .segmented, .os-segmented,
    .os-hsearch__toggle) {
    height: 40px;
  }
  .os-page-head__actions :is(.segmented, .os-segmented) { align-items: center; }

  /* Inline tools row: one horizontal-scroll strip so scope tabs + facet chips
     never wrap into a ragged multi-row block; swipe sideways to reach the rest.

     Sticks BELOW the pinned title bar (top = the header's height, set as
     `--os-tools-top` by the consumer's `page-tools` controller), so the two
     bands stack instead of colliding at top:0 (the "double bar"). z-index 4 is
     below the title bar (z 5) so the row slides up BEHIND it when hidden. The
     controller reveals it on scroll-UP (removes `--hidden`) and hides it on a
     sustained scroll-DOWN — quick access to filters without permanent chrome. */
  .os-page-tools {
    position: sticky;
    top: var(--os-tools-top, 0);
    z-index: 4;
    transition: transform 0.24s var(--ease-out-quart, ease);
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .os-page-tools.os-page-tools--hidden { transform: translateY(-165%); }
  /* When pinned on phones the row stacks BELOW the title bar, so it needs NO
     hamburger gutter — but it must keep the normal content inset (bleed-pad), so
     the first scope tab sits inset at rest and the strip still scrolls full-bleed
     to the edge. (The `page-tools` controller's --pinned gutter, meant for the
     top:0 case, would otherwise leave a 64px gap.) */
  .os-page-tools.os-page-tools--pinned { padding-left: var(--bleed-pad, 16px); }
  .os-page-tools::-webkit-scrollbar { display: none; }
  .os-page-tools > * { flex: 0 0 auto; }
  .os-page-tools > .os-spacer { display: none; }
  .os-page-tools .os-scope-tabs { flex-wrap: nowrap; }
}

/* ═══════════════════════════════════════════════════════════════════════
   Mobile treatment for the OTHER header component — `page_header`
   (`.os-page-band.page-header`), used by form / settings / simple pages. The
   same phone polish the list header (`.os-page-head`) gets, so a page reads the
   same on mobile regardless of which header component it was built with:
   pinned slim bar, stepped-down title, equal-height actions. Loaded after
   patterns.css, so these win over the component's base rules there.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 640px) {
  :is(.page-header, .os-page-header) {
    position: sticky;
    top: 0;
    z-index: 5;
    /* Tighten the desktop space-8/space-6 padding to the same compact phone
       rhythm as the list header. (`margin-top: -56px` from patterns.css keeps
       the bleed-up.) Clear the shell nav toggle HORIZONTALLY via the nav gutter
       (like `.os-page-head`), so the title sits BESIDE the hamburger, not below
       it — consistent across both header components. */
    padding-top: 12px;
    padding-bottom: 12px;
    padding-left: var(--dv-nav-gutter, 0);
  }
  :is(.page-header, .os-page-header) h1 { font-size: var(--text-2xl); }
  :is(.page-header__row, .os-page-header__row) { align-items: center; }
  /* Equal-height actions, matching the list header's 40px cluster. */
  :is(.page-header__actions, .os-page-header__actions) :is(.hero-show__action,
    .os-hero-show__action, .os-btn, .segmented, .os-segmented) {
    height: 40px;
  }

  /* Detail headers — hand-rolled `.os-page-band` bands carrying an entity title
     + status + actions (a booking, a dive trip, a diver). Same compact rhythm
     and stepped title, and clear the hamburger HORIZONTALLY via the gutter
     instead of the 72px vertical clearance that left a big top gap. Non-sticky:
     on these pages the band-2 tab nav (`.os-tabnav`) is what pins. `:has(h1)`
     keeps this off stat strips + tab navs; excludes page_header (above) + navs. */
  .os-page-band:has(h1):not(.os-tabnav):not(.page-header):not(.os-page-header):not(nav) {
    padding-top: 12px;
    padding-bottom: 12px;
    padding-left: var(--dv-nav-gutter, 0);
  }
  .os-page-band:not(.os-tabnav) h1 { font-size: var(--text-2xl); }

  /* Band-2 tab nav: when it pins to the top it collides with the fixed
     hamburger, so inset the tab row by the gutter while stuck (the `stuck`
     controller adds `.is-stuck`). At rest it sits below the header, un-inset. */
  .os-tabnav.is-stuck { padding-left: var(--dv-nav-gutter, 0); }
}


/* ===== os_ds/components/leg.css ===== */
/* os-leg — journey leg card head: a routed segment carrying its own status.
 * Source: `.leghead/.legico/.leglabel/.legroute` in transfers.html.
 * Composition: the head sits above a card body that renders `.os-route`.
 * Live inside an existing card shell (`.dv-card` / `.os-card`) — os-leg supplies the
 * head band only, not the card chrome.
 */

.os-leg__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-3);
  border-bottom: 1px solid var(--hairline);
}

.os-leg__icon {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-lg);
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
}

/* Not-yet-arranged leg: the icon drops out of accent. */
.os-leg__icon--todo {
  background: var(--surface-alt);
  color: var(--muted-2);
}

.os-leg__label {
  font-size: var(--text-2xs);
  line-height: var(--leading-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-2);
}

.os-leg__route {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: 2px;
  font-size: var(--text-lg);
  line-height: var(--leading-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.os-leg__arrow {
  color: var(--muted-2);
  display: inline-flex;
  align-items: center;
}

.os-leg__status {
  margin-left: auto;
  flex: none;
}


/* ===== os_ds/components/link.css ===== */
/* Canonical text-link treatment (DS link).
   Single source for the accent text-link / "View all" styling that was
   previously duplicated as utility-class soup across kanban / roster / show
   surfaces. Token-clean: accent family only, no raw palette, no dark:. */

.os-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1;
  color: var(--accent);
  text-decoration: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: color 0.15s ease;
}

.os-link:hover {
  color: var(--accent-strong);
}

.os-link :is(.lucide, .os-lucide), .os-link svg {
  flex-shrink: 0;
}

/* Count suffix — the "View all 12" number stays in the accent flow. */
.os-link__count {
  font-variant-numeric: tabular-nums;
}


/* ===== os_ds/components/locked.css ===== */
/* os-locked — dependency-locked box: "Add <X> and we'll line up <Y>".
 * Source: `.disabledbox/.dlock/.dt` in transfers.html.
 * Deliberately NOT an `.os-callout` variant: a callout reports a PROBLEM, this reports a
 * PREREQUISITE not yet met. Hence neutral dashed border on surface-alt — no status hue.
 */

.os-locked {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius-xl);
  background: var(--surface-alt);
}

.os-locked__icon {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--muted-2);
}

.os-locked__body {
  min-width: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  color: var(--muted);
}

.os-locked__body b,
.os-locked__body strong {
  color: var(--ink);
  font-weight: 600;
}


/* ===== os_ds/components/option-card.css ===== */
/* os-option-card — a selectable option row (icon tile + title + description + price).
 * Source: `.pick/.pi/.pk-t/.pk-d/.pk-p` (arrival.html, reg-card-hybrid.html) and
 * `.boat/.bi/.bt/.bd/.bp` (transfers.html). Those were the SAME idea at two fidelities —
 * the selectable list row and the "already chosen" accented block — collapsed here into
 * one component with a `--selected` modifier.
 * Works as <label>, <button> or <div>; wrap a visually-hidden radio for real selection.
 */

.os-option-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: left;
  /* The card MUST be the containing block for the visually-hidden input it
     owns (see the `position: absolute` rule below). Without this, that input
     resolves against whatever positioned ancestor happens to exist — and in a
     consumer whose page has none, that is the INITIAL containing block. It then
     escapes every `overflow` clip between here and the root (an overflow
     ancestor only clips an abspos descendant it is also the containing block
     for) and adds its own depth to the DOCUMENT's scrollable overflow.
     Measured in arena-ops: a full-bleed wizard pane at 900px viewport reported
     documentElement.scrollHeight 1289 with nothing visible below the fold, and
     the consumer's 100vh app shell scrolled off the top. A component must not
     depend on its host page to contain what the component itself positions.
     Same fix, same reason as `.side-panel__body` in patterns.css. */
  position: relative;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}

.os-option-card + .os-option-card {
  margin-top: var(--space-2);
}

.os-option-card:hover {
  border-color: var(--hairline-strong);
}

.os-option-card--selected,
.os-option-card--selected:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.os-option-card__icon {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: var(--radius-md);
  display: grid;
  place-items: center;
  background: var(--surface-alt);
  color: var(--text-secondary);
}

.os-option-card--selected .os-option-card__icon {
  background: var(--surface);
  color: var(--accent);
}

.os-option-card__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.os-option-card__title {
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 600;
  color: var(--ink);
}

.os-option-card__desc {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted);
  margin-top: 2px;
}

.os-option-card__price {
  margin-left: auto;
  flex: none;
  text-align: right;
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  color: var(--accent-strong);
}

/* ── The card IS the control (2026-07-28) ─────────────────────────────────
   A card-shaped option must not also show a radio dot. The card already says
   "pick one of these" through its shape, its hover and its selected fill; a
   radio next to it is the same message twice, and it is the message that looks
   like the real control, so the eye goes to a 16px dot instead of the 400px
   target. A PLAIN radio group still shows its radios — that is what
   `.os-radio` is for. This rule is only for the card form.

   THE BUG IT ALSO FIXES: consumers were painting `--selected` from
   SERVER-rendered state while `checked` followed the click, so selecting a
   different card moved the dot and left the fill behind. Painting from
   `:has(input:checked)` means the fill IS the checked state and cannot
   disagree with it. `--selected` still works for a static/no-JS render. */
.os-option-card :is(input[type="radio"], input[type="checkbox"]) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.os-option-card:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.os-option-card:has(input:checked) .os-option-card__icon {
  background: var(--accent-soft);
  color: var(--accent-strong);
}
/* The keyboard ring has to move to the card: the input it belongs to is hidden. */
.os-option-card:has(input:focus-visible) {
  outline: none;
  box-shadow: var(--shadow-focus);
}


/* ===== os_ds/components/page-hero.css ===== */
/* ─── Page hero (.os-page-hero) ──────────────────────────────────────────────
   Page-level welcome band: kicker + display title + supporting sub, with an
   optional row of at-a-glance facts. The one "colour moment" on a page — but
   the COMPONENT ITSELF IS NEUTRAL. The hue is applied by a product modifier
   (HotelOS ships `.hos-page-hero--guest` for the warm sand guest surface);
   never bake a brand tint in here.

   Origin: guest arrival-wizard / registration-card / transfers redesign
   (2026-07-25). `.rhero` / `.kicker` / `.wel` / `.subw` / `.facts` in
   mockups/arrival.html and mockups/transfers.html and mockups/reg-card-hybrid.html.

   API
     .os-page-hero              the band (flat solid fill, no gradient)
     .os-page-hero__kicker      micro uppercase eyebrow, accent-strong
     .os-page-hero__title       display title
     .os-page-hero__sub         supporting copy, measure-limited
     .os-page-hero__facts       wrapping row of fact chips
     .os-page-hero__fact        one chip: icon + label
     .os-page-hero__fact-value  mono, tabular figures, inside a __fact

   Composition notes
     · The generous bottom padding is deliberate: the first card of the page
       body floats UP onto the hero (negative top margin at the page level)
       carrying `var(--elevation-overlay)`. Don't trim it without retuning the
       overlap.
     · Header is a FLAT solid colour by decision — gradients, washes, glows and
       curved edges were explored and rejected.
     · Light-only, token-driven.
   ────────────────────────────────────────────────────────────────────────── */

.os-page-hero {
  position: relative;
  padding: var(--space-8) var(--space-10) 78px;
  background: var(--surface-alt);
  color: var(--ink);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}

.os-page-hero__kicker {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-strong);
}

.os-page-hero__title {
  margin: var(--space-3) 0 var(--space-1);
  font-size: var(--text-3xl);
  line-height: 1.1;
  font-weight: 700;
  letter-spacing: -0.022em;
  color: var(--ink);
}

.os-page-hero__sub {
  margin: 0;
  font-size: var(--text-md);
  line-height: var(--leading-md);
  color: var(--muted);
  max-width: var(--measure);
}

.os-page-hero__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.os-page-hero__fact {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.os-page-hero__fact-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

@media (max-width: 720px) {
  .os-page-hero {
    padding: var(--space-6) var(--space-5) 58px;
    border-radius: 0;
  }
  .os-page-hero__title {
    font-size: var(--text-2xl);
  }
  .os-page-hero__sub {
    font-size: var(--text-sm);
  }
}


/* ===== os_ds/components/partyband.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   partyband.css — `.os-partyband`, the identity header of a document that
   belongs to ONE party.

   WHY THIS EXISTS. A booking, a sale, a folio, a manifest: each is a stack of
   line items that all belong to one named party, and each product had grown
   its own way of saying so at the top — a bare `<h2>`, a card head with a
   name in it, a two-column strip. None of them carried the two facts that
   actually matter at a glance, WHO and HOW MUCH, and because the total was
   usually parked in a footer the reader had to travel the whole document to
   find the number they came for.

   The band answers both on one line. It is deliberately NOT a card head: it
   sits at the top of the content container it heads and is separated by a
   hairline, so a scrolled document reads as one object rather than a card of
   cards — the failure mode `.os-card` inside `.os-card` produces.

   ANATOMY. Avatar, identity block, then the figure pushed to the far edge.

     div.os-partyband
       span.os-avatar                                   (optional)
       div.os-partyband__id
         div.os-partyband__n
           span.os-partyband__name   "Novak Party"
           span.os-chip.os-chip--sm  "4 guests"         (optional)
         div.os-partyband__sub       "Room 1401 · 6–8 Aug"
       span.os-partyband__v          "$540.00"          (optional)

   THE FIGURE IS MONOSPACE and a size step above the name. Two bands stacked
   in a list must let their totals line up on the decimal, and when a reader
   is scanning for money the number should win the size contest against the
   name — the name is how you confirm you are in the right document, the
   number is what you came to read.

   `__v` uses `margin-left: auto`, so it may be omitted entirely; the identity
   block then simply runs the width of the band.
───────────────────────────────────────────────────────────────────── */

.os-partyband {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--hairline);
}

/* min-width:0 is load-bearing: without it a long party name refuses to
   ellipsis and instead pushes the figure off the end of the band. */
.os-partyband__id {
  min-width: 0;
}

.os-partyband__n {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.os-partyband__name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.os-partyband__sub {
  margin-top: var(--space-0-5);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  color: var(--muted);
}

.os-partyband__v {
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* On a phone the name and the figure cannot share a line without one of them
   truncating to nonsense, so the figure drops to its own row and stays
   right-aligned — the column the eye is already scanning. */
@container (max-width: 480px) {
  .os-partyband {
    flex-wrap: wrap;
  }
  .os-partyband__v {
    width: 100%;
    text-align: right;
  }
}


/* ===== os_ds/components/popover.css ===== */
/* ─── Popover — anchored multi-select / picker (.dv-pop) ─────────────────────
   Ported from the Trip v4 prototype roster (pt-pop → dv-pop). An anchored
   floating panel: search row on top, a scrollable option list, a footer for
   count + Done. Options are .dv-opt rows (avatar · name+meta · checkbox).
   The anchor wrapper needs position:relative (.dv-msel) — and remember
   .dv-card clips overflow; give the host card .dv-card--overflow-visible.
   Showcased in /admin/design_system (Popover section). */

.os-msel { position: relative; }

.os-pop {
  position: absolute;
  z-index: 40; top: calc(100% + 8px); left: 0;
  width: var(--width-xs);
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}
.os-pop[hidden] { display: none; }

.os-pop__search {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 12px; border-bottom: 1px solid var(--hairline);
}
.os-pop__search :is(.lucide, .os-lucide) { color: var(--muted-2); flex-shrink: 0; }
.os-pop__search input {
  flex: 1; border: 0; background: none; outline: none;
  font: inherit; font-size: var(--text-sm); color: var(--ink);
}
.os-pop__search input::placeholder { color: var(--muted-2); }

.os-pop__list { max-height: 268px; overflow-y: auto; padding: 4px; }
.os-pop__empty { padding: 16px; text-align: center; }

/* Option row — works as a bare <button> or a button_to form's button. */
.os-pop__list form { display: contents; }
.os-opt {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left;
  padding: 8px 10px; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-sm);
  transition: background .1s;
}
.os-opt:hover { background: var(--hover-warm); }
.os-opt__body { flex: 1; min-width: 0; line-height: 1.25; }
.os-opt__name { font-size: var(--text-sm); font-weight: 600; color: var(--ink); }
.os-opt__meta { font-size: var(--text-2xs); color: var(--muted-2); }
.os-opt__meta.is-off { color: var(--cancelled); }

.os-optcheck {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex-shrink: 0;
  border: 1.5px solid var(--hairline-strong); border-radius: 4px;
  color: var(--surface); background: var(--surface);
  transition: background .1s, border-color .1s;
}
.os-opt.is-on .os-optcheck { background: var(--accent); border-color: var(--accent); }
.os-optcheck :is(.lucide, .os-lucide) { opacity: 0; }
.os-opt.is-on .os-optcheck :is(.lucide, .os-lucide) { opacity: 1; }

.os-pop__foot {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px; border-top: 1px solid var(--hairline);
}
.os-pop__foot :is(.cnt, .os-cnt) { font-size: var(--text-2xs); color: var(--muted); }
.os-pop__foot :is(.cnt, .os-cnt) :is(.mono, .os-mono) { font-family: var(--font-mono); }

@media (max-width: 1024px) {
  .os-pop { width: min(var(--width-xs), calc(100vw - 48px)); }
}

/* ─── Popover dialog (.os-popover) ───────────────────────────────────────────
   A small anchored FORM DIALOG — head / body / foot in a bordered panel,
   role="dialog". Distinct from .os-pop above (a select/search DROPDOWN): this
   is for a focused mini-form (edit a flight, add a payment) rather than picking
   an option. Promoted from arena-ops (HotelOS) 2026-07-25. Token-only. */
.os-popover {
  width: 100%;
  max-width: var(--width-sm);
  max-height: 90vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-panel);
  display: flex;
  flex-direction: column;
}
.os-popover__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--hairline);
}
.os-popover__body {
  padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.os-popover__body--flush { padding: 0; }
.os-popover__sep { height: 1px; background: var(--hairline); margin: var(--space-1) 0; }
.os-popover__foot {
  display: flex; gap: var(--space-2); justify-content: flex-end;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--hairline);
}
.os-popover__foot--bare { padding: var(--space-3) 0 0; border: none; }


/* ===== os_ds/components/profile_row.css ===== */
/* ─── Profile row (.os-profile-row) ──────────────────────────────────────────
   A person/entity row: avatar · name + meta · trailing status/action. The most
   repeated list shape in both products — guest capture, occupants, waiver
   divers, crew, agents, staff pickers.

   Promoted 2026-07-26 from arena-ops `.hos-occrow` (the fuller BEM version);
   dive-os had the same base rule as the page-scoped `.bk-detail
   .waiver-diver-row` (identical flex/gap/padding/border), and arena carried a
   second copy in its guest-portal sheet. One component, three implementations —
   this is the one.

   Structure:
     .os-profile-row            (or --link on an <a>, --pick on a <label>)
       .os-profile-row__avatar  (+ --alt / --empty)
       .os-profile-row__info
         .os-profile-row__name
         .os-profile-row__meta
       .os-profile-row__trail   (or __badges / __status)

   Domain colourings (a role pill, an id pill) stay in the consuming app —
   they are status vocabulary, not row structure. Token-only. */

.os-profile-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--hairline);
}
.os-profile-row:last-child { border-bottom: none; }

/* Whole row is a link — keeps the row's own type colour, tints on hover. */
.os-profile-row--link {
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-md);
}
.os-profile-row--link:hover { background: var(--accent-softer); }

.os-profile-row__avatar {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--radius-pill);
  /* Accent-derived, NOT a raw ramp stop: this shipped in v1.3.0 reading
     `--teal-900`, which the gem does not define. An unresolvable var() voids
     the WHOLE background declaration, so the avatar lost its fill and its
     white initials vanished — invisible in arena (which defines that token
     locally) and broken in dive-os. Gem components read gem tokens. */
  background: linear-gradient(135deg, var(--accent), var(--accent-strong));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--surface);
}
/* --alt flips the gradient so adjacent rows read as different people. */
.os-profile-row__avatar--alt   { background: linear-gradient(135deg, var(--accent-strong), var(--accent)); }
/* --empty: a placeholder person (not captured / unassigned). */
.os-profile-row__avatar--empty { background: var(--surface-alt); color: var(--muted-2); }

.os-profile-row__info { flex: 1; min-width: 0; }
.os-profile-row__name { font-size: var(--text-sm); font-weight: 600; color: var(--ink); }
.os-profile-row__meta { font-size: var(--text-xs); color: var(--muted); margin-top: 1px; }

/* Trailing zones — __trail for actions, __status for pills, __badges for both. */
.os-profile-row__trail,
.os-profile-row__status {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}
.os-profile-row__status { flex-wrap: wrap; }
.os-profile-row__badges {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  flex: none;
  flex-wrap: wrap;
}

/* The trailing "Open ›" affordance on a --link row. */
.os-profile-row__open {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--muted);
}

/* ── --last: an explicit terminal row ───────────────────────────────────────
   `:last-child` is not enough when a control follows the list (a "view all"
   link after a capped roster) — the last ROW is then not the last CHILD.
   Requested by dive-os during the v1.4.0 adoption. */
.os-profile-row--last { border-bottom: none; }

/* ── __badges--columns: align trailing slots across rows ────────────────────
   The flex default lets each row size its own trailing block, so status pills
   drift out of alignment when labels differ in length ("Signed" vs "Valid
   waiver on file"). This switches to a grid whose track list the consumer
   sets, e.g. `style="--os-profile-row-badge-cols: 158px 80px"`. */
.os-profile-row__badges--columns {
  display: grid;
  grid-template-columns: var(--os-profile-row-badge-cols, auto auto);
  align-items: center;
}


/* ===== os_ds/components/progress-spine.css ===== */
/* ─── Progress spine (.os-progress-spine) ────────────────────────────────────
   HORIZONTAL wizard progress: evenly-spread step nodes joined by connector
   segments that fill as steps complete. Each step is an icon node + label +
   optional mono meta line, in one of three states: done / now / todo.

   NOT the gem's `.os-stepper`, which is a different (vertical) component —
   this one neither extends nor replaces it.

   Origin: `.rd-stepper` / `.rd-step*` in arena-ops
   `app/assets/stylesheets/components/reservation_details.css` — the shipped
   original, ported faithfully; re-confirmed by the 2026-07-25 arrival-wizard
   redesign (mockups/arrival.html).

   API
     .os-progress-spine              the track (position: relative)
     .os-progress-spine__step        one step column
     .os-progress-spine__node        44px circle holding an icon
     .os-progress-spine__label       step name
     .os-progress-spine__meta        mono micro sub-line
     .os-progress-spine__seg         absolutely-positioned connector
     .os-progress-spine__seg--fill   progress overlay on a connector
     .os-progress-spine__badge       small warning pip on a delayed step
   States (on __step)
     --done   filled confirmed-green node, white glyph
     --now    accent-light fill, accent border, accent-soft ring
     --todo   muted label

   Composition notes
     · Composes inside `.os-status-card`, below its __head.
     · Segments are positioned by the callsite with layout-only inline
       left/width percentages (geometry, no colour or type) — that is the one
       sanctioned inline style for this component.
     · The spine is secondary navigation: done nodes may be clickable to go
       back; forward progress belongs to the page's primary action.
     · Light-only, token-driven.
   ────────────────────────────────────────────────────────────────────────── */

.os-progress-spine {
  display: flex;
  position: relative;
  padding: 26px var(--space-5) 22px;
}

/* ── Connectors ── */
.os-progress-spine__seg {
  position: absolute;
  top: 47px;
  height: 2px;
  background: var(--hairline);
  z-index: 0;
}
.os-progress-spine__seg--fill {
  background: var(--status-confirmed-fg);
}

/* ── Steps ── */
.os-progress-spine__step {
  flex: 1;
  min-width: 0;
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.os-progress-spine__node {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  position: relative;
  z-index: 2;
  background: var(--surface);
  border: 2px solid var(--border-strong);
  color: var(--muted-2);
}

.os-progress-spine__label {
  font-size: var(--text-sm);
  font-weight: 640;
  color: var(--ink);
  margin-top: 10px;
}

.os-progress-spine__meta {
  font-size: var(--text-2xs);
  color: var(--muted);
  margin-top: 2px;
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── States ── */
.os-progress-spine__step--done .os-progress-spine__node {
  background: var(--status-confirmed-fg);
  border-color: var(--status-confirmed-fg);
  color: var(--surface);
}
.os-progress-spine__step--now .os-progress-spine__node {
  background: var(--accent-light);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: 0 0 0 5px var(--accent-soft);
}
.os-progress-spine__step--todo .os-progress-spine__label {
  color: var(--muted);
  font-weight: 560;
}

/* ── Warning pip (e.g. a delayed step) ── */
.os-progress-spine__badge {
  position: absolute;
  top: -3px;
  right: calc(50% - 30px);
  z-index: 3;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  background: var(--warning);
  color: var(--surface);
  border: 2px solid var(--surface);
}


/* ===== os_ds/components/qty.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   qty.css — `.os-qty`, the quantity stepper.

   WHY THIS EXISTS. Any line-item surface — a POS basket, a folio charge, a
   rental row — needs "how many of this". The system shipped nothing for it,
   so DiveOS wrote `.wz-qty` inside its booking wizard stylesheet and
   arena-ops, porting that flow, was about to write its own third copy under a
   local prefix. That is the exact drift the gem exists to stop, so the
   control is promoted here under its `.os-*` name.

   THE VISUAL CONTRACT, from the DiveOS control this replaces: ONE bordered
   box, not three adjacent widgets. The border belongs to the wrapper, the two
   buttons are flush inside it with no borders or radius of their own, and
   `overflow: hidden` lets their hover fill run into the corners cleanly. The
   value between them is TABULAR — a monospace, fixed-min-width slot — because
   the whole control must not resize when the count crosses 9 to 10, which is
   what makes a stepper feel unstable to hold a pointer over.

   SIZING. Everything derives from the control trio, so the stepper matches
   the button and input beside it at every density and grows inside
   `.os-density--touch` without a modifier. The buttons are SQUARE at the
   control height, which is the same rule `.os-btn--icon` follows. The value
   slot is a fraction of that height rather than a literal, so the control
   stays proportional at 32px and at 48px.

   MARKUP (the buttons are real button elements — see the reset below):

     div.os-qty
       button.os-qty__btn   type=button aria-label="Decrease quantity"
       span.os-qty__value                                    (or an input)
       button.os-qty__btn   type=button aria-label="Increase quantity"

   No Stimulus controller ships with it: the count lives in whatever state the
   consuming flow already owns (a JSON value on a wizard controller, a form
   field, a Turbo round-trip), and a controller here would have to guess.
   ───────────────────────────────────────────────────────────────────── */

.os-qty {
  display: inline-flex;
  align-items: center;
  /* The single border that makes this read as ONE control. */
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  /* Clips the button hover fill to the wrapper's corners. */
  overflow: hidden;
  height: var(--control-h);
  transition: border-color 100ms ease, box-shadow 100ms ease;
}
.os-qty:hover {
  border-color: var(--input-border-hover);
}

/* ── The two buttons ──────────────────────────────────────────────────
   Rendered as button elements, which is correct: they act, they do not
   navigate. That means the UA chrome must be reset here in full. v1.6.2
   shipped a fix for exactly this on the segmented control, which set colour
   and padding but never border, background, appearance or font-family, so
   the UA's outset border and buttonface fill showed through and the group
   rendered as mismatched boxes. Enumerated rather than remembered: border,
   background, appearance, font, colour, radius. */
.os-qty__btn {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--control-fs);
  border-radius: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Square at the control height — the same geometry rule .os-btn--icon
     uses, so the stepper's ends are as clickable as an icon button. */
  width: var(--control-h);
  height: 100%;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color 100ms ease, color 100ms ease;
}
.os-qty__btn:hover {
  background: var(--state-hover);
  color: var(--text-primary);
}
.os-qty__btn:active {
  background: var(--state-active);
}
/* Keyboard focus only, matching .os-btn. The ring is drawn INSIDE the wrapper
   because the wrapper clips overflow — an outer ring would be cut in half by
   the border it sits behind. */
.os-qty__btn:focus-visible {
  outline: 2px solid var(--accent-strong);
  outline-offset: -2px;
}
/* A stepper at its floor or ceiling disables the end it cannot move, rather
   than vanishing it — a control that changes shape at the boundary is worse
   than one that greys out. */
.os-qty__btn:disabled, .os-qty__btn[aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}

/* ── The value ────────────────────────────────────────────────────────
   Monospace + a min-width floor so 1 and 10 occupy the same slot. The floor
   is derived from the control height, not typed, so it scales with density.
   Rendered as a span in the read-only case; as .os-qty__input when the count
   must be typed as well as stepped. */
.os-qty__value {
  min-width: calc(var(--control-h) * 0.72);
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--control-fs);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  user-select: none;
}
/* The typable variant. It is deliberately NOT .os-input — that class carries
   its own border, background and full width, all three of which would break
   the single-box reading. The spinner buttons are stripped because the
   stepper's own buttons ARE the spinner. */
.os-qty__input {
  min-width: calc(var(--control-h) * 0.72);
  width: calc(var(--control-h) * 1.1);
  border: 0;
  background: transparent;
  outline: none;
  padding: 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--control-fs);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  appearance: textfield;
  -moz-appearance: textfield;
}
.os-qty__input::-webkit-outer-spin-button,
.os-qty__input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* Typing into the value rings the WHOLE control, because the input has no
   visible edge of its own to ring. */
.os-qty:focus-within {
  border-color: var(--input-border-focus);
  box-shadow: var(--shadow-focus);
}

/* ── Modifiers ────────────────────────────────────────────────────────
   Each re-declares the trio ON itself, which pins it against an enclosing
   .os-density--* scope — the same back-compat rule every sized component in
   the gem follows since v1.6.0. */
.os-qty--sm {
  --control-h:  var(--control-h-sm);
  --control-fs: var(--control-fs-sm);
}
.os-qty--touch {
  --control-h:  var(--control-h-touch);
  --control-fs: var(--control-fs-touch);
}
/* A stepper that owns its row rather than sitting at the end of one — a
   phone basket line, where the control spans the column and the value takes
   the slack between the two ends. */
.os-qty--block {
  display: flex;
  width: 100%;
}
.os-qty--block .os-qty__value, .os-qty--block .os-qty__input {
  flex: 1;
}
/* Sits inside a busy row (a table cell, a card line) where a second solid
   border would add noise: drops to the row's own tint and shows its edge on
   interaction only. */
.os-qty--quiet {
  background: var(--surface-alt);
  border-color: transparent;
}
.os-qty--quiet:hover {
  border-color: var(--input-border);
}

/* Whole-control disabled — the row is locked, not just an end of it. */
.os-qty--disabled {
  opacity: var(--disabled-opacity);
  pointer-events: none;
}


/* ===== os_ds/components/route.css ===== */
/* os-route — A→B location timeline (origin → dotted connector → destination).
 * Source: `.loc/.locrow/.locmark/.lmk/.lline/.locbody` in transfers.html + arrival-rich.html.
 * NOT `.os-timeline` (that is the vertical NUMBERED node list) — this is a two-endpoint
 * route: ring dot at the origin, filled pin at the destination, right-aligned mono time.
 * Composed inside `.os-leg`, or standalone in any card body.
 */

.os-route {
  display: flex;
  flex-direction: column;
}

.os-route__origin,
.os-route__dest {
  display: flex;
  gap: var(--space-3);
  min-width: 0;
}

/* Marker column: dot on top, connector line growing beneath it. */
.os-route__marker {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: none;
  padding-top: 3px;
}

.os-route__origin .os-route__marker::before {
  content: "";
  width: 14px;
  height: 14px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 3px solid var(--accent);
  background: var(--surface);
}

.os-route__dest .os-route__marker::before {
  content: "";
  width: 14px;
  height: 14px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 3px solid var(--accent-strong);
  background: var(--accent-strong);
}

.os-route__line {
  flex: 1;
  width: 0;
  border-left: 2px dotted var(--muted-2);
  margin: 3px 0;
}

/* Body: title row (title + trailing time) then sub. */
.os-route__origin > :not(.os-route__marker),
.os-route__dest > :not(.os-route__marker) {
  min-width: 0;
  padding-bottom: var(--space-4);
}

.os-route__dest > :not(.os-route__marker) {
  padding-bottom: 0;
}

.os-route__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 680;
  color: var(--ink);
}

.os-route__sub {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted);
  margin-top: 1px;
}

.os-route__time {
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 680;
  font-size: var(--text-sm);
  color: var(--ink);
}


/* ===== os_ds/components/section-head.css ===== */
/* ─── Section head (.os-section-head) ────────────────────────────────────────
   The canvas section-eyebrow ROW that titles a group of cards directly on the
   page canvas (no card chrome): accent glyph + uppercase label + optional
   right-aligned meta.

   Origin: `.rd-sectitle` in arena-ops
   `app/assets/stylesheets/components/reservation_details.css`, used throughout
   the 2026-07-25 arrival-wizard / registration-card / transfers redesign.

   API
     .os-section-head          the row
     .os-section-head__icon    accent glyph (inline Lucide SVG)
     .os-section-head__label   the eyebrow text
     .os-section-head__meta    right-aligned supporting meta, muted mono

   Composition notes
     · __label COMPOSES the gem's `.os-eyebrow` — callsites put BOTH classes on
       the element. Everything `.os-eyebrow` already owns (size, weight,
       transform, letter-spacing) is deliberately NOT restated here; only the
       darker on-canvas colour is added, since a section head carries more
       weight than an in-card eyebrow.
     · Distinct from `.os-section-label`, which is a standalone label, not a row.
     · Light-only, token-driven.
   ────────────────────────────────────────────────────────────────────────── */

.os-section-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 2px 2px 10px;
  min-width: 0;
}

.os-section-head__icon {
  display: inline-flex;
  flex: none;
  color: var(--accent);
}

/* Compose with .os-eyebrow — colour only. */
/* Owns its own type — deliberately does NOT compose `.os-eyebrow`.
   `.os-eyebrow` is a MICRO-LABEL INSIDE a card ("STATUS", "ROOM") at 10px; a
   section head titles a GROUP OF CARDS on the canvas. Composing them forced one
   scale onto two roles and the larger role lost: at 10px (and even at 12px) the
   head renders SMALLER than the 14px body text it introduces, so it reads as a
   field label rather than a title. --text-sm matches the content it heads, and
   uppercase + 700 + tracking carry the hierarchy. Judged side-by-side against a
   neighbouring `.os-status-card` title (16px/680) — it does not compete.
   UPSTREAM: the gem wants both roles named (micro-label vs section head), or an
   `.os-eyebrow--lg`; raised with the os-ds owner. */
/* WHY THIS DOES NOT COMPOSE .os-eyebrow (decided v1.4.0, do not "fix")
   Composing was tried and reverted. `.os-eyebrow` is a 10px/600 micro-label
   that sits INSIDE a card; this is a section head titling a GROUP of cards on
   the canvas. Composed, the head rendered smaller than the 14px body text it
   introduces and read as a field label. 13px would be off-scale (10/12/14/16),
   so hallmark would flag it; 12px still lands under the body.
   Two roles at two scales — kept separate deliberately, NOT named
   `.os-eyebrow--lg`, because that would make the larger role a variant of the
   smaller one and invert the hierarchy. Verified it does not compete with a
   neighbouring `.os-status-card` title (16px/680): that wins on container and
   block, not type size. */
.os-section-head__label {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-secondary);
  min-width: 0;
}

.os-section-head__meta {
  margin-left: auto;
  flex: none;
  font-size: var(--text-xs);
  color: var(--muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}


/* ===== os_ds/components/segment.css ===== */
/* ───────────────────────────────────────────────────────────────────────────
   .dv-segment — segmented control for a small set of mutually-exclusive
   options rendered as a native radio group (accessible, keyboard-navigable).
   Button chrome lives here so views never hand-roll bg-accent + rounded-btn
   utility soup (which trips the design-system lookalike gate).

   Markup:
     <div class="dv-segment dv-segment--block">
       <label class="dv-segment__option">
         <input type="radio" class="dv-segment__input sr-only" ...>
         <span class="dv-segment__label">DIN</span>
       </label>
       ...
     </div>
   ─────────────────────────────────────────────────────────────────────────── */
.os-segment {
  display: inline-flex;
  gap: 0.5rem;
}

.os-segment--block {
  display: flex;
  width: 100%;
}

.os-segment__option {
  flex: 1 1 0;
  cursor: pointer;
}

.os-segment__label {
  display: block;
  text-align: center;
  /* Density via vertical padding — same reasoning as .os-segmented__option in
     patterns.css: this label declares no height and is sized by its line box,
     so introducing a min-height would impose a floor it has never had.
     (36 − 20) / 2 = 8px at base density, identical to the 0.5rem literal it
     replaces; the 8px floor holds --compact at today's size. */
  padding: max(8px, calc((var(--control-h) - 20px) / 2)) 0.75rem;
  font-size: 0.875rem;
  color: var(--ink);
  background-color: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.os-segment__option:hover .os-segment__label {
  background-color: var(--surface-alt);
}

.os-segment__input:checked + .os-segment__label {
  background-color: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

.os-segment__input:focus-visible + .os-segment__label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ===== os_ds/components/sheet.css ===== */
/* ═══════════════════════════════════════════════════════════════════════
   dv-sheet — mobile bottom sheet.

   A modal surface that slides up from the bottom edge for edit-in-context
   tasks on touch/phone (log a dive, pick a value). Backdrop dims the page;
   the panel is capped at 90svh (see the note on .dv-sheet__panel) and scrolls
   internally. On tablet/desktop the same content is better served by a drawer
   or inline panel — the sheet is intended for narrow viewports.

   Anatomy:
     .dv-sheet                 — fixed backdrop (toggle .is-open)
       .dv-sheet__panel        — the sheet surface
         .dv-sheet__grip       — drag affordance pill
         .dv-sheet__head
         .dv-sheet__body       — scroll region
         .dv-sheet__foot       — sticky action row
   ═══════════════════════════════════════════════════════════════════════ */
.os-sheet {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: flex-end;
  background: rgba(17, 24, 39, .45);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s var(--ease-standard);
}
.os-sheet.is-open {
  opacity: 1;
  pointer-events: auto;
}
.os-sheet__panel {
  width: 100%;
  /* `svh`, not `vh` or `dvh` — and on a bottom sheet the distinction is not
     cosmetic. `vh` is the LARGE viewport (toolbar hidden), so with the toolbar
     showing a 90vh panel is taller than the visible area and `.dv-sheet__foot`
     — the action row — sits behind the browser chrome on the one component
     type where the actions matter most.

     `dvh` would track the toolbar live and repaint the panel mid-scroll.
     `svh` is the small viewport (toolbar shown), so the sheet always fits.
     `vh` stays first as the fallback; both svh/dvh are Safari 15.4+.
     See docs/mobile-primitives.md §1. */
  max-height: 90vh;
  max-height: 90svh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--shadow-panel);
  transform: translateY(100%);
  transition: transform .24s var(--ease-out-quart);
}
.os-sheet.is-open .os-sheet__panel { transform: translateY(0); }
.os-sheet__grip {
  width: 40px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--gray-300);
  margin: 10px auto 4px;
  flex-shrink: 0;
}
.os-sheet__head {
  padding: 8px 20px 12px;
  border-bottom: 1px solid var(--hairline);
}
.os-sheet__head .os-eyebrow { color: var(--muted-2); }
.os-sheet__body {
  padding: 18px 20px;
  overflow-y: auto;
  flex: 1;
  /* Containing block, for the same reason as `.os-drawer__body` — this scrolls
     inside a `position: fixed` sheet, and an unowned abspos descendant anchors
     to that wrapper rather than to this scroller. See the note there. */
  position: relative;
}
.os-sheet__body > .os-eyebrow { display: block; color: var(--muted-2); margin-bottom: 4px; }
.os-sheet__foot {
  display: flex;
  gap: 10px;
  padding: 14px 20px calc(14px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--hairline);
  background: var(--surface);
}
.os-sheet__foot > .os-btn { flex: 1; justify-content: center; min-height: 48px; }

/* The sheet is a mobile pattern; on wide viewports keep it available but it is
   typically driven only under a media query by the consuming surface. */
@media (min-width: 641px) {
  .os-sheet__panel { max-width: 440px; margin: 0 auto; border-radius: var(--radius-xl); }
  .os-sheet { align-items: center; }
}


/* ===== os_ds/components/signpad.css ===== */
/* os-signpad — signature capture: dashed capture area, then a signed confirmation.
 * Source: `.signpad/.sigconfirm/.sigchk` in reg-card-hybrid.html.
 * Generic across products (HotelOS registration cards, DiveOS waivers). Uses the
 * `--status-confirmed-*` trio for the signed state so it matches every other
 * "confirmed" surface, and stays leaf-green rather than brand-teal.
 */

.os-signpad {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.os-signpad__pad {
  flex: 1;
  min-height: 120px;
  display: grid;
  place-items: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-alt);
  color: var(--muted-2);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  cursor: crosshair;
  touch-action: none;
}

.os-signpad--signed .os-signpad__pad {
  border-style: solid;
  border-color: var(--status-confirmed-border);
  background: var(--status-confirmed-bg);
  cursor: default;
}

.os-signpad__hint {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  color: var(--muted);
}

/* Signed confirmation tick: a soft round tile holding the check icon. Place it in
   __foot beside the "Signed by …" line; it only shows in the --signed state. */
.os-signpad__check {
  display: none;
  width: 22px;
  height: 22px;
  flex: none;
  place-items: center;
  border-radius: var(--radius-pill);
  background: var(--status-confirmed-bg);
  border: 1px solid var(--status-confirmed-border);
  color: var(--status-confirmed-fg);
}

.os-signpad--signed .os-signpad__check {
  display: grid;
}

.os-signpad__foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  color: var(--muted);
}

.os-signpad--signed .os-signpad__foot {
  color: var(--status-confirmed-fg);
  font-weight: 600;
}

.os-signpad--signed .os-signpad__hint {
  display: none;
}


/* ===== os_ds/components/stat_card.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   Shared component layer — boxed stat card grid.

   Promoted from the one-off `.ledger-stat-card` (+ `-grid`, `-label`, `-value`)
   that lived in account/ledger.css. This is the canonical BOXED stat component
   (surface card per stat), distinct from the inline `.kpi-strip` pattern in
   patterns.css. Use `.dv-stat-card-grid` when you want each KPI in its own
   bordered card (e.g. the Ledger day summary); use `.kpi-strip` for the flat
   inline stat strip.

   Values are matched BYTE-FOR-BYTE to the former `.ledger-stat-*` rules so the
   Ledger page renders identically. Token-driven; @imported from application.css
   after components/header_band.css.
   ───────────────────────────────────────────────────────────────────── */

/* ── Grid ─────────────────────────────────────────────────────────────
   5-up by default (the Ledger day summary). Callers can override
   grid-template-columns inline / page-scoped for other counts. */
.os-stat-card-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 12px;
}

/* ── Card shell ─────────────────────────────────────────────────────── */
.os-stat-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}

/* ── Label (uppercase eyebrow) ──────────────────────────────────────── */
.os-stat-card-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── Value (mono numeric) ───────────────────────────────────────────── */
.os-stat-card-value {
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  margin-top: var(--space-2);
  line-height: 1;
}

/* ── Interactive variant (gear registry category filter cards) ────────
   A stat card that IS the interaction: rendered as an <a>, the whole
   card toggles a filter / navigates. Hover strengthens the border;
   `.is-active` carries the selected state (accent border + ultra-soft
   tint — mirrors the selected-row treatment). Focus ring per DESIGN.md
   accessibility rules. */
.os-stat-card--interactive {
  display: block;
  cursor: pointer;
  text-decoration: none;
  transition: border-color 150ms cubic-bezier(0, 0, 0.2, 1),
    background-color 150ms cubic-bezier(0, 0, 0.2, 1);
}
.os-stat-card--interactive:hover { border-color: var(--hairline-strong); }
.os-stat-card--interactive:focus-visible {
  outline: 2px solid var(--accent-strong);
  outline-offset: 2px;
}
.os-stat-card--interactive.is-active {
  border-color: var(--accent);
  background: var(--accent-softer);
}

/* ── Sub line (one line of supporting data under the value, e.g.
   "3 new this month"). NOT decorative — carries a secondary metric. ──── */
.os-stat-card-sub {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--muted);
}
.os-stat-card-sub strong { font-weight: 600; color: var(--ink); }
.os-stat-card-sub a { color: var(--accent); font-weight: 500; }
.os-stat-card-sub a:hover { text-decoration: underline; }

/* ── Column modifiers ────────────────────────────────────────────────
   The base grid is 5-up (Ledger day summary). These give the common
   responsive 4-up / 3-up layouts without per-call inline overrides. The
   full-width modifier is for a single emphasis stat. */
.os-stat-card-grid--full { grid-template-columns: 1fr; }
.os-stat-card-grid--4 { grid-template-columns: 1fr; }
.os-stat-card-grid--3 { grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .os-stat-card-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .os-stat-card-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .os-stat-card-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}


/* ===== os_ds/components/status-card.css ===== */
/* ─── Status card (.os-status-card) ──────────────────────────────────────────
   A CARD that states where something stands: icon tile + title + sub, with an
   optional right-aligned ETA (mono value + uppercase micro label). It is a
   standalone card — NOT part of a page hero — so it works on pages that have
   no hero at all.

   Origin: the arrival-readiness hero shipped in arena-ops
   `app/assets/stylesheets/components/reservation_details.css` (`.rd-hero__*`),
   re-confirmed by the 2026-07-25 arrival-wizard redesign
   (mockups/arrival.html). Ported, not reinvented.

   API
     .os-status-card             the card shell
     .os-status-card__head       tinted header row (accent-softer + bottom rule)
     .os-status-card__icon       40px icon tile
     .os-status-card__title      what is being tracked
     .os-status-card__sub        one-line state summary
     .os-status-card__eta        right-aligned block
     .os-status-card__eta-value  mono, tabular figures
     .os-status-card__eta-label  uppercase micro label

   Composition notes
     · `.os-progress-spine` composes INSIDE this card, below __head — that is
       the canonical pairing (state + how far along).
     · Card chrome (background/border/radius) comes from whatever card shell the
       product uses; only the head band and its typography are defined here, so
       the component drops into an existing card without fighting it.
     · If it overlaps a tinted page hero, add `var(--elevation-overlay)`.
     · Light-only, token-driven.
   ────────────────────────────────────────────────────────────────────────── */

.os-status-card {
  padding: 0;
  min-width: 0;
}

.os-status-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  background: var(--accent-softer);
  border-bottom: 1px solid var(--accent-border);
}

.os-status-card__icon {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--accent);
}

.os-status-card__title {
  font-size: var(--text-md);
  font-weight: 680;
  color: var(--ink);
  line-height: 1.2;
}

.os-status-card__sub {
  font-size: var(--text-xs);
  color: var(--muted);
  margin-top: 2px;
}

.os-status-card__eta {
  margin-left: auto;
  text-align: right;
  flex: none;
}

.os-status-card__eta-value {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 680;
  letter-spacing: -0.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.os-status-card__eta-label {
  display: block;
  font-size: var(--text-2xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-top: 2px;
}


/* ===== os_ds/components/sticky-bar.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   sticky-bar.css — `.os-stickybar`, a bar that CONDENSES when it sticks.

   WHY THIS EXISTS. A page's top chrome has two jobs that want different
   sizes. In flow, above the content, it is a heading: it needs air, and it
   gets 40px above / 20px below — asymmetric on purpose, so the extra top
   separates it from whatever is above and the tighter bottom binds it to the
   thing it labels. Once it STICKS it stops being a heading and becomes an
   instrument: it is now permanently occupying the top of the reader's
   viewport, so every pixel it keeps is a pixel of content it costs. Stuck, it
   drops to 16/16.

   Measured in DiveOS before it was written down: a stepper in flow
   (`.wz-couple`, bookings/new) is 40/20; a stepper that sticks
   (`.run-stepper`, dive_trips/:id/run) is 16/16. Same component, two states —
   which is why this is one class with a modifier rather than two components.

   CSS has no selector for "this sticky element is currently stuck", so the
   `stuck` Stimulus controller (shipped alongside) adds `.is-stuck` using a
   zero-height sentinel + IntersectionObserver. Without JS the bar simply
   stays at its roomy size — it never breaks, it just never condenses.

     <div class="os-stickybar" data-controller="stuck">…</div>

   The transition is on PADDING only. Animating height would reflow the
   content inside the bar; padding moves the bar's edges and leaves its
   contents where they are.
───────────────────────────────────────────────────────────────────── */

.os-stickybar {
  position: sticky;
  top: 0;
  z-index: 15;            /* under a sticky action bar (20) — a stepper that
                             outranks the footer can overlap it on a short page */
  background: var(--canvas);
  border-bottom: 1px solid var(--hairline);
  padding-block: var(--space-10) var(--space-5);   /* 40 / 20 — in flow */
  transition: padding-block 160ms var(--ease-standard);
}

.os-stickybar.is-stuck {
  padding-block: var(--space-4);                   /* 16 / 16 — stuck */
}

/* Motion is decoration here: the size change carries the meaning, the tween
   does not. */
@media (prefers-reduced-motion: reduce) {
  .os-stickybar { transition: none; }
}


/* ===== os_ds/components/summary-card.css ===== */
/* ============================================================
   summary-card.css — DiveOS Summary Card (design-system component).
   Header band · icon-tile rows · tinted total footer.
   Palette is a cool blue-gray surface (NOT the white card surface) so
   the summary reads as a distinct "running tally" object:
     · card    #F2F5F9   · icon tile #E3EAF2   · footer band #E3EAF1
   Type: label 12px · row title 14px/700 · desc 12px · icon 16 · price 24.
   ============================================================ */
:is(.ds-sum, .os-ds-sum) {
  --ds-sum-bg:     #F2F5F9;
  --ds-sum-tile:   #E3EAF2;
  --ds-sum-line:   var(--slate-200);
  --ds-sum-foot:   #E3EAF1;
  --ds-sum-bd:     #D5DEEA;   /* cool blue-gray outer border — bespoke to this card */
  background: var(--ds-sum-bg);
  border: 1px solid var(--ds-sum-bd);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 2px rgba(17,24,39,.05), 0 1px 0 rgba(255,255,255,.6) inset;
  overflow: hidden;
}

/* header band */
:is(.ds-sum-head, .os-ds-sum-head) { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 15px 20px 13px; }
:is(.ds-sum-head, .os-ds-sum-head) :is(.t, .os-t) { font-family: var(--font-sans); font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.11em; text-transform: uppercase; color: var(--slate-500, var(--slate-500)); white-space: nowrap; }
:is(.ds-sum-head, .os-ds-sum-head) :is(.st, .os-st) { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); white-space: nowrap; }
:is(.ds-sum-head, .os-ds-sum-head) :is(.st, .os-st):is(.ok, .os-ok) { color: var(--confirmed); }

/* icon-tile rows */
:is(.ds-sum-row, .os-ds-sum-row) { display: flex; align-items: center; gap: 14px; padding: 12px 20px; border-top: 1px solid var(--ds-sum-line); }
:is(.ds-sum-row, .os-ds-sum-row):first-of-type { border-top: 0; }
:is(.ds-sum-row, .os-ds-sum-row) :is(.ico, .os-ico) { width: 36px; height: 36px; border-radius: var(--radius); background: var(--ds-sum-tile); color: var(--accent); display: grid; place-items: center; flex-shrink: 0; }
/* The label keeps its natural width and never collapses. (It was `flex: 1` with
   `min-width: 0`, which with a `flex-basis: 0%` shrank the label box to 0 and let
   its text overflow onto the value.) The value takes the remaining space below. */
:is(.ds-sum-row, .os-ds-sum-row) :is(.main, .os-main) { flex: 0 0 auto; }
:is(.ds-sum-row, .os-ds-sum-row) :is(.main, .os-main) :is(.nm, .os-nm) { font-family: var(--font-sans); font-weight: 700; font-size: var(--text-sm); letter-spacing: -0.005em; color: var(--ink); line-height: 1.2; }
:is(.ds-sum-row, .os-ds-sum-row) :is(.main, .os-main) :is(.sub, .os-sub) { font-size: var(--text-xs); color: var(--muted); margin-top: 2px; line-height: 1.2; }
/* The value takes the remaining space, right-aligned, and WRAPS a long token
   (e.g. a verbose vessel name) instead of nowrap-overflowing across the label.
   `flex: 1 1 auto` + `min-width: 0` lets it fill and shrink; `overflow-wrap:
   anywhere` breaks unbroken tokens. */
:is(.ds-sum-row, .os-ds-sum-row) :is(.val, .os-val) { flex: 1 1 auto; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 500; letter-spacing: -0.01em; color: var(--ink); text-align: right; overflow-wrap: anywhere; }

/* progress strip (payment) */
:is(.ds-sum-prog, .os-ds-sum-prog) { padding: 13px 20px; border-top: 1px solid var(--ds-sum-line); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.bar, .os-bar) { height: 7px; border-radius: 99px; background: rgba(100,116,139,0.20); overflow: hidden; }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.bar, .os-bar) > span { display: block; height: 100%; border-radius: 99px; background: var(--accent); transition: width .25s cubic-bezier(0.32,0.72,0,1); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.bar, .os-bar):is(.full, .os-full) > span { background: var(--confirmed); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.row, .os-row) { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-top: 9px; font-size: var(--text-xs); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.row, .os-row) :is(.paid, .os-paid) { font-family: var(--font-mono); font-weight: 600; color: var(--accent); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.row, .os-row) :is(.paid, .os-paid):is(.ok, .os-ok) { color: var(--confirmed); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.row, .os-row) :is(.total, .os-total) { color: var(--muted); }
:is(.ds-sum-prog, .os-ds-sum-prog) :is(.row, .os-row) :is(.total, .os-total) :is(.mono, .os-mono) { font-family: var(--font-mono); }

:is(.ds-sum-state, .os-ds-sum-state) { display: flex; align-items: center; gap: 8px; padding: 11px 20px; border-top: 1px solid var(--ds-sum-line); font-size: var(--text-xs); color: var(--gray-700); }
:is(.ds-sum-state, .os-ds-sum-state):is(.ok, .os-ok) { color: var(--confirmed); }
:is(.ds-sum-state, .os-ds-sum-state):is(.no, .os-no) { color: var(--briefing); }
:is(.ds-sum-state, .os-ds-sum-state) :is(.ico, .os-ico) { display: inline-flex; }
:is(.ds-sum-defer, .os-ds-sum-defer) { display: flex; align-items: baseline; justify-content: space-between; padding: 0 20px 12px; margin-top: -2px; font-size: var(--text-xs); color: var(--muted); }
:is(.ds-sum-defer, .os-ds-sum-defer) :is(.mono, .os-mono) { font-family: var(--font-mono); }

/* tinted total footer */
:is(.ds-sum-foot, .os-ds-sum-foot) { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 15px 20px; background: var(--ds-sum-foot); border-top: 1px solid var(--ds-sum-line); }
:is(.ds-sum-foot, .os-ds-sum-foot) :is(.k, .os-k) { font-family: var(--font-sans); font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--slate-500, var(--slate-500)); }
:is(.ds-sum-foot, .os-ds-sum-foot) :is(.v, .os-v) { font-family: var(--font-display); font-weight: 600; font-size: var(--text-2xl); letter-spacing: -0.02em; line-height: 1; }
:is(.ds-sum-foot, .os-ds-sum-foot) :is(.v, .os-v):is(.accent, .os-accent) { color: var(--accent); }
:is(.ds-sum-foot, .os-ds-sum-foot) :is(.v, .os-v):is(.ok, .os-ok) { color: var(--confirmed); }
:is(.ds-sum-foot, .os-ds-sum-foot) :is(.v, .os-v) :is(.cents, .os-cents) { font-size: var(--text-md); color: var(--muted-2); }


/* ===== os_ds/components/table.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   .dv-table — list/index table TYPOGRAPHY component (maturity ladder ①②③).

   Promotes the cell typography that every account/* list table was
   re-inventing with bespoke utilities (text-[13.5px], text-[12px] mt-0.5,
   font-mono, mixed medium/semibold, 13/13.5/14.5px sizes). This sheet owns
   the *cell content* vocabulary; the surrounding chrome (outer border,
   radius, canvas header band, hairline rows, hover, py-3 rhythm,
   first/last-child padding) STILL lives in header_band.css under
   `.dv-table-wrap` / `.box-table` and is NOT moved or duplicated here.
   The two COEXIST: a table renders inside `.dv-table-wrap` for chrome and
   uses `.dv-table__*` classes on its cells for typography.

   Canonical type scale (anchored on bookings/divers — the closest pair):
     .dv-table__primary  13.5px / 500 / --ink   (link → hover accent, no underline)
     .dv-table__sub      12px   / --muted        (+2px top INSIDE the stack)
     .dv-table__stack    flex column owning the 2px primary→sub gap
     .dv-table__meta     13px   / --muted        (--mono → mono tabular-nums)
     .dv-table__th       11px uppercase tracking / --muted header cell

   Token-only (var(--token)); no raw hex, no raw Tailwind palette.
   Loaded after header_band.css (see application.css).
   ───────────────────────────────────────────────────────────────────── */

/* ── Optional element hook ────────────────────────────────────────────
   `.dv-table` may be added to a <table> for scoping, but the cell classes
   work standalone so existing `.dv-table-wrap table` markup adopts them
   incrementally with zero structural change. */
.os-table { width: 100%; border-collapse: collapse; }

/* ── Primary cell text (the row's headline: name, ID, title) ──────────── */
.os-table__primary {
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.3;
  color: var(--ink);
}
/* A link inside the primary slot: inherits the headline type, lifts to the
   accent on hover, never underlines (the row-hover tint is the affordance). */
.os-table__primary a, a.os-table__primary {
  color: inherit;
  text-decoration: none;
  transition: color var(--t-fast) ease;
}
.os-table__primary a:hover, a.os-table__primary:hover {
  color: var(--accent);
  text-decoration: none;
}

/* ── Sub text (secondary line under the primary: email, brand, serial) ─── */
.os-table__sub {
  font-size: var(--text-xs);
  line-height: 1.3;
  color: var(--muted);
}

/* ── Stack (primary + sub) — owns the 2px gap so cells stop using mt-0.5 ── */
.os-table__stack {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.os-table__stack .os-table__sub { margin-top: 2px; }

/* ── Meta cell (neutral data: dates, counts, specialties, condition) ───── */
.os-table__meta {
  font-size: var(--text-sm);
  color: var(--muted);
}
.os-table__meta--mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ── Figure cell (prominent commercial numeric: price, total, commission) ──
   Like __meta--mono but INK, not muted — a price is primary data, not neutral
   meta. Mono + tabular so a column of figures aligns. */
.os-table__figure {
  font-size: var(--text-sm);
  color: var(--ink);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ── Header cell — matches the existing .dv-table-wrap thead th treatment
   so promoting a header to this class doesn't shift pixels. ─────────────── */
.os-table__th {
  /* Component owns the header band height — mirrors how `.dv-table-wrap td`
     owns body-row rhythm (header_band.css), so a surface never has to remember
     a py-* utility on every <th>. ~14px ≈ divers/index's py-3.5, one step
     tighter than the 16px body row. */
  padding-top: 14px;
  padding-bottom: 14px;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
}

/* ── Row interaction states (consistent with header_band.css's
   `.dv-table-wrap tbody tr:hover { background: var(--surface-alt) }`). ──── */
.os-table tbody tr.is-interactive { cursor: pointer; }
.os-table tbody tr.is-interactive:hover, .os-table tbody tr.is-interactive:focus-within { background: var(--hover-warm); }
.os-table tbody tr.is-selected {
  background: var(--accent-soft);
  box-shadow: inset 2px 0 0 var(--accent);
}
.os-table tbody tr.is-selected:hover { background: var(--accent-soft); }

/* ════════════════════════════ Variants ════════════════════════════════ */

/* --avatar — first cell pairs an avatar with the primary/sub stack. The
   avatar partial owns its own size; this just sets the flex pairing + gap. */
.os-table--avatar td:first-child .os-table__cell, .os-table__avatar-cell {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* --compact — tighter vertical rhythm for dense lists (overrides the gated
   py-3 baseline on cells that don't set their own padding). */
.os-table--compact td:not([class*="py-"]) {
  padding-top: var(--space-2, 8px);
  padding-bottom: var(--space-2, 8px);
}
.os-table--compact .os-table__primary { font-size: var(--text-sm); }

/* --numeric — right-align numeric columns and tabularise their figures so
   digits line up column-wise (amounts, pax, counts). */
.os-table--numeric td.os-table__num, .os-table--numeric th.os-table__num, .os-table__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.os-table--numeric td.os-table__num { font-family: var(--font-mono); }

/* ════════════════════ __actions — right-pinned action column ═══════════════
   Keeps the row's actions (edit/delete/overflow) visible without scrolling to
   the end of a wide table. The cell sticks to the right edge of the scroll
   container; a gradient bleed on its left signals that body content passes
   UNDER it (shown only when the table actually overflows — the
   `dv-table-scroll` Stimulus controller toggles `.is-scrollable`, so a table
   that fits shows no fade over real content). Add `.dv-table__actions` to BOTH
   the actions `<th>` and each row's actions `<td>`.

   --dv-pin-bg is the opaque backing the pinned cell paints (so scrolled cells
   don't bleed through it) AND the solid end of the gradient — it tracks the
   row background (surface → hover sky-50 → selected accent-soft) so the pin and
   its fade always match the row underneath. Token-only; light-only. */
.os-table-wrap { --dv-pin-bg: var(--surface); }
.os-table-wrap thead th.os-table__actions { --dv-pin-bg: var(--canvas); }
.os-table-wrap tbody tr:hover { --dv-pin-bg: var(--sky-50); }
.os-table-wrap tbody tr.is-selected { --dv-pin-bg: var(--accent-soft); }

.os-table__actions {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--dv-pin-bg);
}
/* Transparency bleed: the scrolling body content fades out just before the
   pinned actions instead of hard-cutting under them. The ::before sits to the
   LEFT of the (opaque) cell so the fade falls over the CONTENT, never over the
   action icons themselves; its solid end tracks --dv-pin-bg so it matches the
   row (surface / hover / selected). Shown only when the wrap overflows. */
.os-table__actions::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 28px;
  transform: translateX(-100%);
  pointer-events: none;
  background: linear-gradient(to right, transparent, var(--dv-pin-bg));
  opacity: 0;
  transition: opacity var(--t-fast) ease;
}
.os-table-wrap.is-scrollable .os-table__actions::before { opacity: 1 !important; }

/* Touch / tablet have no hover, so the desktop hover-reveal pattern
   (`opacity-0 group-hover:opacity-100` on the action group) leaves row actions
   permanently invisible. Force them visible on ANY touch device (hover: none,
   any width) and across the whole tablet range (≤1024px — portrait 768 +
   landscape 1024). Mouse desktop (>1024) keeps the hover-reveal declutter. */
@media (hover: none), (max-width: 1024px) {
  .os-table__actions > * { opacity: 1 !important; }
}

/* ════════════════════ --stack — card-stack below 640px ═════════════════════
   BL-1b. Opt-in modifier on the `.dv-table-wrap` for NARROW ENTITY tables
   (bookings, divers, roster, renters, invoices). Below the sm boundary each
   <tr> collapses into a bordered card and each <td> becomes a label→value row,
   the label sourced from `data-label` on the cell. Wide financial grids DO NOT
   get this — they scroll (BL-1a) instead. Token-only; light-only. */
@media (max-width: 639px) {
  /* Drop the desktop table FRAME in card-stack. `.dv-table-wrap` (header_band.css)
     carries its own surface/border/radius so a desktop table reads as one framed
     card; but here each row is already its own bordered card, so that frame draws
     a redundant outer box around the stack and its white fill shows through the
     inter-card gaps. Make the wrap a transparent, borderless container so the
     cards float on the page canvas. */
  .os-table-wrap--stack {
    background: transparent;
    border: 0;
    border-radius: 0;
    overflow: visible;
  }

  /* Cards fill the wrap: drop the desktop min-width AND become a block that
     stretches to 100% (a bare table would shrink to its content width, leaving
     the cards narrower than the container). */
  .os-table-wrap--stack table { min-width: 0; width: 100%; display: block; }
  .os-table-wrap--stack tbody { display: block; }
  .os-table-wrap--stack thead { display: none; }

  .os-table-wrap--stack tbody tr {
    display: block;
    padding: var(--space-3, 12px) var(--space-4, 16px);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-md, 8px);
    background: var(--surface);
  }
  .os-table-wrap--stack tbody tr + tr { margin-top: var(--space-3, 12px); }

  /* Card-stack has no horizontal scroll, so the pinned/sticky action column is
     pointless here — worse, its stacking context paints cells OVER a sticky page
     header while the list scrolls (the row's "…/Fit gear" cell surfacing above
     the pinned title). Flatten every cell to normal flow in card mode.
     !important because the sticky rule that sets the high z is more specific. */
  .os-table-wrap--stack tbody td,
  .os-table-wrap--stack tbody th,
  .os-table-wrap--stack .os-table__actions {
    position: static !important;
    z-index: auto !important;
  }

  /* Cell → label/value row. data-label drives the field name on the left.
     Undo any desktop truncation/nowrap so long values (dive lists, notes) wrap
     instead of clipping; the value column may shrink and wrap under its label. */
  .os-table-wrap--stack tbody td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3, 12px);
    padding: var(--space-1, 4px) 0;
    border: 0;
    text-align: right;
    white-space: normal;
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    overflow-wrap: anywhere;
    /* Every cell aligns to the CARD's own inner padding. Kill the desktop
       per-cell horizontal padding that otherwise indents the title/status
       inconsistently: header_band's `td:first-child`/`:last-child` (16px) and
       `.bulk-actions … td:nth-child(2)` (12px), plus Tailwind px-3/pl-0/pr-3 —
       all out-specify the shorthand above, so force them off. */
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  /* Truly-empty cells (no value, e.g. an unset Stay glyph) are tagged by the
     dv-table-scroll controller and dropped so the card has no bare label rows. */
  .os-table-wrap--stack tbody td.os-td-empty { display: none; }
  .os-table-wrap--stack tbody td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    text-align: left;
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* A cell with no data-label (or explicitly blank) drops the label column. */
  .os-table-wrap--stack tbody td:not([data-label])::before, .os-table-wrap--stack tbody td[data-label=""]::before { content: none; }

  /* A leading drag-handle column (sortable catalog tables) is meaningless in the
     card stack and would render as an empty bordered stub above the name. Hide
     it and let the NEXT cell become the card header. */
  .os-table-wrap--stack tbody td:is(.cursor-grab, .os-cursor-grab), .os-table-wrap--stack tbody td:has([data-sortable-target="handle"]) { display: none; }

  /* Card header = the first CONTENT cell (the name/title): the literal first
     cell, OR the cell right after a hidden drag handle. Bold accent title, no
     label row, status pill kept on the right. */
  .os-table-wrap--stack tbody td:first-child, .os-table-wrap--stack tbody td:is(.cursor-grab, .os-cursor-grab) + td, .os-table-wrap--stack tbody td:has([data-sortable-target="handle"]) + td {
    padding-top: 0;
    padding-bottom: var(--space-2, 8px);
    margin-bottom: var(--space-1, 4px);
    border-bottom: 1px solid var(--hairline);
    align-items: center;
    /* The title is a heading, not a value — left-align it (the generic cell rule
       right-aligns values, which would shove an avatar-cell name to the right). */
    text-align: left;
  }
  .os-table-wrap--stack tbody td:first-child::before, .os-table-wrap--stack tbody td:is(.cursor-grab, .os-cursor-grab) + td::before, .os-table-wrap--stack tbody td:has([data-sortable-target="handle"]) + td::before { content: none; }
  .os-table-wrap--stack tbody td:first-child .os-table__primary, .os-table-wrap--stack tbody td:is(.cursor-grab, .os-cursor-grab) + td .os-table__primary, .os-table-wrap--stack tbody td:has([data-sortable-target="handle"]) + td .os-table__primary {
    font-weight: 600;
    color: var(--accent);
  }
  .os-table-wrap--stack tbody td:first-child .os-table__primary a, .os-table-wrap--stack tbody td:is(.cursor-grab, .os-cursor-grab) + td .os-table__primary a, .os-table-wrap--stack tbody td:has([data-sortable-target="handle"]) + td .os-table__primary a { color: var(--accent); }

  /* Cards stack vertically — no horizontal scroll — so the sticky pin is
     meaningless. Return the actions cell to normal flow and make it a COMPACT
     footer: buttons right-aligned and ALWAYS visible (no hover on touch, so the
     desktop `opacity-0 group-hover:opacity-100` reveal would hide them). Tight
     padding — a full divider + generous padding reads as dead space under two
     small icons. */
  .os-table-wrap--stack .os-table__actions {
    position: static;
    background: transparent;
    box-shadow: none;
    justify-content: flex-end;
    padding: 0;
    margin-top: var(--space-2, 8px);
    border-top: 0;
  }
  .os-table-wrap--stack .os-table__actions > * { opacity: 1 !important; }
}


/* ===== os_ds/components/tracker.css ===== */
/* os-tracker — completion tracker + jump: "2 of 4 ready" · meter · "Next unconfirmed →".
 * Source: `.swhead/.prog/.progbar/.jump` in reg-card-hybrid.html.
 * For any multi-entity completion flow (party check-in, per-diver waivers). The jump
 * button's scroll-to-next-incomplete behaviour is JS, not styling.
 * The mockup's track/label were sand-tinted (page-local); the gem version is neutral —
 * a product theme may retint the track.
 */

.os-tracker {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.os-tracker__count {
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  color: var(--text-secondary);
}

.os-tracker__bar {
  flex: none;
  width: 120px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-alt);
  overflow: hidden;
}

.os-tracker__fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width var(--t-med);
}

.os-tracker__jump {
  margin-left: auto;
  flex: none;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--accent-strong);
  font: inherit;
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}

.os-tracker__jump:hover {
  background: var(--accent-softer);
  border-color: var(--accent-border);
}


/* ===== os_ds/components/wizard-footer.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   wizard-footer.css — .os-wz-foot : the canonical WIZARD FOOTER, a sticky
   three-zone action bar (start · center · end) for a multi-step flow.

   Zones: START (Back + a destructive Discard/exit), CENTER (hint), END
   (running total + primary/secondary actions). Buttons are the gem's
   `.os-btn` family; this component owns the bar chrome + the total + the
   quiet destructive text-link. Pairs with `shared/patterns/_wizard_footer`.

   Token-driven; sits at the bottom of a scrolling wizard body (e.g. inside
   `.os-wz-couple`). No prefix but `.os-*`.
   ───────────────────────────────────────────────────────────────────── */

.os-wz-foot {
  position: sticky; bottom: 0; z-index: 20; background: var(--surface);
  border-top: 1px solid var(--hairline); box-shadow: 0 -1px 2px rgba(17, 24, 39, 0.03);
}
/* Full-width action row — the zones sit at the pane edges (with the band
   inset); a 1fr/auto/1fr grid keeps CENTER centred regardless of side widths. */
.os-wz-foot-inner {
  margin: 0 auto; padding: 14px 40px;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px;
}
.os-wz-foot .os-grp { display: flex; align-items: center; gap: 10px; }
.os-wz-foot .os-grp.os-start { justify-self: start; }
.os-wz-foot .os-grp.os-center { justify-self: center; }
.os-wz-foot .os-grp.os-end { justify-self: end; }

/* Hairline divider between Back and the destructive Discard link. */
.os-wz-foot-sep {
  display: inline-block; width: 1px; align-self: stretch; min-height: 20px;
  margin: 0 2px; background: var(--hairline-strong);
}
/* Quiet, never-filled destructive text-link (the Discard/exit). Reuses the
   `.os-btn--ghost` transparent base and only recolours it. */
.os-btn-destructive { color: var(--cancelled); }
.os-btn-destructive:hover { color: var(--cancelled); }
.os-btn--ghost.os-btn-destructive:hover { background: var(--cancelled-bg); }

/* Optional CENTER hint (keyboard shortcut / requirement note). */
.os-wz-foot-hint { display: inline-flex; align-items: center; gap: 7px; font-size: var(--text-xs); color: var(--muted-2); white-space: nowrap; }
.os-wz-foot-hint .os-kbd { font-family: var(--font-mono); font-size: var(--text-xs); padding: 2px 6px; background: var(--gray-100); border: 1px solid var(--hairline); border-radius: 4px; color: var(--muted); }
.os-wz-foot-hint .os-req { display: inline-flex; align-items: center; gap: 6px; color: var(--briefing); font-weight: 600; }

/* Running total in the END zone. */
.os-wz-foot .os-total { display: flex; align-items: baseline; gap: 9px; margin-right: 4px; }
.os-wz-foot .os-total .os-k { font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.os-wz-foot .os-total .os-v { font-family: var(--font-mono); font-size: var(--text-lg); font-weight: 500; color: var(--ink); letter-spacing: -0.01em; }


/* ===== os_ds/components/wizard-mobile-panes.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   wizard-mobile-panes.css — os-wizm-* : the reusable mobile pane primitives
   for a wizard's full-screen sub-flow (app-bar → date pane → pick-list →
   option-group). Each is a self-contained, token-driven .os-* contract; the
   consuming app renders the markup (or emits it from JS) and provides the
   surrounding wizard shell.

   Scope note: this ships the reusable PRIMITIVES only. The responsive shell
   that turns a desktop wizard into panes below 640px (footer-fold, stepper
   slimming) restyles a wizard footer/stepper that isn't fully in the gem yet,
   so it stays app-side until the wizard chrome is promoted. Group-summary /
   order-summary cards are app-specific and are NOT part of this component.

   Honours docs/mobile-primitives.md: `svh` shells, env(safe-area-inset-*),
   no position:fixed footer under an open keyboard (the shell owns that).
   ───────────────────────────────────────────────────────────────────── */

/* ── App-bar: back control + title/subtitle, optional trailing action ────
   Always laid out here; the consumer decides where/when it shows (e.g. only
   below the phone breakpoint). Clears the status bar / notch on a full-screen
   surface via the safe-area inset. */
.os-wizm-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  padding: calc(env(safe-area-inset-top) + 16px) 10px 12px;
  background: var(--surface);
}
.os-wizm-bar__back {
  width: 40px; height: 40px; flex: 0 0 auto;
  display: grid; place-items: center;
  border: 0; background: transparent; color: var(--ink);
  border-radius: var(--radius-sm); cursor: pointer;
}
.os-wizm-bar__back:active { background: var(--surface-alt); }
.os-wizm-bar__back[hidden] { display: none; }
.os-wizm-bar__ttl { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.os-wizm-bar__ttl .t {
  font-family: var(--font-display); font-size: var(--text-md); font-weight: 600;
  letter-spacing: -0.01em; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.os-wizm-bar__ttl .s {
  font-size: var(--text-xs); font-weight: 500; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* trailing escape hatch (e.g. Cancel), right-aligned */
.os-wizm-bar__cancel {
  margin-left: auto; flex: 0 0 auto; align-self: center;
  padding: 8px 6px; font-size: var(--text-sm); font-weight: 600;
  color: var(--muted); text-decoration: none; white-space: nowrap;
}
.os-wizm-bar__cancel:active { color: var(--ink); }

/* ── Pane scaffold: a full-screen pane (app-bar + scroll body + sticky foot).
   Not media-scoped — renders identically at any width. */
.os-wizm-pane { display: flex; flex-direction: column; min-height: 0; background: var(--canvas); }
.os-wizm-pane__body { flex: 1; min-height: 0; overflow-y: auto; }
.os-wizm-pane__eyebrow {
  padding: 16px 16px 0; font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted-2);
}
.os-wizm-pane__h {
  padding: 4px 16px 12px; font-family: var(--font-display); font-size: var(--text-xl);
  font-weight: 700; letter-spacing: -0.02em; color: var(--ink);
}
/* self-contained action bar — a full-width primary button pinned at the pane
   foot (the shell, not this primitive, owns keyboard-safe positioning). */
.os-wizm-foot {
  flex: 0 0 auto; background: var(--surface); border-top: 1px solid var(--hairline);
  padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
}
.os-wizm-foot__meta {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--text-xs); color: var(--muted); margin-bottom: 8px;
}
.os-wizm-foot .os-btn { width: 100%; height: 50px; }
.os-wizm-foot .os-btn--primary { justify-content: space-between; }
/* keep each label/value cluster (e.g. "Fri, Jul 31 ›") on one line */
.os-wizm-foot .os-btn > span { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; min-width: 0; }

/* ══════════════════════════════════════════════════════════════
   1 · date pane — date-only picker (quick chips + month calendar)
   ══════════════════════════════════════════════════════════════ */
.os-wizm-date__chips { display: flex; gap: 8px; padding: 0 16px 14px; }
.os-wizm-date__chip {
  flex: 1; height: 52px; border-radius: var(--radius);
  border: 1px solid var(--hairline-strong); background: var(--surface);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  font: inherit; cursor: pointer; color: var(--ink);
}
.os-wizm-date__chip b { font-size: var(--text-sm); font-weight: 600; }
.os-wizm-date__chip span { font-size: var(--text-xs); color: var(--muted); }
.os-wizm-date__chip.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.os-wizm-date__chip.is-on span { color: var(--accent); }

.os-wizm-cal {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); margin: 0 16px; padding: 14px; box-shadow: var(--shadow-sm);
}
.os-wizm-cal__hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.os-wizm-cal__hd b { font-size: var(--text-sm); font-weight: 600; }
.os-wizm-cal__nav { display: flex; gap: 6px; }
.os-wizm-cal__nav button {
  width: 30px; height: 30px; border-radius: var(--radius);
  border: 1px solid var(--hairline); background: var(--surface); display: grid; place-items: center;
  color: var(--muted); cursor: pointer;
}
.os-wizm-cal__dow { display: grid; grid-template-columns: repeat(7,1fr); gap: 2px; margin-bottom: 4px; }
.os-wizm-cal__dow span { text-align: center; font-size: var(--text-2xs); font-weight: 600; color: var(--muted-2); text-transform: uppercase; }
.os-wizm-cal__days { display: grid; grid-template-columns: repeat(7,1fr); gap: 3px; }
.os-wizm-cal__day {
  aspect-ratio: 1; display: grid; place-items: center; font-size: var(--text-sm);
  border-radius: var(--radius); color: var(--ink); font-variant-numeric: tabular-nums;
  border: 0; background: none; font: inherit; cursor: pointer;
}
.os-wizm-cal__day.is-mut { color: var(--hairline-strong); pointer-events: none; }
.os-wizm-cal__day.is-today { border: 1.5px solid var(--accent-border); }
.os-wizm-cal__day.is-sel { background: var(--accent); color: var(--surface); font-weight: 600; }

/* ══════════════════════════════════════════════════════════════
   2 · pick-list — menu (category + search + row with a single +)
   ══════════════════════════════════════════════════════════════ */
.os-wizm-pick__bar {
  display: flex; gap: 8px; padding: 12px 16px;
  background: var(--surface); border-bottom: 1px solid var(--hairline); position: sticky; top: 0; z-index: 1;
}
.os-wizm-pick__drop {
  flex: 1; height: 40px; border-radius: var(--radius-pill);
  border: 1px solid var(--hairline-strong); background: var(--surface);
  display: flex; align-items: center; gap: 6px; padding: 0 14px;
  font: inherit; font-size: var(--text-sm); font-weight: 600; color: var(--ink); cursor: pointer;
}
.os-wizm-pick__drop .chev { margin-left: auto; color: var(--muted); display: inline-flex; }
.os-wizm-pick__search {
  width: 40px; height: 40px; border-radius: var(--radius-pill);
  border: 1px solid var(--hairline-strong); background: var(--surface); display: grid; place-items: center;
  color: var(--ink); cursor: pointer;
}
.os-wizm-pick__sec { padding: 14px 16px 6px; font-size: var(--text-md); font-weight: 700; letter-spacing: -0.01em; color: var(--ink); }

.os-wizm-pick-row { display: flex; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--hairline-2); align-items: flex-start; }
.os-wizm-pick-row__info { flex: 1; min-width: 0; }
.os-wizm-pick-row__info b { font-size: var(--text-sm); font-weight: 600; display: block; margin-bottom: 2px; color: var(--ink); }
.os-wizm-pick-row__info p { font-size: var(--text-xs); color: var(--muted); line-height: 1.4; margin-bottom: 6px; }
.os-wizm-pick-row__info .pr { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 600; color: var(--ink); }
.os-wizm-pick-row__info .pr s { color: var(--muted-2); font-weight: 400; margin-left: 6px; font-size: var(--text-xs); }

/* the single + affordance — filled (adds) / ghost (opens detail) / qty (in cart) */
.os-wizm-plus, .os-wizm-plus--ghost, .os-wizm-qtymini {
  width: 38px; height: 38px; border-radius: var(--radius-pill); flex: 0 0 auto;
  display: grid; place-items: center; font: inherit; cursor: pointer;
}
.os-wizm-plus { background: var(--accent); color: var(--surface); border: 0; box-shadow: var(--shadow-sm); }
.os-wizm-plus--ghost { border: 1.5px solid var(--accent-border); color: var(--accent); background: var(--surface); }
.os-wizm-qtymini {
  border: 1.5px solid var(--accent); color: var(--accent); background: var(--surface);
  font-size: var(--text-sm); font-weight: 700; font-variant-numeric: tabular-nums;
}

/* ══════════════════════════════════════════════════════════════
   3 · option-group — detail (Pick-1 radios + optional checks + note + qty)
   ══════════════════════════════════════════════════════════════ */
.os-wizm-optsec { background: var(--surface); border-bottom: 1px solid var(--hairline); }
.os-wizm-optsec:last-of-type { border-bottom: 0; }
.os-wizm-optsec__hd { display: flex; align-items: center; justify-content: space-between; padding: 16px 16px 8px; }
.os-wizm-optsec__hd b { font-size: var(--text-md); font-weight: 700; color: var(--ink); }

.os-wizm-badge { font-size: var(--text-xs); font-weight: 600; padding: 3px 10px; border-radius: var(--radius-pill); }
.os-wizm-badge--pick { background: var(--confirmed-bg); color: var(--confirmed); }
.os-wizm-badge--opt { background: var(--surface-alt); color: var(--muted); }
.os-wizm-badge--done { background: var(--accent); color: var(--surface); }

/* rows: a single hairline between SECTIONS, none between rows within a section */
.os-wizm-opt-row { display: flex; align-items: center; gap: 12px; padding: 11px 16px; }
.os-wizm-opt-row__info { flex: 1; }
.os-wizm-opt-row__info b { font-size: var(--text-sm); font-weight: 500; display: block; color: var(--ink); }
.os-wizm-opt-row .pr { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); }
.os-wizm-radio, .os-wizm-check { width: 22px; height: 22px; flex: 0 0 auto; border: 2px solid var(--hairline-strong); }
.os-wizm-radio { border-radius: 50%; }
.os-wizm-radio.is-on { border-color: var(--accent); background: radial-gradient(var(--accent) 42%, var(--surface) 46%); }
.os-wizm-check { border-radius: 6px; }
.os-wizm-check.is-on { background: var(--accent); border-color: var(--accent); display: grid; place-items: center; color: var(--surface); }

.os-wizm-note {
  margin: 12px 16px; border: 1px solid var(--hairline-strong); border-radius: var(--radius);
  min-height: 74px; padding: 11px; font: inherit; font-size: var(--text-sm); color: var(--ink);
  width: calc(100% - 32px); resize: vertical;
}
.os-wizm-note::placeholder { color: var(--muted-2); }
.os-wizm-qtybar { display: flex; align-items: center; justify-content: center; gap: 22px; padding: 14px; }
.os-wizm-qtybar button {
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  border: 1px solid var(--hairline-strong); background: var(--surface); display: grid; place-items: center;
  font-size: var(--text-xl); color: var(--accent); cursor: pointer;
}
.os-wizm-qtybar b { font-size: var(--text-lg); font-variant-numeric: tabular-nums; min-width: 30px; text-align: center; }


/* ===== os_ds/components/wizard-mobile.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   wizard-mobile.css — the ≤640px SHELL that turns a desktop wizard
   (`.os-wz-couple` frame + `.os-wz-bars` stepper + `.os-wz-foot` footer +
   `.os-wz-body`) into the full-screen PANE model: an app-bar (`.os-wizm-bar`),
   a slim tappable segmented progress strip, an `svh` scroll body, and a
   keyboard-safe one-row footer that folds its total into the primary button.

   A pure responsive LAYER — every rule is inside the @media block (plus the
   wizard app-bar which is display:none until the breakpoint). Desktop is
   untouched. Pairs with `wizard_mobile_controller` (identifier "wizm"), which
   sizes the shell from `visualViewport.height` (--wizm-vh) so the flex-pinned
   footer stays above an open keyboard, mirrors step state into the app-bar,
   and folds each footer total (see docs/mobile-primitives.md).

   Chrome selectors are dual `:is(.wz-x, .os-wz-x)` to match `.os-wz-couple`
   and the `_stepper_bars` partial (which still emits `.wz-*`); the footer uses
   the clean `.os-wz-foot` from v1.20.0.
   ───────────────────────────────────────────────────────────────────── */

/* The wizard's own app-bar is hidden until the phone breakpoint; standalone
   overlay panes keep theirs via the base `.os-wizm-bar` rule. */
:is(.wz-flow, .os-wz-flow) > .os-wizm-bar { display: none; }

@media (max-width: 640px) {
  /* Pane surface: a fixed, viewport-tall column (height from --wizm-vh, 100svh
     fallback), taking over the screen for the focused flow. */
  :is(.wz-flow, .os-wz-flow) {
    position: fixed; inset: 0; z-index: 60;
    height: var(--wizm-vh, 100svh);
    height: var(--wizm-vh, 100vh);
    background: var(--canvas);
  }
  :is(.wz-flow, .os-wz-flow) > .os-wizm-bar { display: flex; }

  /* Segmented progress: the stepper band slims to bare, tappable bars — labels
     hidden, thin bars, a real touch target. */
  :is(.wz-flow, .os-wz-flow) > :is(.wz-couple, .os-wz-couple) {
    margin-top: 0; padding: 0 16px 12px; background: var(--surface); border-bottom: 1px solid var(--hairline);
  }
  :is(.wz-flow, .os-wz-flow) > :is(.wz-couple, .os-wz-couple).is-scrolled { margin-top: 0; }
  :is(.wz-couple, .os-wz-couple):is(.wz-couple-card, .os-wz-couple-card) { padding-top: 0; padding-bottom: 12px; }
  :is(.wz-bars, .os-wz-bars) { max-width: none; }
  :is(.wz-bars-inner, .os-wz-bars-inner) { max-width: none; gap: 6px; }
  :is(.wz-seg, .os-wz-seg) { gap: 0; pointer-events: auto; padding: 7px 0; cursor: pointer; }
  :is(.wz-seg, .os-wz-seg) :is(.lbrow, .os-lbrow) { display: none; }
  :is(.wz-seg, .os-wz-seg):is(.on, .os-on) :is(.bar, .os-bar) { height: 5px; box-shadow: none; }

  /* Body: the svh scroll region, edge-to-edge (drop the desktop reading inset). */
  :is(.wz-body, .os-wz-body) { background: var(--canvas); scrollbar-gutter: auto; }
  :is(.wz-inner, .os-wz-inner) { max-width: none; padding: 16px; }
  :is(.wz-inner, .os-wz-inner):is(.narrow, .os-narrow) { max-width: none; }

  /* Footer: flex-pinned (NOT position:fixed — the controller's --wizm-vh keeps
     it above the keyboard), one row — a compact Back on the left, the primary
     Next filling the rest with its folded total. */
  .os-wz-foot { position: static; flex: 0 0 auto; box-shadow: 0 -4px 16px -8px rgba(2, 6, 23, 0.12); }
  .os-wz-foot-inner {
    display: flex; flex-direction: row; align-items: center; gap: 10px;
    padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
  }
  .os-wz-foot .os-grp.os-center { display: none; }
  .os-wz-foot .os-grp.os-start { display: none; }
  .os-wz-foot .os-grp.os-start:has(.os-btn--secondary) { display: flex; order: -1; flex: 0 0 auto; align-items: center; }
  .os-wz-foot .os-grp.os-start .os-btn-destructive,
  .os-wz-foot .os-wz-foot-sep { display: none; }
  .os-wz-foot .os-grp.os-start .os-btn--secondary {
    background: transparent; border-color: transparent; box-shadow: none;
    color: var(--muted); height: auto; padding: 6px 12px; font-size: var(--text-sm); font-weight: 600;
  }
  .os-wz-foot .os-grp.os-end { flex: 1 1 auto; min-width: 0; align-items: stretch; }
  .os-wz-foot .os-grp.os-end .os-btn { width: 100%; justify-content: center; height: 50px; font-size: var(--text-md); }

  /* Total folded into the primary button (one row). The controller tags the
     footer `.os-wizm-foot--folded` and mirrors the total into `.os-wizm-foot-amt`. */
  .os-wizm-foot-amt { display: none; }
  .os-wz-foot.os-wizm-foot--folded .os-total { display: none; }
  .os-wz-foot.os-wizm-foot--folded .os-grp.os-end .os-btn--primary { justify-content: flex-start; }
  .os-wz-foot.os-wizm-foot--folded .os-wizm-foot-amt { display: inline; margin-left: auto; font-family: var(--font-mono); font-weight: 600; }
}


/* ===== os_ds/components/workpage.css ===== */
/* ─── Work page archetype (.os-workpage) ─────────────────────────────────────
   THE sitewide "Form + Summary + Footer" page shape: a fluid work/content
   column, a fixed ~320px read-only summary that mirrors live state, and a
   persistent action footer. Use for any create / edit / checkout / multi-step
   page. An optional tinted page hero (.os-page-hero) may sit above it, with the
   first card floating up onto it.

   Origin: guest arrival-wizard / registration-card / transfers redesign
   (2026-07-25). `.rd-body` / `.rd-main` / `.rd-rail` in mockups/arrival.html
   and mockups/transfers.html; `.floatwrap` in mockups/reg-card-hybrid.html is
   the --a2 (summary-as-banner) case.

   API
     .os-workpage            the grid: work column + summary rail
     .os-workpage__main      fluid content column (sectioned cards/inputs)
     .os-workpage__rail      fixed 320px sticky read-only summary (--a1)
     .os-workpage__banner    full-width summary above the work (--a2)
     .os-workpage__footer    persistent action bar under the page
   Variants
     --a1   summary lives in the rail (narrow + glanceable)   [default shape]
     --a2   summary spans the top as a banner (wide/tabular, or the thing
            being confirmed); the grid collapses to a single column

   Composition notes
     · Elevation: a __banner or the first __main card that overlaps a tinted
       hero should carry `box-shadow: var(--elevation-overlay)` — the one
       "lifts onto a tinted header" shadow. Nothing else on the page elevates.
     · The rail is sticky only while the two-column grid is live; below 1100px
       it becomes static and stacks under the work column.
     · Light-only, token-driven.
   ────────────────────────────────────────────────────────────────────────── */

.os-workpage {
  display: grid;
  grid-template-columns: 1fr 320px;
  column-gap: var(--space-6);
  row-gap: var(--space-5);
  align-items: start;
  min-width: 0;
}

.os-workpage__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  min-width: 0;
}

.os-workpage__rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  position: sticky;
  top: var(--space-5);
  min-width: 0;
}

/* Full-width summary above the work column (the --a2 placement). Spans both
   grid tracks so it also works when dropped inside the default grid. */
.os-workpage__banner {
  grid-column: 1 / -1;
  min-width: 0;
}

.os-workpage__footer {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) 0 0;
  min-width: 0;
}

/* ── --sticky: the footer is a GATE, so it has to be reachable ────────────
   The anatomy above calls `__footer` "a persistent action bar under the page",
   and on a short page it is. On a long one it was not: a registration card
   with four adults runs several screens, and the bar carrying "Complete
   check-in" only appeared once you had scrolled to the very end — so the state
   of the gate ("0 of 4 adults signed") and the action that closes it were both
   invisible during the entire task they describe.

   Sticky rather than fixed: it stays inside the content column, so it cannot
   overlap a rail or fight a drawer for the viewport, and it stops being sticky
   naturally at the end of the page. The surface + top hairline + shadow are
   what stop the page's own content showing through it — an in-flow footer
   needs none of them, a floating one needs all three. Opt-in, because a footer
   holding only minor actions ("Print", "Export") does not deserve to occupy a
   permanent band of every screen. */
.os-workpage__footer--sticky {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin-top: var(--space-4);
  padding: var(--space-3) var(--gutter);
  /* Cancels the content column's inset so the bar spans the full column edge
     to edge, then re-pads its own content back to that inset above. */
  margin-inline: calc(var(--gutter) * -1);
  background: var(--surface);
  border-top: 1px solid var(--hairline);
  box-shadow: var(--elevation-overlay);
}

/* ── --a1: summary in the rail (default). Explicit for callsite clarity. ── */
.os-workpage--a1 {
  grid-template-columns: 1fr 320px;
}

/* ── --a2: summary as a top banner — no rail, one column throughout. ── */
.os-workpage--a2 {
  grid-template-columns: 1fr;
  row-gap: var(--space-5);
}
.os-workpage--a2 .os-workpage__rail {
  position: static;
}

/* ── Collapse: below 1100px the rail stops being a column. ── */
@media (max-width: 1100px) {
  .os-workpage {
    grid-template-columns: 1fr;
    row-gap: var(--space-5);
  }
  .os-workpage--a1 {
    grid-template-columns: 1fr;
  }
  .os-workpage__main {
    gap: var(--space-5);
  }
  .os-workpage__rail {
    position: static;
  }
}


/* ===== os_ds/components/wz-couple.css ===== */
/* ─────────────────────────────────────────────────────────────────────
   wz-couple.css — the WIZARD STEP FRAME: a stepper coupled to a scrolling
   body, condensing cleanly as the body scrolls.

   SUPERSEDES `.os-stickybar`. That component made ONE element do everything —
   `position: sticky` + its own `padding-block` as the rhythm (40/20 in flow,
   16/16 stuck). Animating the element's OWN padding meant its height changed
   right where the stick math measured it, so near a scroll boundary the height
   drop clamped scrollTop and the bar flip-flopped (a vertical jitter). Two
   padding states cascading on one element — hard to reason about, and the bug
   was structural.

   This model separates the concerns instead — proven in DiveOS (bookings/new,
   dive_trips/:id/run), promoted here verbatim:

     FRAME      .os-wz-flow   flex column; the stepper is a fixed flex-child,
                              .os-wz-body is the ONLY scroller. The stepper stays
                              put STRUCTURALLY — no position:sticky, no self-
                              measuring stick math.
     STEPPER    .os-wz-couple the stepper region. Owns the SEPARATOR (padding-
                              bottom + border-bottom): the divider belongs to the
                              fixed header, not the scrolling body.
     CHROME     .os-wz-couple-card / .bare   card vs bare presentation of the
                              inner .os-wz-bars — chrome decoupled from layout.
     SCROLLER   .os-wz-body   flex:1; overflow-y:auto; scrollbar-gutter:stable.
     COLUMN     .os-wz-inner  centered content column inside the body.

   CONDENSE. Breathing room above the stepper (margin-top: 2rem) collapses to 0
   when the body scrolls — the `wz-couple` controller toggles `.is-scrolled`.
   Because it moves a MARGIN, not the stepper's own padding, the stepper's height
   never changes, so it cannot feed back into the scroll and jitter. Without JS
   the margin simply stays at its roomy 2rem — it never breaks, it just never
   condenses.

     <div class="os-wz-flow" data-controller="wz-couple">
       <div class="os-wz-couple os-wz-couple-card">
         <div class="os-wz-bars">…stepper…</div>
       </div>
       <form>…                              (display:contents)
         <div class="os-wz-body" data-wz-couple-target="body">
           <div class="os-wz-inner">…</div>
         </div>
         …footer…
       </form>
     </div>
───────────────────────────────────────────────────────────────────── */

/* Selectors are dual-aliased `:is(.wz-x, .os-wz-x)` — the gem's convention for a
   component promoted with existing bare consumers (DiveOS markup uses bare
   `.wz-*`; new consumers use `.os-wz-*`). Both bind; nobody has to migrate markup
   to adopt the gem copy. */

/* FRAME. The stepper (fixed) → body (scrolls) → footer (pinned) column. The
   <form> wraps the body + footers, so it is display:contents to let them
   participate as flex children of the flow. */
:is(.wz-flow, .os-wz-flow) { display: flex; flex-direction: column; flex: 1; min-height: 0; }
:is(.wz-flow, .os-wz-flow) > form { display: contents; }

/* STEPPER REGION. width:100% so that inside the flex-column flow the auto
   side-margins don't override align-items:stretch and shrink-wrap it. */
/* Full-width header BAND so its bottom separator spans the pane edge-to-edge;
   the stepper CONTENT (.os-wz-bars) self-centers at the reading width. Capping
   the couple at --content-max clipped the separator to 768px. */
:is(.wz-couple, .os-wz-couple) { width: 100%; margin: 0 auto; padding: 0 40px; }
:is(.wz-bars, .os-wz-bars) { width: 100%; max-width: var(--content-max); margin-inline: auto; }
:is(.wz-couple-detached, .os-wz-couple-detached) { padding-top: 22px; padding-bottom: 4px; }

/* CHROME variants — card (stepper in a card atop the content) vs bare. */
:is(.wz-couple-card, .os-wz-couple-card) { padding-top: 40px; padding-bottom: 6px; }
:is(.wz-couple-card, .os-wz-couple-card) > :is(.wz-bars, .os-wz-bars) {
  max-width: 720px; margin: 0 auto;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); box-shadow: 0 1px 2px rgba(17, 24, 39, .04);
  padding: 16px 24px; box-sizing: border-box;
}
:is(.wz-couple-card, .os-wz-couple-card):is(.bare, .os-bare) > :is(.wz-bars, .os-wz-bars) { background: transparent; border: 0; box-shadow: none; padding: 4px 0; }

/* CONDENSE. Breathing room above the stepper at rest; collapses to flush once
   the body scrolls (the `wz-couple` controller toggles .is-scrolled). The page
   separator is semantically the stepper's bottom edge — it lives on the fixed
   header region, carried here as padding-bottom (matching the space-y margin
   stripped off the body) so the hairline lands after the gap, not hugging the
   stepper. */
:is(.wz-flow, .os-wz-flow) > :is(.wz-couple, .os-wz-couple) {
  margin-top: 2rem;
  transition: margin-top .2s ease;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--hairline);
}
:is(.wz-flow, .os-wz-flow) > :is(.wz-couple, .os-wz-couple):is(.is-scrolled, .os-is-scrolled) { margin-top: 0; }

@media (prefers-reduced-motion: reduce) {
  :is(.wz-flow, .os-wz-flow) > :is(.wz-couple, .os-wz-couple) { transition: none; }
}

/* SCROLLER. scrollbar-gutter: stable reserves the scrollbar track always, so the
   body width doesn't reflow when the scrollbar appears/disappears — the OTHER
   axis of the condense bounce. */
:is(.wz-body, .os-wz-body) { flex: 1; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; background: var(--canvas); }

/* form_with's hidden CSRF/method inputs make the body a subsequent sibling, so a
   theme form-divide utility can hand it a border-top + margin-top. The separator
   + gap live on the couple (above), so strip them here. */
:is(.wz-flow, .os-wz-flow) form:is(.form, .os-form) > :is(.wz-body, .os-wz-body) { margin-top: 0 !important; border-top: 0 !important; }

/* CONTENT COLUMN. */
:is(.wz-inner, .os-wz-inner) { max-width: var(--content-max); margin: 0 auto; padding: 30px 40px 40px; }
:is(.wz-inner, .os-wz-inner):is(.narrow, .os-narrow) { max-width: 880px; }

/* ── Segmented bar stepper (the `.os-wz-bars` content) ─────────────────
   Thin per-step bars: `.os-wz-seg` (a real <button> when navigable) with a
   `.os-bar`, a `.os-lbrow` (label + `.os-tk` check pip), states done/on/nav.
   Promoted from the DiveOS booking wizard so the stepper is self-contained. */
:is(.wz-bars-inner, .os-wz-bars-inner) { display: flex; gap: 10px; max-width: var(--content-max); margin: 0 auto; }
:is(.wz-seg, .os-wz-seg) { flex: 1; display: flex; flex-direction: column; gap: 9px; min-width: 0; background: none; border: 0; padding: 0; cursor: default; font: inherit; }
:is(.wz-seg, .os-wz-seg) :is(.bar, .os-bar) { height: 4px; border-radius: 4px; background: var(--hairline); transition: background .25s cubic-bezier(.22,1,.36,1), height .15s ease, box-shadow .15s ease; }
:is(.wz-seg, .os-wz-seg):is(.done, .os-done) :is(.bar, .os-bar), :is(.wz-seg, .os-wz-seg):is(.on, .os-on) :is(.bar, .os-bar) { background: var(--accent); }
:is(.wz-seg, .os-wz-seg):is(.on, .os-on) :is(.bar, .os-bar) { height: 6px; box-shadow: 0 0 0 3px var(--accent-softer); }
:is(.wz-seg, .os-wz-seg) :is(.lbrow, .os-lbrow) { display: flex; align-items: center; gap: 5px; min-width: 0; }
:is(.wz-seg, .os-wz-seg) :is(.lb, .os-lb) { font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .12s ease; }
:is(.wz-seg, .os-wz-seg):is(.done, .os-done) :is(.lb, .os-lb) { color: var(--muted); }
:is(.wz-seg, .os-wz-seg):is(.on, .os-on) :is(.lb, .os-lb) { color: var(--accent); font-weight: 700; }
:is(.wz-seg, .os-wz-seg) :is(.tk, .os-tk) { display: none; align-items: center; justify-content: center; width: 13px; height: 13px; border-radius: 50%; background: var(--accent); color: var(--surface); flex-shrink: 0; }
:is(.wz-seg, .os-wz-seg):is(.done, .os-done) :is(.tk, .os-tk) { display: inline-flex; }
:is(.wz-seg, .os-wz-seg):is(.on, .os-on) :is(.lbrow, .os-lbrow)::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
:is(.wz-seg, .os-wz-seg):is(.nav, .os-nav) { cursor: pointer; }
:is(.wz-seg, .os-wz-seg):is(.nav, .os-nav):hover :is(.bar, .os-bar) { background: var(--accent); }
:is(.wz-seg, .os-wz-seg):is(.done, .os-done):is(.nav, .os-nav):hover :is(.bar, .os-bar) { background: var(--accent-strong); }
:is(.wz-seg, .os-wz-seg):is(.nav, .os-nav):hover :is(.lb, .os-lb) { color: var(--ink); }
:is(.wz-seg, .os-wz-seg):is(.done, .os-done):is(.nav, .os-nav):hover :is(.lb, .os-lb) { color: var(--accent); }
:is(.wz-seg, .os-wz-seg):focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); border-radius: 6px; }

