:root {
  /* Base palette - a richer near-black rather than flat #000, so the app has
     some depth instead of reading as a plain terminal.
     2026-09-10: retoned per an explicit "clean, modern, less flashy, Apple
     like" request. --bg/--panel/--border shifted from blue-black toward a
     more neutral near-black (a cooler blue-black reads more like a glowing
     screen), and --accent was desaturated from a near-neon teal to a calmer
     one - per the request, that neon teal against near-black was probably
     doing most of the "flashy" work. This is the DEFAULT theme only,
     deliberately - see each [data-theme=...] block below for why the other
     five presets were each touched by a different amount (some barely at
     all): every preset still needs to serve its own real purpose (night
     vision, glare, monochrome) before it needs to look "nice," and this app
     still defaults dark - none of that changed, only how loud the dark
     default reads. Every token NAME below is unchanged; only values moved,
     so nothing else in this file needed to change. */
  --bg: #0D0E12;
  --panel: #161A20;
  --tile: #161A20;
  --border: #232830;
  --accent: #22D6BC;
  --amber: #FFA630;
  --danger: #FF4D5E;
  --text: #EEF2F5;
  --muted: #8A94A6;
  --btn-ink: #04231E;    /* text color for a solid --accent button */
  --danger-ink: #2A0A0A; /* text color for a solid --danger button (recording state) */
  --secondary-bg: #1C1F26; /* background for secondary buttons and the lock chrome */
  --radius: 12px;
  --radius-sm: 10px; /* matches the design reference's near-universal button/input/chip radius - unchanged, buttons keep their exact shape */
  --cell-h: 44px;   /* one instruments-grid row; JS reads this to match its drag/snap math - kept smaller than before since the grid no longer scrolls */
  --grid-gap: 10px; /* untouched - JS's row-ceiling math (updateMaxRows() in app.js) is tuned around this exact spacing on the non-scrolling Instruments grid, so it's out of scope for this pass even though "more breathing room" was part of the ask; see PLANS.md */
}

/* ---------- color theme presets ----------
   Same token names throughout, so every existing var(--...) rule below picks
   these up automatically - no other selector needs to change per theme. */
/* 2026-09-10 "clean/less flashy" pass: left entirely unchanged. Mono has no
   saturated accent to tone down by design (it IS the "strip all color"
   theme), and its border/brightness contrast is already load-bearing, not
   decorative - see the --muted comment below, brightness is carrying
   hierarchy that color normally would. Nothing here was reading as
   "flashy" in the first place. */
[data-theme="mono"] {
  --bg: #080908;
  --panel: #101110;
  --tile: #101110;
  --border: #3a3a3a;
  --accent: #e8e8e8;
  --amber: #ffffff;
  --danger: #ffffff;
  --text: #f0f0f0;
  --muted: #8a8a8a; /* kept lighter than default's --muted - mono loses all color cues, so brightness has to carry more of the hierarchy */
  --btn-ink: #0a0a0a;
  --danger-ink: #0a0a0a;
  --secondary-bg: #1c1c1c;
}

/* 2026-09-10 "clean/less flashy" pass: only --border softened (was a fairly
   saturated amber-brown outline against near-black; brought a shade closer
   to --panel so it still reads as an outline without shouting). --accent/
   --amber/--danger deliberately left alone - a vivid amber glow IS this
   theme's whole identity (a classic amber-CRT instrument look), not
   incidental "flashy" to tone down; diluting it would undercut the theme's
   actual purpose the same way the brief warned against for daylight/red. */
[data-theme="amber"] {
  --bg: #0a0704;
  --panel: #120d06;
  --tile: #120d06;
  --border: #423012;
  --accent: #ffb020;
  --amber: #ffe08a;
  --danger: #ff6a00;
  --text: #ffd28a;
  --muted: #9c7530;
  --btn-ink: #241500;
  --danger-ink: #1a0a00;
  --secondary-bg: #1e1508;
}

/* Classic nautical "red at night" mode - preserves night vision on deck.
   Single-hue like mono, so warn/go and lift/header need the same
   escalation-safe overrides further down.
   2026-09-10 "clean/less flashy" pass: deliberately left 100% untouched.
   This is the one theme whose entire job is a real physiological effect
   (preserving dark-adapted night vision), not a look - every value here was
   presumably chosen for that, and there's no way to verify a retone against
   an actual dark deck from this environment. Not worth the risk for a
   cosmetic goal; flagged in PLANS.md rather than guessed at. */
[data-theme="red"] {
  --bg: #0a0404;
  --panel: #120606;
  --tile: #120606;
  --border: #4a1010;
  --accent: #ff4242;
  --amber: #ff8080;
  --danger: #ff1a1a;
  --text: #ffb3b3;
  --muted: #9c4040;
  --btn-ink: #1a0000;
  --danger-ink: #1a0000;
  --secondary-bg: #1e0808;
}

/* 2026-09-10 "clean/less flashy" pass: --border and --accent softened by
   the same reasoning as the default theme above (ocean's own accent is a
   similarly vivid, similarly "neon" sky-cyan, and its border was a fairly
   saturated blue outline) - kept clearly ocean-blue, just calmer. Nothing
   else touched; --amber/--danger/--text keep doing their existing jobs. */
[data-theme="ocean"] {
  --bg: #050a10;
  --panel: #0a1220;
  --tile: #0a1220;
  --border: #2b3f4f;
  --accent: #46c3dd;
  --amber: #ffd24f;
  --danger: #ff6b6b;
  --text: #d4ecff;
  --muted: #5f819e;
  --btn-ink: #001a26;
  --danger-ink: #2a0a0a;
  --secondary-bg: #122032;
}

/* Bright, light-background palette for high-glare daylight readability -
   the opposite tradeoff from every other theme here, which assume a dim
   cockpit/night use case.
   2026-09-10 "clean/less flashy" pass: deliberately left untouched. The
   "near-black + neon" critique this pass responds to doesn't describe a
   light theme in the first place - there's nothing here to tone down. */
[data-theme="daylight"] {
  --bg: #f4f4ef;
  --panel: #e7e7df;
  --tile: #ffffff;
  --border: #adada4;
  --accent: #046b4e;
  --amber: #8f5200;
  --danger: #a8281c;
  --text: #15181a;
  --muted: #5d6266;
  --btn-ink: #ffffff;
  --danger-ink: #ffffff;
  --secondary-bg: #dbdbd1;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', -apple-system, sans-serif;
  overscroll-behavior: none;
  height: 100%;
}

#app {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100vh;
  max-width: 520px;
  margin: 0 auto;
}

.page {
  display: none;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  padding: 14px;
  gap: 12px;
  overflow-y: auto;
}
.page.active { display: flex; }

/* No .page-header anywhere anymore, matching the design reference - every
   page starts straight into its content; GPS status lives in the status
   tile, editing is entered from Settings, Lock is a tab bar item. */

.tile-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: var(--cell-h);
  gap: var(--grid-gap);
  position: relative;
}

/* Edit mode: dashed frame + ghosted cell lines so the user can see the grid
   they're placing blocks on. Pure decoration - an outline (not a border) and
   no padding, so the grid's content box - and therefore JS's cell-size math
   in gridMetrics() - doesn't shift by a few px the moment edit mode toggles. */
.tile-grid.edit-mode {
  outline: 1px dashed var(--border);
  outline-offset: 4px;
  border-radius: var(--radius);
  background-image:
    repeating-linear-gradient(to right, var(--border) 0 1px, transparent 1px 25%),
    repeating-linear-gradient(to bottom, var(--border) 0 1px, transparent 1px calc(var(--cell-h) + var(--grid-gap)));
  background-size: 100% 100%, 100% calc(var(--cell-h) + var(--grid-gap));
}
/* In edit mode, tiles are dragged, not tapped - stop the browser's own touch
   scroll/zoom gestures from fighting the pointer drag, and stop buttons
   inside a tile (Zero, Start/Sync, Reset) from intercepting the drag. */
.tile-grid.edit-mode .tile { touch-action: none; cursor: grab; }
/* Functional tile buttons (Zero, Start/Sync, Reset) shouldn't fire mid-drag,
   but the remove button IS a button and must stay clickable in edit mode -
   the blanket rule this used to be (`.tile button { pointer-events:none }`)
   accidentally disabled the X too, so tapping it did nothing. */
.tile-grid.edit-mode .tile button:not(.tile-remove-btn) { pointer-events: none; }

.tile {
  background: var(--tile);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  text-align: center;
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
/* Every tile's real position/visibility is set by app.js's applyLayout()
   (inline grid-column/grid-row/display), which only runs once the script
   has parsed and loadLayout() has resolved - there's a real gap between
   first paint and then (worse on a slow connection, since a render-blocking
   <script src="https://unpkg.com/leaflet..."> sits before app.js in the
   HTML). Until applyLayout() runs, TIMER and LINE have no positioning of
   their own and fall into plain grid auto-placement alongside the other six
   tiles - both default *hidden* (DEFAULT_LAYOUT), so left unstyled here
   they'd flash visible/jumbled in with everything else on every first
   paint. This inline `style.display` override wins the instant JS runs for
   anyone who has actually unhidden them (element.style always beats a
   stylesheet rule), so it only affects the pre-JS instant - see
   ideas-and-plans/PLANS.md's 2026-09-11 entry. */
.tile[data-block="timer"], .tile[data-block="line"] { display: none; }
.tile.dragging {
  z-index: 10;
  box-shadow: 0 0 0 1px var(--accent);
  /* Hints the browser to composite this tile on its own GPU layer instead
     of repainting it in-place on every pointermove - the actual drag code
     only ever touches transform (cheap), but without this hint some mobile
     browsers still repaint the tile's box-shadow/border each frame, which
     is what reads as "not smooth" on lower-end phones. */
  will-change: transform;
}
.tile.reject { animation: tile-reject 0.3s ease; }
@keyframes tile-reject {
  0%, 100% { transform: translateX(0); box-shadow: 0 0 0 1px var(--danger); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

.resize-handle {
  display: none;
  position: absolute;
  right: 0;
  bottom: 0;
  width: 20px;
  height: 20px;
  touch-action: none;
  cursor: nwse-resize;
}
.resize-handle::before {
  content: '';
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
}
.tile-grid.edit-mode .resize-handle { display: block; }

.tile-remove-btn {
  display: none;
  position: absolute;
  top: 4px;
  left: 4px;
  z-index: 5;
  width: 22px;
  height: 22px;
  flex: none;
  padding: 0;
  border-radius: 50%;
  background: var(--danger);
  color: var(--danger-ink);
  border: none;
  font-size: 0.75rem;
  line-height: 1;
}
.tile-grid.edit-mode .tile-remove-btn { display: block; }

/* Instruments page is deliberately non-scrolling: not every instrument has
   to fit at once (that's the point of the tray below) rather than letting
   the page grow and scroll past the visible screen. */
#page-instruments { overflow: hidden; }
#instr-grid { flex: 1; min-height: 0; overflow: hidden; }

#instrument-tray {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}
#instrument-tray[hidden] { display: none; }
.tray-hint { font-size: 0.7rem; color: var(--muted); text-align: center; }
.tray-row { display: flex; gap: 8px; align-items: center; }
#hidden-tiles-list { flex: 1; min-width: 0; display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; }
#edit-done-btn { flex: 0 0 auto; }
.tray-chip {
  flex: 0 0 auto;
  background: var(--tile);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1px;
  touch-action: none;
  cursor: grab;
}

/* Ghost tile that follows the pointer while dragging a chip up from the
   tray - a plain visual clone, not a real grid member until dropped. */
.drag-ghost {
  position: fixed;
  z-index: 50;
  pointer-events: none;
  opacity: 0.85;
  transform: translate(-50%, -50%);
}

/* Shown on an on-grid tile while it's being dragged above the grid's top
   edge, to indicate the drop-to-remove zone is armed. */
.tile.will-remove { box-shadow: 0 0 0 2px var(--danger); opacity: 0.6; }

.compass-dial {
  width: 100%;
  height: 100%;
  max-width: 160px;
  max-height: 160px;
  margin: 0 auto;
}
.compass-ring { fill: none; stroke: var(--border); stroke-width: 2; }
.compass-cardinal {
  fill: var(--muted);
  font-family: 'Inter', sans-serif;
  font-size: 9px;
  font-weight: 600;
  text-anchor: middle;
}
/* Ring rotates with heading (like a gyrocompass repeater card); the wind
   needle rotates separately with TWA, boat-relative and independent of
   heading; the boat icon never rotates - it's the fixed "bow up" reference
   both of the above are read against. All three share one center via the
   same transform-origin. */
#compass-ring-group, #wind-needle { transform-origin: 50px 50px; }
.wind-needle-line { stroke: var(--amber); stroke-width: 3; stroke-linecap: round; }
.wind-needle-tip { fill: var(--amber); }
.boat-icon { fill: var(--accent); }

/* TWA/AWA/TWS readout row under the dial. Real values (apparent wind is
   computed from true wind + SOG/COG, not faked) - VMG is deliberately not
   here, see README's "Notes on the sailing math" for why it's deferred. */
.wind-stats-row { display: flex; justify-content: space-between; gap: 6px; flex: none; width: 100%; margin-top: 4px; }
.wind-stat { text-align: center; }
.wind-stat-label { font-size: 0.58rem; font-weight: 600; letter-spacing: 0.5px; }
.wind-stat-label.twa { color: var(--amber); }
.wind-stat-label.awa { color: var(--accent); }
.wind-stat-label.tws { color: var(--danger); }
.wind-stat-value { font-family: 'Space Grotesk', sans-serif; font-weight: 700; font-size: 0.95rem; }

/* SOG/COG/HEEL/etc are real marine abbreviations (Speed/Course Over Ground),
   not a styling choice - they stay capitalized because that's the actual
   terminology, same as a real instrument's silkscreened label. */
.tile-label {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  margin-bottom: 6px;
}

.tile-value {
  /* --tile-value-px is set per-block by JS (applyLayout()) from the tile's
     actual rendered pixel height (row span x the live --cell-h, which
     itself stretches to fill the page - see updateMaxRows()), not just its
     w/h ratio relative to a fixed base size. Sizing off --tile-scale alone
     (a fixed 3rem times that ratio) left the big readouts looking small and
     lost in their box whenever few tiles were visible and rows stretched
     tall, since the box grew but the font didn't grow with it - this way
     the number always fills roughly the same proportion of its own tile
     regardless of how tall the grid's rows currently are. --tile-scale
     stays as the calc()'s fallback for any tile this hasn't run for yet
     (e.g. before first layout). */
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: calc(var(--tile-value-px, 48) * 1px);
  line-height: 1;
  color: var(--text);
}
.tile-value.small { font-size: calc(2rem * var(--tile-scale, 1)); }
#timer-value.tile-value { font-size: calc(2.6rem * var(--tile-scale, 1)); }
.tile-unit {
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 1rem;
  color: var(--muted);
  margin-left: 6px;
}

.tile-sub {
  margin-top: 6px;
  color: var(--muted);
  font-size: 0.85rem;
}

/* Status tile (wind + tack, previously a fixed .sub-readout strip above the
   grid) - full width, 1 row, so it needs tighter padding than a normal tile
   to leave room for its content at --cell-h. */
.tile[data-block="status"] {
  padding: 4px 14px;
  display: flex;
  align-items: center;
}
.status-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 12px;
  width: 100%;
  overflow: hidden;
}
/* The status tile's content sits flush against the top-left corner (unlike
   every other tile, which centers its content and leaves that corner empty)
   - without this, the remove button in edit mode sits directly on top of
   the wind/tack text instead of over empty space. */
.tile-grid.edit-mode .tile[data-block="status"] .status-row { padding-left: 30px; }
.status-pair {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}
.status-label {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  flex: none;
}
.status-value {
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: calc(1.05rem * var(--tile-scale, 1));
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.status-sep {
  width: 1px;
  height: 16px;
  background: var(--border);
  flex: none;
}
/* Pushes GPS status + tack-side chip to the right on a wide tile; the first
   thing to give up room as the tile shrinks (see .status-row's overflow:
   hidden) since wind/tack are the primary readouts. */
.status-spacer { flex: 1 1 auto; min-width: 0; }
.status-gps {
  color: var(--muted);
  font-size: 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 0 1 auto;
}
.status-chip {
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 600;
  white-space: nowrap;
  flex: none;
  background: var(--secondary-bg);
  color: var(--muted);
  border: 1px solid var(--border);
}
.status-chip.side-stbd { background: var(--accent); color: var(--btn-ink); border-color: transparent; }
.status-chip.side-port { background: var(--danger); color: var(--danger-ink); border-color: transparent; }
/* Mono/amber/red themes' accent and danger are both light/near-identical
   hues (same reasoning as .shift-banner above) - a dashed border keeps port
   distinguishable from starboard by shape, not just color. */
[data-theme="mono"] .status-chip.side-port,
[data-theme="amber"] .status-chip.side-port,
[data-theme="red"] .status-chip.side-port { border: 1px dashed var(--bg); }

.shift-banner {
  text-align: center;
  padding: 10px;
  border-radius: var(--radius-sm);
  font-weight: 700;
  letter-spacing: 0.5px;
}
.shift-banner.lift { background: #0d2620; color: var(--accent); border: 1px solid var(--accent); }
.shift-banner.header { background: #2a1014; color: var(--danger); border: 1px solid var(--danger); }
/* Non-color themes: the LIFT/HEADER text already carries the meaning (see
   JS), but a dashed border on HEADER gives a second, shape-based cue since
   lift/header would otherwise render as near-identical grey/amber boxes. */
[data-theme="mono"] .shift-banner.lift { background: #1a1a1a; }
[data-theme="mono"] .shift-banner.header { background: #1a1a1a; border-style: dashed; }
[data-theme="amber"] .shift-banner.lift { background: #241a04; }
[data-theme="amber"] .shift-banner.header { background: #241004; border-style: dashed; }
/* Red theme's accent/danger are both red-family hues - same escalation-safe
   dashed-border treatment as mono/amber. */
[data-theme="red"] .shift-banner.lift { background: #1a0505; }
[data-theme="red"] .shift-banner.header { background: #1a0505; border-style: dashed; }

.sub-readout {
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

label {
  display: block;
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 600;
}

.settings-section-title {
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.3px;
  margin: 6px 2px -2px;
}
.settings-section-title:first-child { margin-top: 0; }

/* Settings sub-tabs (2026-09-10) - a second-level tab row inside Settings
   only, replacing the old single scrolling list of .settings-section-title
   groups. Reuses the existing chip-row look/mechanism (wireChipGroup() in
   app.js) rather than a new control, and is deliberately unlike #tabbar
   (transparent row of chips, not a fixed bottom bar) so it never reads as a
   second main-nav. Sticky so it's still reachable once a tab's own content
   scrolls past it. */
#settings-tabbar {
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  padding: 6px 0 10px;
  margin: -6px 0 0;
  border-bottom: 1px solid var(--border);
}
#settings-tabbar button { flex: 1 1 28%; }
.settings-tab-page { display: none; flex-direction: column; gap: 12px; }
.settings-tab-page.active { display: flex; }

/* Settings-only tweaks to match the design reference's exact panel/input
   numbers (14px panel padding vs. the shared .panel's 16px default; a styled
   text input for the free-text start-sequence field). Scoped to #page-settings
   rather than changed globally, since .panel/input are shared with the Route
   and Races pages. */
#page-settings .panel { padding: 14px; }
/* Denser chip rows (4-5 short options per row, e.g. update-rate pickers) and
   the 3-column theme grid - matches the design reference's own smaller
   font-size/padding for these specific rows vs. the standard 3-button row. */
#page-settings .row.dense button { font-size: 0.78rem; padding: 12px 4px; }
#page-settings .chip-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 8px;
}
#page-settings .chip-grid button { font-size: 0.78rem; padding: 11px 4px; }
/* Was scoped to #page-settings only - every text/date input on the Races
   page (day name, boat name, race code) fell through to the unstyled
   browser default (white box, black text) as a result, clashing hard
   against the dark theme everywhere else. Applied site-wide instead; more
   specific rules elsewhere (e.g. .session-rename-input) still win on tied
   specificity since they're declared later in this file. */
input[type="text"] {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: 1.15rem;
  text-align: center;
  border-radius: var(--radius-sm);
  padding: 10px;
}

input[type="date"] {
  flex: 1;
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  border-radius: var(--radius-sm);
  padding: 10px;
  color-scheme: dark; /* keeps the native calendar picker/icon legible on a dark field - without it browsers render a dark-on-dark glyph */
}
[data-theme="daylight"] input[type="date"] { color-scheme: light; }

.row { display: flex; gap: 8px; }

/* A labeled field for compact side-by-side number inputs (race day scheduler:
   Races / Gap (min)) - the existing nudge-button pattern doesn't fit two
   fields sharing a row, so this pairs a small caption with the input. */
.field { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.field-label { color: var(--muted); font-size: 0.72rem; }

select {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  border-radius: var(--radius-sm);
  padding: 10px;
}

input[type="number"] {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: 1.3rem;
  text-align: center;
  border-radius: var(--radius-sm);
  padding: 8px;
}

/* 2026-09-10: a visible focus state for every text-ish input, previously
   missing entirely (focus fell back to the browser's own default outline,
   inconsistent across browsers and easy to miss against a dark field).
   Border-color only, not a box-shadow ring - a ring's glow would read
   exactly like the "flashy" look this pass is toning down, and every theme
   already has a border to shift color on, no new token needed. Doesn't
   touch border-width/padding, so no layout impact on any input's box. */
input[type="text"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
}

/* Native checkboxes re-skinned as toggle switches - purely visual, no HTML/JS
   changes needed since `.checked` still works exactly the same underneath. */
input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 44px;
  height: 26px;
  flex: none;
  border-radius: 999px;
  background: var(--border);
  position: relative;
  cursor: pointer;
  margin: 0;
  transition: background 0.15s ease;
}
input[type="checkbox"]::before {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--text);
  transition: transform 0.15s ease;
}
input[type="checkbox"]:checked { background: var(--accent); }
input[type="checkbox"]:checked::before {
  transform: translateX(18px);
  background: var(--btn-ink);
}

button {
  flex: 1;
  background: var(--accent);
  color: var(--btn-ink);
  border: none;
  border-radius: var(--radius-sm);
  padding: 12px 6px;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  /* 2026-09-10: smooths the existing :active dimming (below) instead of it
     snapping instantly - same shape, same padding, same trigger/behavior,
     just reads a touch more "considered" on press. Not a new interaction. */
  transition: filter 0.12s ease;
}
button.secondary { background: var(--secondary-bg); color: var(--text); border: 1px solid var(--border); }
button.nudge { flex: 0 0 52px; }
button.small-btn { padding: 6px 10px; font-size: 0.75rem; flex: 0 0 auto; }
button:active { filter: brightness(0.85); }
/* A disabled action stays legible (not invisible) but reads as unavailable -
   e.g. Races page "Start (host is active)" while a non-host can't yet start
   a race the host might still start themselves (API-AUTH.md's start-time
   access rules). */
button:disabled { opacity: 0.45; filter: none; cursor: not-allowed; }

#timer-value.warn { color: var(--amber); }
#timer-value.go { color: var(--danger); }
/* Mono gives warn/go the same --amber/--danger (both white - nothing else
   to reach for without color), so .go gets reverse video on top of that:
   still louder than .warn, preserving the warn < go escalation. warn/go
   never apply together, so sharing white between the two tokens is safe. */
[data-theme="mono"] #timer-value.go { color: var(--bg); background: var(--danger); }
/* Amber's own --amber (#ffe08a) is barely different from normal --text
   (#ffd28a), and --danger (#ff6a00) reads darker/duller than both - same
   escalation-inversion problem as mono, fixed the same way. */
[data-theme="amber"] #timer-value.warn { color: #ffffff; }
[data-theme="amber"] #timer-value.go { color: var(--bg); background: var(--danger); }
/* Red theme's --amber/--danger are both red-family - same treatment. */
[data-theme="red"] #timer-value.go { color: var(--bg); background: var(--danger); }

/* #start-timer-value mirrors #timer-value's warn/go escalation exactly (same
   states, same per-theme overrides) - it's a second view of the identical
   countdown, not a separate readout with its own rules to drift out of sync. */
#start-timer-value.warn { color: var(--amber); }
#start-timer-value.go { color: var(--danger); }
[data-theme="mono"] #start-timer-value.go { color: var(--bg); background: var(--danger); }
[data-theme="amber"] #start-timer-value.warn { color: #ffffff; }
[data-theme="amber"] #start-timer-value.go { color: var(--bg); background: var(--danger); }
[data-theme="red"] #start-timer-value.go { color: var(--bg); background: var(--danger); }

.hold-btn { position: relative; overflow: hidden; touch-action: none; }
.hold-btn .fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0%;
  background: var(--danger);
  opacity: 0.4;
}
.hold-btn .label { position: relative; }

.reset-btn { padding: 18px 6px; font-size: 1.05rem; border-radius: 999px; }

#record-toggle.recording { background: var(--danger); color: var(--danger-ink); }

/* ---------- Start page ----------
   Dedicated full-screen pre-start view: a timer panel and a line panel, each
   ~half the page height, non-scrolling - same visual language as the
   Instruments tiles (.panel/.tile-label/.tile-sub/button) just blown up to
   page scale. Recreates design-reference/project/Race Computer v3.dc.html's
   START block visually; the countdown/line math itself stays entirely in
   app.js's existing tickTimer()/updateLineReadout() - this file and the
   #start-* elements are output only. */
#page-start { gap: 10px; }

/* Route page: matches the design reference's exact rendered spacing (10px
   top / 14px sides+bottom, 10px gap) rather than the generic .page 14px/12px
   - see the map-viewport/panel rules further down for the rest of the
   Route-specific chrome (map badges, recenter glyph, recording stat row). */
#page-route { padding: 10px 14px 14px; gap: 10px; }
.start-panel { flex: 1 1 50%; min-height: 0; overflow: hidden; }
.start-panel-head { display: flex; align-items: baseline; gap: 8px; flex: none; }
.start-timer-display {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
#start-timer-value {
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: clamp(4rem, 22vw, 8rem);
  line-height: 0.88;
  letter-spacing: -3px;
  text-align: center;
  color: var(--text);
}

/* RRS 5-4-1-0 start-sequence marks - lit (via tickTimer()) once the
   countdown has reached that point, using the same START_MARKS_S crossing
   points that already drive the sound signals. Purely a passed/not-passed
   indicator, no independent state of its own. */
.start-marks { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 14px; }
.start-mark { display: flex; align-items: center; gap: 6px; }
.start-mark-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
  flex: none;
}
.start-mark-label {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
}
.start-mark.lit .start-mark-dot { background: var(--accent); }
.start-mark.lit .start-mark-label { color: var(--text); }

.start-line-row { display: flex; align-items: flex-end; gap: 12px; flex: none; }
.start-line-dist { flex: 1; text-align: right; }
.start-burn-value, .start-line-dist-value {
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
}
.start-burn-value { font-size: 2.6rem; }
.start-line-dist-value { font-size: 1.6rem; }

.bias-meter { flex: none; }
.bias-meter-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--muted);
  margin-bottom: 5px;
}
/* Highlights whichever end of the line the current bias favours - mirrors
   the sign of the same `bias` value updateLineReadout() already computes
   for the text readout, just also reflected here. */
.bias-meter-labels .favored { color: var(--accent); }
.bias-meter-track {
  position: relative;
  height: 12px;
  border-radius: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
}
.bias-meter-center {
  position: absolute;
  left: 50%;
  top: -3px;
  bottom: -3px;
  width: 1px;
  background: var(--border);
}
.bias-meter-dot {
  position: absolute;
  top: -2px;
  left: 50%;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  border-radius: 50%;
  background: var(--amber);
  border: 2px solid var(--tile);
  transition: left 0.2s ease;
}

/* Heading-up map rotation: #map-viewport is the fixed-size visible window;
   #map itself is oversized (set in JS, ~1.45x the viewport's diagonal) and
   centered inside it, so rotating it never reveals empty corners. The
   rotation itself is applied to #map-rotate-wrapper (inserted around
   Leaflet's map pane in JS), not to #map or the pane directly - see the
   comment above mapRotateWrapperEl in app.js for why. */
/* touch-action:none stops the browser from hijacking a drag here for its
   own native page-scroll/pinch-zoom gestures - without it, mobile browsers
   can grab the touch sequence away from our pointer handlers mid-drag,
   which is why panning felt broken/unresponsive before this. */
#map-viewport {
  flex: 1;
  min-height: 200px;
  border-radius: var(--radius);
  overflow: hidden;
  position: relative;
  touch-action: none;
  background: #101922; /* fixed map chrome, not a theme token - matches the design reference's own literal (non-var) value */
  border: 1px solid var(--border);
}
/* Subtle grid-line texture behind the Leaflet tiles, matching the design
   reference's placeholder map background. #map is painted after this in DOM
   order (same stacking context, z-index:auto on both) so it covers the
   texture once tiles load - this only actually shows through briefly while
   tiles are loading, or in gaps if a tile fetch fails offline. */
#map-viewport::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(to right, rgba(255, 255, 255, .05) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(rgba(255, 255, 255, .05) 0 1px, transparent 1px 40px);
}
#map {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 145%;  /* JS overrides this with a precise diagonal-based size once the page is visible */
  height: 145%;
  filter: grayscale(0.3) brightness(0.9);
}
/* Rotates as a unit around its own center (== #map's center, since it fills
   #map exactly via inset:0) - see the comment above mapRotateWrapperEl in
   app.js for why this has to be a separate element from Leaflet's own pane. */
#map-rotate-wrapper { position: absolute; inset: 0; }
/* Monochrome should mean monochrome - strip the OSM tiles' blue water /
   green land too, not just the app's own UI colors. */
[data-theme="mono"] #map { filter: grayscale(1) brightness(0.75) contrast(1.1); }
/* Daylight is the one theme built for bright ambient light, not a dim
   cockpit - the default darkening filter (tuned for dark themes) would just
   fight it, so skip it here. */
[data-theme="daylight"] #map { filter: none; }

.line-end-icon { background: transparent; border: none; touch-action: none; }
.line-end-circle {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid var(--amber);
  background: rgba(0, 0, 0, 0.75);
  color: var(--amber);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Space Grotesk', sans-serif;
  font-size: 0.7rem;
  font-weight: 700;
}

#sessions-list { display: flex; flex-direction: column; gap: 8px; }
.session-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  gap: 8px;
}
.session-info { font-size: 0.85rem; color: var(--text); line-height: 1.3; min-width: 0; }
.session-info .muted { color: var(--muted); }
.session-title-row { display: flex; align-items: center; gap: 6px; }
.session-title-row span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
/* The name itself (always the first span - a rename button or race badges
   may follow it) is the one part of the row allowed to shrink/truncate, so
   a rename button or a "HOST"/"Next up"/"Averaged" badge next to it always
   stays fully visible rather than getting squeezed by a long race/session
   name. */
.session-title-row span:first-child { flex: 1; min-width: 0; }
.session-rename-btn { flex: 0 0 auto; padding: 2px 6px; font-size: 0.8rem; line-height: 1; }
.session-rename-input {
  flex: 1;
  min-width: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 0.8rem;
  border-radius: var(--radius-sm);
  padding: 2px 6px;
}
/* flex-wrap - a race row's 3 actions (Start/Propose, View Routes, host-only
   Cancel) don't all fit one line at typical phone widths; without wrapping
   the row (and the whole page under it) was forced into horizontal scroll
   instead of the button group just dropping to a second line. */
.session-actions { display: flex; flex-wrap: wrap; gap: 6px; }

#tabbar {
  display: flex;
  border-top: 1px solid var(--border);
  background: var(--panel);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
#tabbar .tab {
  flex: 1;
  background: transparent;
  color: var(--muted);
  border-radius: 0;
  padding: 12px 4px;
  font-size: 0.8rem;
  font-weight: 600;
}
#tabbar .tab.active { color: var(--accent); }
/* Icon mode: bigger glyphs read fine at a size where the text labels would
   wrap/truncate - this is purely a font-size swap, same buttons/handlers. */
#tabbar.icon-mode .tab { font-size: 1.35rem; }

/* ---------- screen lock ---------- */
/* Lock now lives as the tab bar's own leftmost item, matching the design
   reference - no more floating top-right circle. */
#lock-toggle.lock-tab {
  flex: 0 0 auto;
  border-right: 1px solid var(--border);
  border-radius: 0;
}

#map-recenter {
  position: absolute;
  bottom: 10px;
  right: 10px;
  z-index: 15;
  flex: none;
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 50%;
  background: var(--secondary-bg);
  color: var(--accent);
  border: 1px solid var(--border);
  font-size: 1.3rem;
  line-height: 1;
}

/* ---------- Route map badges ----------
   Top-left: map rotation mode ("Heading up N°" / "North up"), driven by the
   real mapNorthLock setting + smoothed heading (state.heading) - see
   updateMapHeadingBadge()/applyMapRotation() in app.js. Top-right: line
   length, only shown once both ends are pinged - see updateLineReadout()/
   clearLine(). Both sit above #map by DOM order (see #map-viewport::before
   above for why that's enough without an explicit z-index; a small one is
   still set here for safety, matching #map-recenter's own pattern). */
.map-badges-left {
  position: absolute;
  left: 10px;
  top: 10px;
  z-index: 5;
  display: flex;
  gap: 6px;
}
.map-badge {
  font-size: 0.625rem;
  font-weight: 600;
  color: var(--muted);
  background: rgba(0, 0, 0, 0.5);
  padding: 4px 7px;
  border-radius: 6px;
}
.map-line-badge {
  position: absolute;
  right: 10px;
  top: 10px;
  z-index: 5;
  color: var(--amber);
}
.map-line-badge[hidden] { display: none; }

/* ---------- Route page action panel ----------
   14px/13px/.88rem match the design reference's exact rendered values,
   slightly tighter than the generic .panel/button defaults used elsewhere
   (Settings included) - scoped to this panel only so nothing outside Route
   is affected. */
.route-panel { padding: 14px; }
.route-panel button { padding: 13px 6px; font-size: 0.88rem; }
.record-stats {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}
.record-stat-label { color: var(--muted); font-size: 0.68rem; }
.record-stat-value { font-family: 'Space Grotesk', sans-serif; font-weight: 700; font-size: 1.1rem; }

#lock-overlay {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding-bottom: 40px;
  background: rgba(0, 0, 0, 0.08);
}
#lock-overlay[hidden] { display: none; }

#lock-banner {
  position: absolute;
  top: 8px;
  left: 0;
  right: 0;
  text-align: center;
  color: var(--muted);
  font-size: 0.75rem;
}

#unlock-btn {
  flex: none;
  width: 160px;
  height: 56px;
  border-radius: 28px;
  background: var(--secondary-bg);
  color: var(--text);
  border: 1px solid var(--border);
  font-weight: 700;
  position: relative;
  overflow: hidden;
  touch-action: none;
}
#unlock-btn .fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0%;
  background: var(--accent);
  opacity: 0.35;
}
#unlock-btn .label { position: relative; }

/* ---------- Races page: race-day groups ----------
   Each race day (or lone race, rendered as its own one-row "day" - see
   raceDayKey() in app.js) is one of these groups inside #races-list, reusing
   the existing .session-row/.session-info/.session-actions look for each
   individual race row within it - no new row-level visual language, just a
   day-level header wrapped around the existing rows. */
.race-day-group { display: flex; flex-direction: column; gap: 8px; }
.race-day-group + .race-day-group {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.race-day-header { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.race-day-title {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  font-size: 0.9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.race-day-header button { flex: 0 0 auto; }

/* Status badges next to a race's name - same visual family as the route
   map's .map-badge (small, pill-ish, low-emphasis) rather than a new
   pattern, just inline instead of positioned over the map. */
.race-badge {
  flex: 0 0 auto;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.3px;
  padding: 2px 7px;
  border-radius: 999px;
  line-height: 1.5;
}
.race-badge-host { background: var(--accent); color: var(--btn-ink); }
.race-badge-next { background: var(--secondary-bg); color: var(--accent); border: 1px solid var(--border); }
.race-badge-cancelled { background: transparent; color: var(--muted); border: 1px solid var(--border); }

.race-row-cancelled .session-info { color: var(--muted); }
.race-row-cancelled .session-title-row span:first-child { text-decoration: line-through; color: var(--muted); }

/* Fallback-setter attribution line ("Set by Crew Boat - the host hadn't set
   one.") - a full line rather than a badge, since a boat's display name can
   run longer than a pill badge comfortably fits (replaces the old averaging
   model's .race-proposals-note, API-AUTH.md's 2026-09-09 "race day is a
   day" rework - there's no more proposal count to report). */
.race-setby-note { font-size: 0.72rem; color: var(--muted); line-height: 1.3; }
