/* =============================================================================
   Divot Golf — design system
   -----------------------------------------------------------------------------
   Layers, in order: tokens → base → primitives → chrome → components → screens.
   Nothing below the token layer may use a raw colour: every value comes from a
   custom property, which is what lets one theme swap flip the whole app.
   ============================================================================= */

/* --- Tokens ---------------------------------------------------------------- */

:root {
  /* Light is the default because that's what a phone on a fairway in full sun
     needs. Dark is the signature look and is applied below. */
  --bg: #f6f8f6;
  --bg-elev: #ffffff;
  --surface: #ffffff;
  --surface-2: #edf1ee;
  --surface-3: #e3e9e5;
  --line: rgba(10, 20, 14, 0.13);
  --line-strong: rgba(10, 20, 14, 0.24);
  --text: #0e1512;
  --muted: #5c6b64;
  --faint: #616e67;

  --accent: #0a6e3c;
  --accent-hover: #085c32;
  --accent-ink: #ffffff;
  --accent-soft: rgba(10, 110, 60, 0.1);
  --accent-line: rgba(10, 110, 60, 0.3);

  --gold: #8f6206;
  --gold-soft: rgba(143, 98, 6, 0.12);
  --danger: #b3261e;
  --danger-ink: #ffffff;
  --danger-soft: rgba(179, 38, 30, 0.1);
  --warn: #8a5a08;
  --warn-soft: rgba(138, 90, 8, 0.12);

  /* Score relative to par. Golf's own convention — under par reads hot, over par
     reads cool — inverted here so the app's accent means "good". */
  --tone-eagle: #8f6206;
  --tone-birdie: #0a6e3c;
  --tone-bogey: #8a5a08;
  --tone-double: #b3261e;

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 22px;
  --r-pill: 999px;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;

  --shadow-1: 0 1px 2px rgba(10, 20, 14, 0.06), 0 1px 3px rgba(10, 20, 14, 0.04);
  --shadow-2: 0 8px 24px rgba(10, 20, 14, 0.1);

  --dur: 140ms;
  /* Standard curve: at rest at both ends, 50% covered at 35% elapsed. The previous
     one packed the change into the front of the duration, which made short animations
     look like they were skipping frames rather than moving. */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  /* Pure decelerate, for things that arrive from off-screen and settle. */
  --ease-out: cubic-bezier(0, 0, 0.2, 1);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  --bar-h: 56px;

  color-scheme: light;
}

/* Dark tokens live in two selectors on purpose: the media query is what an OS
   preference (or a no-JS load) picks up, and the attribute is what the in-app
   toggle sets. The `:not([data-theme="light"])` guard means choosing light on a
   dark phone doesn't need the whole light palette repeated back. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0a0f0c;
    --bg-elev: #131a16;
    --surface: #131a16;
    --surface-2: #1c2621;
    --surface-3: #26332c;
    --line: rgba(255, 255, 255, 0.09);
    --line-strong: rgba(255, 255, 255, 0.2);
    --text: #f1f5f2;
    --muted: #9ba8a1;
    --faint: #8a9a93;

    --accent: #3fe08b;
    --accent-hover: #57e79a;
    --accent-ink: #06120c;
    --accent-soft: rgba(63, 224, 139, 0.14);
    --accent-line: rgba(63, 224, 139, 0.34);

    --gold: #f5c451;
    --gold-soft: rgba(245, 196, 81, 0.15);
    --danger: #ff6b5e;
    --danger-ink: #2a0703;
    --danger-soft: rgba(255, 107, 94, 0.14);
    --warn: #e7a93b;
    --warn-soft: rgba(231, 169, 59, 0.14);

    --tone-eagle: #f5c451;
    --tone-birdie: #3fe08b;
    --tone-bogey: #e7a93b;
    --tone-double: #ff6b5e;

    --shadow-1: none;
    --shadow-2: 0 12px 32px rgba(0, 0, 0, 0.5);

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: #0a0f0c;
  --bg-elev: #131a16;
  --surface: #131a16;
  --surface-2: #1c2621;
  --surface-3: #26332c;
  --line: rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.2);
  --text: #f1f5f2;
  --muted: #9ba8a1;
  --faint: #8a9a93;

  --accent: #3fe08b;
  --accent-hover: #57e79a;
  --accent-ink: #06120c;
  --accent-soft: rgba(63, 224, 139, 0.14);
  --accent-line: rgba(63, 224, 139, 0.34);

  --gold: #f5c451;
  --gold-soft: rgba(245, 196, 81, 0.15);
  --danger: #ff6b5e;
  --danger-ink: #2a0703;
  --danger-soft: rgba(255, 107, 94, 0.14);
  --warn: #e7a93b;
  --warn-soft: rgba(231, 169, 59, 0.14);

  --tone-eagle: #f5c451;
  --tone-birdie: #3fe08b;
  --tone-bogey: #e7a93b;
  --tone-double: #ff6b5e;

  --shadow-1: none;
  --shadow-2: 0 12px 32px rgba(0, 0, 0, 0.5);

  color-scheme: dark;
}

/* --- Base ------------------------------------------------------------------ */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  font-family: var(--font);
  font-size: 16px; /* prevents iOS zoom-on-focus, spec §7 */
  line-height: 1.45;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

#app {
  max-width: 620px;
  margin: 0 auto;
  min-height: 100vh;
  background: var(--bg);
}

main {
  /* Bottom padding clears the fixed tab bar plus the iOS home indicator. */
  padding: var(--sp-4) var(--sp-4) calc(104px + env(safe-area-inset-bottom));
}

h1,
h2,
h3 {
  letter-spacing: -0.02em;
  line-height: 1.15;
  text-wrap: balance;
}

h1 {
  font-size: 1.65rem;
  font-weight: 800;
  margin: 0 0 var(--sp-3);
}

h2 {
  font-size: 1.15rem;
  font-weight: 700;
  margin: var(--sp-5) 0 var(--sp-2);
}

h3 {
  font-size: 1rem;
  font-weight: 700;
  margin: var(--sp-4) 0 var(--sp-2);
}

p {
  margin: 0 0 var(--sp-3);
}

a {
  color: var(--accent);
  text-underline-offset: 2px;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* Each screen arrives rather than appearing. Restarted from AppShell on every route
   change by removing and re-adding the class, which is the only way to replay a CSS
   animation on an element that never unmounts. */
main.screen-enter {
  animation: screen-enter 300ms var(--ease-out);
}

@keyframes screen-enter {
  from {
    opacity: 0;
    translate: 0 6px;
  }
}

/* A placeholder shaped like the content that's coming, rather than the word
   "Loading" followed by a jump. */
.skeleton,
.skeleton-row {
  position: relative;
  overflow: hidden;
  background: var(--surface-2);
  border-color: transparent;
}

.skeleton-row {
  height: 55px;
}

.skeleton::after,
.skeleton-row::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--surface-3), transparent);
  translate: -100% 0;
  animation: shimmer 1.8s ease-in-out infinite;
}

@keyframes shimmer {
  to {
    translate: 100% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    /* Without this, a looping animation becomes an endless run of zero-length
       iterations rather than stopping. */
    animation-iteration-count: 1 !important;
  }
}

/* --- Primitives: buttons --------------------------------------------------- */

button,
input,
select,
textarea {
  font-size: 16px;
  font-family: inherit;
}

button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 48px;
  padding: 12px 18px;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

button:active:not(:disabled) {
  transform: scale(0.98);
}

@media (hover: hover) {
  /* Every override of this rule must repeat `:not(:disabled)`. The pseudo-class
     inside :not() counts toward specificity, so this selector is (0,2,1) and a
     plain `.thing:hover` at (0,2,0) silently loses to it — which is how the
     wordmark ended up filled accent-green when tapped.

     The gate matters too: on a touch screen :hover sticks after a tap until you
     tap something else, and the app bar survives navigation — so a tapped logo
     stayed lit. No pointer, no hover styles, nothing left behind. */
  button:hover:not(:disabled) {
    background: var(--accent-hover);
  }
}

button.secondary {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text);
}

@media (hover: hover) {
  button.secondary:hover:not(:disabled) {
    background: var(--surface-3);
  }
}

button.ghost {
  background: transparent;
  border-color: var(--line);
  color: var(--text);
}

@media (hover: hover) {
  button.ghost:hover:not(:disabled) {
    background: var(--surface-2);
  }
}

button.danger {
  background: var(--danger);
  border-color: transparent;
  color: var(--danger-ink);
}

@media (hover: hover) {
  button.danger:hover:not(:disabled) {
    background: var(--danger);
    filter: brightness(1.08);
  }
}

button.secondary.danger-quiet {
  background: var(--danger-soft);
  border-color: transparent;
  color: var(--danger);
}

button:disabled {
  opacity: 0.42;
  cursor: default;
}

button.linklike {
  background: none;
  border: none;
  color: var(--accent);
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  padding: 0;
  min-height: 0;
  cursor: pointer;
  width: auto;
}

@media (hover: hover) {
  button.linklike:hover:not(:disabled) {
    background: none;
  }
}

/* Square icon-only control, used across the app bar and compact rows. */
button.icon-btn {
  min-height: 40px;
  width: 40px;
  padding: 0;
  flex: 0 0 auto;
  background: transparent;
  border-color: var(--line);
  color: var(--text);
  border-radius: var(--r-md);
}

@media (hover: hover) {
  button.icon-btn:hover:not(:disabled) {
    background: var(--surface-2);
  }
}

button.icon-btn.is-current {
  color: var(--faint);
  border-color: transparent;
  cursor: default;
}

@media (hover: hover) {
  button.icon-btn.is-current:hover {
    background: transparent;
  }
}

.icon {
  display: block;
  flex: 0 0 auto;
}

/* --- Primitives: inputs ---------------------------------------------------- */

input,
select,
textarea {
  min-height: 48px;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  width: 100%;
  background: var(--surface-2);
  color: var(--text);
  transition: border-color var(--dur) var(--ease);
}

@media (hover: hover) {
  input:hover,
  select:hover,
  textarea:hover {
    border-color: var(--line-strong);
  }
}

input::placeholder,
textarea::placeholder {
  color: var(--faint);
}

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 17px) calc(50% + 1px),
    calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px;
  background-repeat: no-repeat;
  padding-right: 34px;
}

/* Checkboxes opt out of the full-width text-input treatment above. */
input.checkbox,
input[type="checkbox"] {
  width: auto;
  min-height: auto;
  accent-color: var(--accent);
  flex: 0 0 auto;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

/* A visible label that stays put once the input is filled, rather than a
   placeholder that disappears the moment you type. */
.field-label,
.eyebrow {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.field-help {
  color: var(--muted);
  font-size: 0.875rem;
  margin: var(--sp-2) 0;
}

/* Help text that belongs to the control directly above it. */
.field-help-tight {
  margin: 2px 0 0;
}

/* --- Primitives: layout & surfaces ----------------------------------------- */

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.row {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
}

.row-between {
  justify-content: space-between;
}

.row-bottom {
  align-items: flex-end;
}

.grow {
  flex: 1;
  min-width: 0;
}

/* Two fields on one row where one carries the longer value. */
.grow-wide {
  flex: 1.4;
  min-width: 0;
}

.field-narrow {
  flex: 0 1 116px;
  min-width: 0;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  margin: var(--sp-3) 0;
  box-shadow: var(--shadow-1);
}

.card > :last-child {
  margin-bottom: 0;
}

.card.card-danger {
  border-color: var(--danger);
  background: var(--danger-soft);
}

.card.card-warn {
  border-color: var(--warn);
  background: var(--warn-soft);
}

.card.card-accent {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* A titled region of a long screen (Setup, Admin, Course Library). */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  margin: var(--sp-4) 0;
  box-shadow: var(--shadow-1);
}

.panel > h2,
.panel > h3 {
  margin-top: 0;
}

.panel > :last-child {
  margin-bottom: 0;
}

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.panel-head h2,
.panel-head h3 {
  margin: 0;
}

.divider {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: var(--sp-4) 0;
}

.muted {
  color: var(--muted);
  font-size: 0.9rem;
}

.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.num {
  font-variant-numeric: tabular-nums;
}

/* Spacing utilities — these replace the inline style="margin-top:…" attributes
   that were scattered through the screens. */
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-3 { margin-bottom: var(--sp-3); }
.m-0 { margin: 0; }
.text-danger { color: var(--danger); }
.text-warn { color: var(--warn); }
.text-accent { color: var(--accent); }
.center { text-align: center; }

/* --- Primitives: chips & badges -------------------------------------------- */

.badge,
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  background: var(--surface-2);
  color: var(--muted);
  white-space: nowrap;
}

.badge-live,
.chip-accent {
  background: var(--accent-soft);
  color: var(--accent);
}

.chip-gold {
  background: var(--gold-soft);
  color: var(--gold);
}

.chip-warn {
  background: var(--warn-soft);
  color: var(--warn);
}

.chip-danger {
  background: var(--danger-soft);
  color: var(--danger);
}

.chip-code {
  font-family: var(--font-mono);
  font-weight: 700;
  letter-spacing: 0.12em;
  background: var(--accent-soft);
  color: var(--accent);
  padding-right: 7px;
}

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* Only the accent chip's dot breathes — that's the one that means "this round is
   being played right now". A pulsing dot on a neutral chip would say nothing. */
.chip-accent .dot,
.conn-banner .dot {
  animation: pulse-dot 2s var(--ease) infinite;
}

@keyframes pulse-dot {
  50% {
    opacity: 0.35;
    scale: 0.8;
  }
}

/* --- Chrome: app bar ------------------------------------------------------- */

.app-bar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: var(--bar-h);
  padding: var(--sp-2) var(--sp-4);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  border-bottom: 1px solid var(--line);
}

@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .app-bar,
  .tab-bar {
    background: var(--bg);
  }
}

.app-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text);
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  white-space: nowrap;
  background: none;
  border: none;
  padding: 0;
  min-height: 0;
  cursor: pointer;
}

@media (hover: hover) {
  button.app-brand:hover:not(:disabled) {
    background: none;
  }
}

.brand-mark {
  display: block;
  flex: 0 0 auto;
}

/* The mark is drawn once (components/Brand.ts) and coloured by the theme: the ball
   takes the surrounding text colour, the flight and turf take the accent. Setting
   `color` rather than fill/stroke lets each part keep its own paint attributes —
   they all reference currentColor. */
.brand-mark .mark-flight {
  color: var(--accent);
}

.brand-mark .mark-turf {
  color: var(--accent);
  opacity: 0.62;
}

/* --- The mark in motion ---------------------------------------------------- */

/* The mark is a freeze-frame of a strike, so the animation plays the strike: the ball
   sits in the gap between the two ground strokes — that gap is the divot — gets hit,
   and climbs away while the turf it tore out tumbles clear. The flight arc draws in
   behind the ball as its trail.

   Nothing fades in. A ball that materialises at the end of the arc reads as delivered
   rather than struck, which was the first version's mistake.

   The flight path carries `pathLength="1"`, which normalises its length — so the dash
   geometry below is 1, not a measured number that would silently strand itself if the
   curve were ever retuned.

   Two copies run this: the boot splash (index.html) via the keyframes here, and the
   app-bar wordmark via the Web Animations API (components/Brand.ts) — WAAPI there
   because re-adding a class won't replay a CSS animation on a second tap without
   forcing a reflow. The splash loops for slow loads; the wordmark plays once as the
   app opens, which is the copy anyone actually sees. Same shape, different tempo;
   change one, change both.

   Reduced motion needs nothing special: the global block near the top of this file
   collapses these to one 0.01ms iteration, and every 100% keyframe is the mark at
   rest, so it lands there immediately. */

.splash {
  display: grid;
  place-items: center;
  min-height: 70vh;
  opacity: 0.5;
}

/* Both copies scale and rotate about their own centres rather than the SVG origin.
   For the wedge that lands on (15, 40) — the same pivot its static `rotate(-28 15 40)`
   already uses, so the animation resolves exactly onto the mark at rest. */
.splash-wedge,
.splash-ball,
.brand-mark .mark-wedge,
.brand-mark .mark-ball {
  transform-box: fill-box;
  transform-origin: center;
}

/* `1 2`, not `1`. With a period of exactly 1 the *next* dash begins precisely on the
   path's end point, and `stroke-linecap: round` renders that zero-length dash as a
   visible dot — a stray blob hanging in the air at the top of the arc for the whole
   pre-impact hold. Padding the gap pushes that dash off the end of the path. */
.splash-flight {
  stroke-dasharray: 1 2;
}

/* One cycle, then a beat of stillness before it repeats. A loop with no rest reads as
   a spinner, which would promise progress this can't actually measure. */
.splash-wedge,
.splash-flight,
.splash-ball {
  animation-duration: 2.6s;
  /* linear on purpose — the easing lives in where the keyframes sit, not here. */
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}

.splash-wedge {
  animation-name: mark-wedge;
}

.splash-flight {
  animation-name: mark-flight;
}

.splash-ball {
  animation-name: mark-ball;
}

/* The ball is struck, and travels. It is on screen the whole time — nothing fades in.
   At rest it starts in the gap between the two ground strokes, which is the divot
   scar itself, and it climbs to where the static mark holds it.

   The offsets below are the flight path's own cubic, sampled — P0 (25,50), P1 (30,36),
   P2 (40,26), P3 (52,22) — expressed relative to the ball's resting (55,18). So the
   ball rides the exact curve the trail draws rather than a straight line near it.

   The keyframe *times* carry the easing (which is why these run `linear`): they follow
   1-sqrt(1-t), so the ball leaves the face fast and decelerates as it climbs. Even
   spacing read as a ball being carried rather than hit. */
@keyframes mark-ball {
  0%,
  4% {
    transform: translate(-30px, 32px);
  }
  10.2% {
    transform: translate(-25.4px, 22.3px);
  }
  17.5% {
    transform: translate(-19.1px, 14.3px);
  }
  27% {
    transform: translate(-11.6px, 8.1px);
  }
  50%,
  100% {
    transform: none;
  }
}

/* The trail, drawing in behind the ball. Same sample times as `mark-ball`, and each
   dash offset is 1 minus that keyframe's path position, so the tip of the arc stays
   pinned to the ball instead of racing it or lagging behind. */
@keyframes mark-flight {
  0%,
  4% {
    stroke-dashoffset: 1;
  }
  10.2% {
    stroke-dashoffset: 0.75;
  }
  17.5% {
    stroke-dashoffset: 0.5;
  }
  27% {
    stroke-dashoffset: 0.25;
  }
  50%,
  100% {
    stroke-dashoffset: 0;
  }
}

/* The turf, torn out at impact and thrown clear. It doesn't exist before contact — so
   it appears on a hard cut at the moment of the strike, scaled down at the ball's
   position, rather than fading up out of nothing. It tumbles past its resting angle
   and settles back, the way a clod lands and rocks flat. */
@keyframes mark-wedge {
  0%,
  4% {
    transform: translate(10px, 10px) rotate(40deg) scale(0.25);
    opacity: 0;
  }
  4.01% {
    transform: translate(10px, 10px) rotate(40deg) scale(0.25);
    opacity: 1;
  }
  12% {
    transform: translate(2px, -1px) rotate(-12deg) scale(1.05);
    opacity: 1;
  }
  26%,
  100% {
    transform: none;
    opacity: 1;
  }
}

.app-bar-right {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.app-identity {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.app-me {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--muted);
  /* Tight enough that brand + name + code + both controls still fit a 390px phone;
     below that it's dropped entirely (see the narrow-phone block at the end) rather
     than clipped to an ellipsis that says nothing. */
  max-width: 10ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-code {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  background: var(--accent-soft);
  color: var(--accent);
  padding: 4px 9px;
  border-radius: var(--r-pill);
}

/* --- Chrome: tab bar ------------------------------------------------------- */

.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 30;
  display: flex;
  gap: var(--sp-1);
  max-width: 620px;
  margin: 0 auto;
  padding: 6px var(--sp-3) calc(6px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  border-top: 1px solid var(--line);
}

.tab-bar .tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  background: none;
  border: none;
  border-radius: var(--r-md);
  color: var(--muted);
  font-weight: 700;
  min-height: 54px;
  padding: 6px 4px;
}

@media (hover: hover) {
  .tab-bar .tab:hover:not(:disabled) {
    background: var(--surface-2);
  }
}

.tab-bar .tab.active {
  color: var(--accent);
  background: var(--accent-soft);
}

.tab-label {
  font-size: 0.7rem;
  letter-spacing: 0.01em;
}

/* --- Components: connection banner ----------------------------------------- */

.conn-banner {
  position: sticky;
  top: calc(var(--bar-h) + 4px);
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 8px 12px;
  border-radius: var(--r-pill);
  margin-bottom: var(--sp-3);
  font-size: 0.85rem;
  font-weight: 700;
  border: 1px solid transparent;
  animation: drop-in 320ms var(--ease-out);
}

@keyframes drop-in {
  from {
    opacity: 0;
    translate: 0 -10px;
  }
}

.conn-banner.online {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

.conn-banner.offline,
.conn-banner.error {
  background: var(--danger-soft);
  color: var(--danger);
}

.conn-banner.connecting {
  background: var(--warn-soft);
  color: var(--warn);
}

/* An inline message attached to the thing that produced it — unlike .conn-banner,
   which is pinned to the top because it describes the whole screen's state. */
.notice {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 10px 14px;
  border-radius: var(--r-md);
  font-size: 0.9rem;
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text);
}

.notice-error {
  background: var(--danger-soft);
  color: var(--danger);
}

.notice-warn {
  background: var(--warn-soft);
  color: var(--warn);
}

.notice-ok {
  background: var(--accent-soft);
  color: var(--accent);
}

/* --- Components: banners --------------------------------------------------- */

.closed-banner,
.winner-banner,
.segment-banner {
  border-radius: var(--r-md);
  font-weight: 700;
  text-align: center;
}

.closed-banner {
  margin: var(--sp-3) 0 0;
  padding: 10px 14px;
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 0.9rem;
}

.winner-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  margin: var(--sp-3) 0;
  padding: 14px;
  border-radius: var(--r-lg);
  background: var(--gold-soft);
  color: var(--gold);
  font-size: 1.05rem;
  animation: winner-in 560ms var(--ease-out) backwards;
}

@keyframes winner-in {
  from {
    opacity: 0;
    scale: 0.96;
    translate: 0 6px;
  }
}

.segment-banner {
  margin: var(--sp-3) 0 0;
  padding: 9px 14px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.9rem;
}

/* --- Screen: home ---------------------------------------------------------- */

.hero {
  padding: var(--sp-5) 0 var(--sp-4);
}

.hero-mark {
  display: block;
  margin-bottom: var(--sp-3);
  color: var(--text);
}

.hero h1 {
  font-size: 2.1rem;
  margin: 0 0 var(--sp-2);
}

.hero p {
  color: var(--muted);
  margin: 0;
  max-width: 34ch;
}

/* Four letters, spaced out and centred — it looks like the thing being typed. */
.code-input {
  font-family: var(--font-mono);
  font-size: 1.85rem;
  font-weight: 700;
  letter-spacing: 0.42em;
  text-indent: 0.42em; /* keeps the glyphs optically centred despite the tracking */
  text-align: center;
  min-height: 64px;
  padding: 8px;
  background: var(--surface);
  border-width: 2px;
}

.code-input:focus {
  border-color: var(--accent);
  outline: none;
}

.action-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}

.action-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
  min-height: 0;
  padding: var(--sp-4);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  color: var(--text);
  font-weight: 700;
  box-shadow: var(--shadow-1);
}

@media (hover: hover) {
  .action-card:hover:not(:disabled) {
    background: var(--surface-2);
    border-color: var(--accent-line);
  }
}

.action-card .icon {
  color: var(--accent);
}

.action-card small {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
  line-height: 1.3;
}

/* A remembered game, or a roster name to claim. */
.tap-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.tap-list button,
.list-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  text-align: left;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-weight: 700;
  padding: 12px 14px;
  box-shadow: var(--shadow-1);
}

@media (hover: hover) {
  .tap-list button:hover:not(:disabled) {
    background: var(--surface-2);
    border-color: var(--accent-line);
  }
}

.tap-list button.claimed {
  opacity: 0.55;
}

.list-main {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list-sub {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
}

.avatar {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.85rem;
  font-weight: 800;
}

.footer-links {
  display: flex;
  justify-content: center;
  gap: var(--sp-4);
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

.footer-links button {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
  min-height: 40px;
  padding: 0 var(--sp-2);
}

@media (hover: hover) {
  .footer-links button:hover:not(:disabled) {
    background: none;
    color: var(--text);
  }
}

/* --- Screen: scorecard ----------------------------------------------------- */

.course-line {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
  font-size: 0.9rem;
}

.stat-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.hole-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: var(--sp-3) var(--sp-3) var(--sp-4);
  margin: var(--sp-3) 0;
  box-shadow: var(--shadow-1);
}

.hole-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
}

.hole-nav button {
  min-width: 56px;
  min-height: 56px;
  padding: 0;
  border-radius: var(--r-md);
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text);
}

@media (hover: hover) {
  .hole-nav button:hover:not(:disabled) {
    background: var(--surface-3);
  }
}

.hole-nav > div {
  flex: 1;
  min-width: 0;
  text-align: center;
}

.hole-number {
  font-size: 2.4rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}

.hole-number small {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.hole-meta {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: var(--sp-2);
  flex-wrap: wrap;
}

/* One tick per hole in the round — position at a glance, and a tap target free
   way to see how far in you are. */
.hole-rail {
  display: flex;
  gap: 3px;
  margin-top: var(--sp-3);
}

.hole-rail span {
  flex: 1;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  transition: background 260ms var(--ease);
}

.hole-rail span.done {
  background: var(--accent-line);
}

.hole-rail span.here {
  background: var(--accent);
}

.player-score-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
}

.player-score-row .who {
  flex: 1;
  min-width: 0;
  font-weight: 700;
}

.player-score-row .who small {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
}

.score-stepper {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.score-stepper button {
  min-width: 48px;
  width: 48px;
  min-height: 48px;
  font-size: 1.5rem;
  line-height: 1;
  border-radius: 50%;
  padding: 0;
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text);
}

@media (hover: hover) {
  .score-stepper button:hover:not(:disabled) {
    background: var(--surface-3);
  }
}

.score-stepper .value,
.score-readonly {
  /* Same footprint either way, so rows stay aligned whether or not the score is
     yours to edit. */
  min-width: 52px;
  text-align: center;
  font-size: 1.7rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  transition: color var(--dur) var(--ease);
}

.score-stepper .value.pop {
  animation: score-pop 300ms var(--ease);
}

@keyframes score-pop {
  40% {
    scale: 1.18;
  }
}

.score-readonly {
  color: var(--muted);
  padding-right: var(--sp-2);
}

.tone-eagle { color: var(--tone-eagle); }
.tone-birdie { color: var(--tone-birdie); }
.tone-bogey { color: var(--tone-bogey); }
.tone-double { color: var(--tone-double); }

.team-group {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  box-shadow: var(--shadow-1);
}

.team-heading {
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  padding: var(--sp-2) 0;
}

.team-group .player-score-row {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.team-group .player-score-row:last-child {
  border-bottom: none;
}

.wolf-panel {
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  padding: var(--sp-3) var(--sp-4);
  margin: var(--sp-3) 0;
}

.wolf-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

.wolf-choices button {
  flex: 1 1 auto;
}

/* --- Screen: leaderboard --------------------------------------------------- */

.lb {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A standings row is a button (it opens the breakdown), so it has to opt out of the
   primary-button base above — otherwise it inherits accent-on-accent colouring and,
   worse, `button:hover` repaints the whole row green. */
.lb-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-3);
  width: 100%;
  padding: 12px 14px;
  text-align: left;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  transition:
    background 480ms var(--ease),
    border-color 480ms var(--ease);
  /* The entrance uses `translate`, not `transform`, on purpose: useFlipList writes
     `transform` when the board reorders, and the two properties compose instead of
     one clobbering the other if a reorder arrives while a row is still entering. */
  animation: lb-enter 420ms var(--ease-out) backwards;
  animation-delay: calc(min(var(--i, 0), 8) * 60ms);
}

@keyframes lb-enter {
  from {
    opacity: 0;
    translate: 0 8px;
  }
}

/* Hover has to be stated for each state: `button:hover:not(:disabled)` outranks a
   single class, so without these the base rule's accent fill wins. */
@media (hover: hover) {
  button.lb-row:hover:not(:disabled) {
    background: var(--surface-2);
    border-color: var(--accent-line);
  }
}

/* These three keep their hover halves outside the (hover: hover) gate on purpose:
   each also carries the base-state selector, so wrapping them would take the tint
   away from phones entirely. On a touch device the hover half can still match via
   sticky hover, but it declares exactly the same values, so nothing moves. */
.lb-row.leader,
button.lb-row.leader:hover:not(:disabled) {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* A row that just changed places, held for as long as the component keeps the class
   on (~2.2s) and then transitioning back when it drops it.
   Transitions rather than a keyframe animation on purpose: adding a second animation
   to the shorthand above would restart the entrance one at the same time, so a row
   would re-enter every time it changed rank. */
.lb-row.moved-up,
button.lb-row.moved-up:hover:not(:disabled) {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.lb-row.moved-down,
button.lb-row.moved-down:hover:not(:disabled) {
  background: var(--surface-2);
}

.rank-move {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  font-size: 0.75rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  /* The row it labels has already finished travelling by the time this mounts (the
     cue is delayed by FLIP_MS), so it arrives into a still frame. */
  animation: cue-in 220ms var(--ease-out) backwards;
}

@keyframes cue-in {
  from {
    opacity: 0;
    translate: 0 4px;
  }
}

.rank-move.up {
  color: var(--accent);
}

.rank-move.down {
  color: var(--faint);
}

.rank {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.rank.rank-1 {
  background: var(--gold-soft);
  color: var(--gold);
}

.rank.rank-2,
.rank.rank-3 {
  background: var(--accent-soft);
  color: var(--accent);
}

.lb-name {
  flex: 1;
  min-width: 0;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lb-points {
  font-size: 1.25rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.projected {
  margin-left: 5px;
  /* Halftone: present but clearly secondary to the confirmed score. */
  opacity: 0.6;
  font-size: 0.8em;
  font-weight: 700;
  color: var(--accent);
}

.projected-note {
  font-style: italic;
}

table.scorecard-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

table.scorecard-table th {
  font-size: 0.7rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  text-align: left;
}

table.scorecard-table th,
table.scorecard-table td {
  padding: 8px 6px;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

table.scorecard-table td + td,
table.scorecard-table th + th {
  text-align: right;
}

table.scorecard-table tr:last-child td {
  border-bottom: none;
}

/* The course editor puts number inputs in cells; full-size fields would make an
   18-row table taller than the screen. */
table.scorecard-table input {
  min-height: 40px;
  padding: 6px 8px;
  text-align: right;
}

/* --- Don mode -------------------------------------------------------------- */
/* The app's easter egg (shared/donMode.ts), across the two screens it touches: the
   button that fires it, on Don's own breakdown page, and the robbery it sets off on
   the standings. Everything here is gold — the colour this app already reserves for
   first place — so a crowned row looks like the real thing rather than like a
   rendering fault.

   Nothing marks the standings before he fires it. An earlier version hid the trigger
   on his row behind a press-and-hold and tried to advertise it with a pulse on his
   rank badge; a wordless cue can't teach a gesture nobody expects, and it stayed
   undiscovered. The offer is made in words now, on a page that belongs to one player,
   which leaves the board itself with nothing to explain. */

.don-panel {
  border-color: var(--gold);
}

/* Hold-to-fire. Both selectors carry the base state, and the hover half is stated
   outside the (hover: hover) gate on purpose — it declares the same values, and
   `button:hover:not(:disabled)` is (0,2,1), so a plain `.don-fire` would silently lose
   the button its colour the moment a mouse rested on it. */
button.don-fire,
button.don-fire:hover:not(:disabled) {
  position: relative;
  overflow: hidden;
  width: 100%;
  margin-top: var(--sp-3);
  background: var(--gold-soft);
  color: var(--gold);
  font-weight: 800;
}

/* The charge, along the button's bottom edge. A track rather than a fill behind the
   label: the two themes' golds sit on opposite sides of the label's lightness — light
   theme's is a dark brown, dark theme's is a bright straw — so a label spanning a
   half-filled button cannot be legible in both. Grows in step with DON_HOLD_MS:
   change one, change the other. */
.don-fire::before {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 4px;
  background: var(--gold);
  transform-origin: left;
  scale: 0 1;
  /* Release snaps back quickly — an aborted hold shouldn't look like it half-counted. */
  transition: scale 160ms var(--ease);
}

/* This one keeps moving under prefers-reduced-motion, and not by accident: the global
   cutoff near the top of this file is a `*` rule, which never matches a pseudo-element.
   It is the right outcome either way — the bar isn't decoration, it's the only thing
   telling you how much longer to hold, and snapping it to full would leave you pressing
   a button with no idea when to stop. */
.don-fire:active::before {
  scale: 1 1;
  transition-duration: 800ms;
  transition-timing-function: linear;
}

.lb-row.crowned {
  position: relative;
  overflow: hidden;
}

.lb-row.crowned,
button.lb-row.crowned:hover:not(:disabled) {
  background: var(--gold-soft);
  border-color: var(--gold);
}

.crown {
  flex: 0 0 auto;
  color: var(--gold);
}

/* A slow gloss across the crowned row. Transform-only, so it stays on the compositor
   for the whole minute it runs — and it inherits the global reduced-motion cutoff,
   which stops it after one pass rather than looping. */
.lb-row.crowned::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 35%, var(--gold-soft) 50%, transparent 65%);
  translate: -100% 0;
  animation: shimmer 2600ms linear infinite;
  pointer-events: none;
}

/* A robbed row. The points cell has already counted itself down to nothing by the
   time this lands — this is the row noticing. */
.lb-row.robbed .lb-points {
  color: var(--faint);
  opacity: 0.55;
  transition:
    color 480ms var(--ease),
    opacity 480ms var(--ease);
}

/* A stolen total, in flight between two rows. Parked on the body rather than inside
   the list so the row's own `overflow: hidden` can't clip it, and out of the way of
   pointers so a token crossing the board can't eat a tap. */
.steal-token {
  position: fixed;
  z-index: 40;
  font-size: 1.25rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--gold);
  text-shadow: 0 1px 3px var(--bg);
  pointer-events: none;
  will-change: transform, opacity;
}

.don-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin: var(--sp-3) 0;
  padding: 12px 14px;
  border-radius: var(--r-lg);
  background: var(--gold-soft);
  color: var(--gold);
  text-align: center;
  transition:
    background 400ms var(--ease),
    color 400ms var(--ease);
  animation: winner-in 560ms var(--ease-out) backwards;
}

.don-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: 800;
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Out of the title's uppercase: the unit is a lowercase s, and "59S" reads as an
   abbreviation rather than as a clock. */
.don-clock {
  text-transform: none;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

.don-banner small {
  font-size: 0.78rem;
  font-weight: 600;
  opacity: 0.85;
}

/* Louder while the points are in the air, then it settles into the reign banner. The
   two are the same element, so the change is a transition rather than one banner
   replacing another and shoving the board down a line mid-heist. */
.don-banner.is-robbery {
  background: var(--gold);
  color: var(--accent-ink);
}

/* The epitaph is quieter than the reign it follows — it's a caption, and it sits on
   screen until the player navigates away. */
.don-banner.is-over {
  background: var(--surface-2);
  color: var(--muted);
}

/* --- Screen: score breakdown ----------------------------------------------- */

.back-link {
  margin: 0 0 var(--sp-3);
  font-size: 0.9rem;
  font-weight: 700;
}

.back-link a {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  text-decoration: none;
}

@media (hover: hover) {
  .back-link a:hover {
    text-decoration: underline;
  }
}

/* A column of arithmetic: label (with an optional aside) on the left, figure on the
   right, and the answer marked out at the bottom. */
.calc {
  display: flex;
  flex-direction: column;
}

.calc-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  width: 100%;
}

.calc-row:last-child {
  border-bottom: none;
}

.calc-label {
  flex: 1;
  min-width: 0;
  font-weight: 600;
}

.calc-label small {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
  line-height: 1.35;
}

.calc-value {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.calc-total {
  border-top: 2px solid var(--line-strong);
  border-bottom: none;
  margin-top: 2px;
  padding-top: 12px;
}

.calc-total .calc-label,
.calc-total .calc-value {
  font-size: 1.15rem;
  font-weight: 800;
  color: var(--accent);
}

/* A row of the sum that is also a way into the detail behind it. */
button.calc-link {
  /* Centred rather than baseline-aligned like the static rows: these have a fixed
     min-height for the tap target, and a one-line label would otherwise sit at the
     top of it with the slack showing underneath. */
  align-items: center;
  background: none;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  min-height: 56px;
  padding: 10px 0;
  color: var(--text);
  cursor: pointer;
}

@media (hover: hover) {
  button.calc-link:hover:not(:disabled) {
    background: var(--surface-2);
  }
}

button.calc-link .calc-value {
  color: var(--text);
}

button.calc-link.is-open .calc-value .muted-points {
  color: var(--muted);
}

.muted-points {
  color: var(--muted);
}

/* Rows in a breakdown table that lead somewhere. */
tr.row-link {
  cursor: pointer;
}

@media (hover: hover) {
  tr.row-link:hover td {
    background: var(--surface-2);
  }
}

.row-link-chevron {
  display: inline-block;
  vertical-align: -2px;
  margin-left: 6px;
  color: var(--faint);
}

.lb-chevron {
  color: var(--faint);
  flex: 0 0 auto;
}

/* The round's name is the way into its breakdown. Given a tap target of its own so
   it reads as a control, without stealing the taps on the player rows below it. */
.round-name-link {
  cursor: pointer;
  border-radius: var(--r-sm);
  margin: calc(-1 * var(--sp-2));
  padding: var(--sp-2);
}

@media (hover: hover) {
  .round-name-link:hover {
    background: var(--surface-2);
  }
}

tr.row-empty td {
  color: var(--faint);
}

.cell-note {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

/* An 18-row breakdown with a result column is wider than a phone; it scrolls in its
   own box rather than making the page scroll sideways. */
.table-scroll {
  overflow-x: auto;
  margin: 0 calc(-1 * var(--sp-4));
  padding: 0 var(--sp-4);
}

/* --- Screen: round breakdown (whole field, hole by hole) -------------------- */

/* The hole a side won. Colour alone can't carry it — a fifth of men can't reliably
   separate these two — so the weight change and the .sr-only text below do the work
   and the tint is the fast path for everyone else. */
.grid-win {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 800;
  border-radius: var(--r-sm);
}

/* Gross and net are one reading of one hole; letting them break across two lines
   turns "4 3" into something that looks like two separate scores. */
.grid-table td {
  white-space: nowrap;
}

/* The result is the column people came for, so it wraps rather than sitting off the
   right edge behind a horizontal scroll like the other nowrap notes do. */
.contest-result {
  white-space: normal;
  min-width: 5rem;
}

/* A two-side match needs no more width than its numbers; without this the status
   column takes every spare pixel and leaves the sides huddled left of centre. */
.contest-table th:first-child,
.contest-table td:first-child {
  width: 1%;
}

/* Wolf's sides change every hole, so each hole is a block rather than a table row —
   see ShiftingSidesList for why a table couldn't hold it. */
.contest-hole {
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--line);
}

.contest-hole:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.contest-hole.is-undecided {
  color: var(--faint);
}

.contest-hole-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  margin-bottom: 2px;
}

.contest-hole-num {
  font-weight: 700;
  font-size: 0.82rem;
  white-space: nowrap;
}

.contest-hole-note {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  text-align: right;
}

/* Label left, number right, on one line each — the number can never wrap away from
   the side it belongs to. */
.contest-side {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  font-size: 0.88rem;
  padding: 2px 0;
}

.contest-side-value {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}

.contest-side.is-won {
  color: var(--accent);
  font-weight: 700;
}

/* The net beside the gross, where handicap strokes moved it. */
.grid-net {
  color: var(--muted);
  font-weight: 600;
  margin-left: 4px;
}

/* A side's number is the headline; the balls behind it sit underneath, smaller. */
.grid-side-value {
  font-weight: 700;
  display: block;
}

.grid-side-balls {
  color: var(--muted);
  font-weight: 600;
  display: block;
  font-size: 0.72rem;
}

/* The ball the side actually counted — four-ball's better net. */
.grid-ball-counts {
  color: var(--text);
  font-weight: 800;
}

/* What the column above added up to, and what it was worth. */
.grid-table tfoot th,
.grid-table tfoot td {
  border-bottom: none;
  white-space: nowrap;
}

/* Separates the sums from the holes they sum, the way .calc-total does on the
   per-player breakdown. */
.grid-table tfoot tr:first-child th,
.grid-table tfoot tr:first-child td {
  border-top: 2px solid var(--line-strong);
}

/* The label is a `th` spanning Hole/Par/SI, so the first figure beside it isn't a
   `td + td` and would otherwise sit left while the rest of the row sits right. */
.grid-table tfoot td {
  text-align: right;
  font-weight: 700;
}

.grid-table tfoot .grid-points td {
  color: var(--accent);
  font-weight: 800;
}

/* Which run of holes these sides played, for a round whose partners change. */
.grid-section-title {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: var(--sp-3) 0 0;
}

/* With a column per player the grid always scrolls sideways, and losing the hole
   number to the left edge makes a row unreadable. */
.grid-table th:first-child,
.grid-table td:first-child {
  position: sticky;
  left: 0;
  background: var(--surface);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- Screen: setup --------------------------------------------------------- */

.player-edit-row {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-end;
}

.remove-btn {
  min-width: 48px;
  width: 48px;
  padding: 0;
  flex: 0 0 auto;
  color: var(--muted);
}

/* One segment of a team round: its hole range, then the sides as visible groups.
   Players sit under the team they're on rather than in roster order — with a
   threesome or someone sitting out, a flat list forces you to read the grouping
   out of a column of dropdowns. A select per player rather than drag-and-drop,
   because this gets edited one-handed on a course. */
.segment-editor {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  margin-top: var(--sp-3);
}

.segment-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.segment-head .muted {
  flex: 1;
}

.side-group {
  border-left: 3px solid var(--accent);
  padding: 6px 0 6px var(--sp-3);
  margin: var(--sp-3) 0;
}

.side-group-out {
  border-left-color: var(--line-strong);
}

.side-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
}

.side-name {
  font-weight: 700;
}

.side-count {
  flex: 1;
  color: var(--muted);
  font-size: 0.82rem;
}

.side-empty {
  color: var(--muted);
  font-size: 0.85rem;
  font-style: italic;
  margin: 2px 0;
}

.segment-player-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 4px 0;
}

.segment-player-row > span {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.segment-player-row select {
  flex: 0 0 auto;
  width: 9.5rem;
}

.place-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin: var(--sp-2) 0;
}

.place-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.place-label {
  flex: 0 0 44px;
  font-weight: 700;
  color: var(--muted);
}

.share-wrap {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin: var(--sp-2) 0 var(--sp-3);
}

.share-status {
  color: var(--accent);
  font-size: 0.9rem;
  font-weight: 700;
}

.nested-config {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  margin-top: var(--sp-3);
}

.nested-config legend {
  padding: 0 6px;
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.rules-preview {
  margin-top: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  background: var(--surface-2);
}

.rules-preview summary {
  cursor: pointer;
  font-weight: 700;
}

.rules-preview ul {
  margin: var(--sp-2) 0 0;
  padding-left: 20px;
}

.rules-preview li {
  margin-bottom: 6px;
}

.rule-list {
  margin: var(--sp-2) 0 0;
  padding-left: 20px;
  font-size: 0.92rem;
}

.rule-list li {
  margin-bottom: 6px;
}

.tee-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.tee-tabs button {
  flex: 1 1 auto;
  min-height: 44px;
  padding: 8px 14px;
}

/* --- Screen: admin --------------------------------------------------------- */

.usage-metric {
  margin-bottom: var(--sp-4);
}

.meter {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  overflow: hidden;
  margin: 6px 0 4px;
}

.meter-fill {
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur) var(--ease);
}

.meter-fill.warn {
  background: var(--warn);
}

.meter-fill.danger {
  background: var(--danger);
}

/* --- Narrow phones --------------------------------------------------------- */

@media (max-width: 379px) {
  main {
    padding-left: var(--sp-3);
    padding-right: var(--sp-3);
  }

  .app-me {
    display: none;
  }

  .action-grid {
    grid-template-columns: 1fr;
  }

  .score-stepper button {
    min-width: 44px;
    width: 44px;
    min-height: 44px;
  }

  .score-stepper .value,
  .score-readonly {
    min-width: 42px;
    font-size: 1.5rem;
  }
}
