/* =============================================================================
   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;
  /* Course illustration. Deliberately quieter than an aerial photo: this is a
     block of colour sitting inside an otherwise light, airy card, and a
     saturated one dominates the whole screen. Muted sage rough, soft fairway,
     lightest of all on the putting surface — the reference stacking order, at a
     fraction of the intensity. */
  --course-rough: #bad4c1;
  --course-fairway: #8bc99a;
  --course-fairway-line: #64a877;
  --course-green: #d8eda6;
  --course-green-line: #8cb863;
  --course-bunker: #f3e5c4;
  --course-bunker-line: #d6bf8e;
  --course-water: #a9d6ef;
  --course-water-line: #79b6da;
  --course-apron: #bfdea2;
  --course-apron-line: #93b775;
  /* Deliberately cooler and paler than the fairway. They were two shades of the same
     green and, at the size this draws, indistinguishable. */
  --course-tee: #b7d8c4;
  --course-tee-line: #7ea992;
  --course-tee-marker: #2f4a3b;
  --course-path: rgba(255, 255, 255, 0.8);
  /* Solid, and cased, for the whole-course view: at that scale a translucent hairline
     over grass is not a line at all. */
  --course-cartpath: #f4f6f2;
  --course-cartpath-line: #8fa294;
  --course-pin: #35513f;
  --course-pin-flag: #d6453a;
  --course-me: #10361f;
  --course-me-halo: rgba(16, 54, 31, 0.18);
  --course-landmark: #ffffff;
  --course-landmark-ink: #24402f;

}

/* 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;

    /* Deeper and less saturated than the light ramp: the same colours on a dark
       card glow, and a fairway that glows reads as an alert rather than as grass. */
    --course-rough: #16351f;
    --course-fairway: #2a7047;
    --course-fairway-line: #17492c;
    --course-green: #74b96a;
    --course-green-line: #3f7c42;
    --course-bunker: #c4ab7c;
    --course-bunker-line: #907a51;
    --course-water: #3d84ae;
    --course-water-line: #2a6285;
    --course-apron: #4b9a5c;
    --course-apron-line: #2c6b3d;
    --course-tee: #3d8871;
    --course-tee-line: #1d5546;
    --course-tee-marker: #eaf7ef;
    --course-path: rgba(238, 245, 240, 0.55);
    --course-cartpath: #dfe8e1;
    --course-cartpath-line: #55665c;
    --course-pin: #f2f7f4;
    --course-pin-flag: #ff6b5e;
    --course-me: #e8fff2;
    --course-me-halo: rgba(232, 255, 242, 0.2);
    --course-landmark: #10251a;
    --course-landmark-ink: #dff0e5;

  }
}

: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;

  /* Deeper and less saturated than the light ramp: the same colours on a dark
     card glow, and a fairway that glows reads as an alert rather than as grass. */
  --course-rough: #16351f;
  --course-fairway: #2a7047;
  --course-fairway-line: #17492c;
  --course-green: #74b96a;
  --course-green-line: #3f7c42;
  --course-bunker: #c4ab7c;
  --course-bunker-line: #907a51;
  --course-water: #3d84ae;
  --course-water-line: #2a6285;
  --course-apron: #4b9a5c;
  --course-apron-line: #2c6b3d;
  --course-tee: #3d8871;
  --course-tee-line: #1d5546;
  --course-tee-marker: #eaf7ef;
  --course-path: rgba(238, 245, 240, 0.55);
  --course-cartpath: #dfe8e1;
  --course-cartpath-line: #55665c;
  --course-pin: #f2f7f4;
  --course-pin-flag: #ff6b5e;
  --course-me: #e8fff2;
  --course-me-halo: rgba(232, 255, 242, 0.2);
  --course-landmark: #10251a;
  --course-landmark-ink: #dff0e5;

}

/* --- 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); }
.nowrap { white-space: nowrap; }
.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: var(--beta-h, 0px);
  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);
}

/* --- Chrome: beta banner -------------------------------------------------- */

/*
 * Only ever on beta.divotgolf.club: the Worker adds data-channel="beta" and the banner
 * element there (worker/lib/channel.ts); production's pages have neither, so none of
 * this applies. The banner sticks above the app bar, and --beta-h moves everything
 * else that sticks down by its height, so it stays on screen while scrolling.
 */
html[data-channel="beta"] {
  --beta-h: 30px;
}

.beta-banner {
  position: sticky;
  top: 0;
  z-index: 40; /* above the app bar (30), under the launch screen (50) */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--beta-h, 30px);
  padding: 0 var(--sp-3);
  background: #e7a93b;
  color: #1d1300;
  font-size: 0.8rem;
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
}

.beta-banner strong {
  padding: 3px 6px;
  border-radius: 4px;
  background: #1d1300;
  color: #e7a93b;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
}

.beta-banner span {
  overflow: hidden;
  text-overflow: ellipsis;
}

.beta-banner a {
  color: inherit;
  font-weight: 700;
  text-decoration: underline;
}

@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. */

/* A launch screen, not a loading hint — it covers the app rather than standing in for
   it, so the app can render underneath while this plays out. z-index clears 40, the
   highest otherwise in use here. `--bg` is the same token body paints with, so the
   handoff doesn't flash a different colour in either theme. */
.splash {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  background: var(--bg);
  transition: opacity 260ms ease-out;
}

/* main.ts sets this once the strike has played, then removes the node on transitionend.
   pointer-events has to go with it: for the length of the fade this is still a
   full-screen layer sitting over every control underneath. */
.splash.is-done {
  opacity: 0;
  pointer-events: none;
}

/* 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;
}

/* 1.6s: the strike takes the first 1.0s (to 62.5%), then a beat of stillness before it
   repeats. main.ts holds the screen for that 1.0s and then hands off, so on a normal
   load you see exactly one strike; the loop is only reached when the app is genuinely
   slow to arrive. 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: 1.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%,
  5% {
    transform: translate(-30px, 32px);
  }
  12.75% {
    transform: translate(-25.4px, 22.3px);
  }
  21.9% {
    transform: translate(-19.1px, 14.3px);
  }
  33.75% {
    transform: translate(-11.6px, 8.1px);
  }
  62.5%,
  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%,
  5% {
    stroke-dashoffset: 1;
  }
  12.75% {
    stroke-dashoffset: 0.75;
  }
  21.9% {
    stroke-dashoffset: 0.5;
  }
  33.75% {
    stroke-dashoffset: 0.25;
  }
  62.5%,
  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%,
  5% {
    transform: translate(10px, 10px) rotate(40deg) scale(0.25);
    opacity: 0;
  }
  5.01% {
    transform: translate(10px, 10px) rotate(40deg) scale(0.25);
    opacity: 1;
  }
  15% {
    transform: translate(2px, -1px) rotate(-12deg) scale(1.05);
    opacity: 1;
  }
  32.5%,
  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;
}

/* Anchors the identity menu, which is positioned against this rather than the app bar
   so it stays under the badge whatever else is in the bar. */
.app-identity-wrap {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
}

/* The badge as a control, for an organiser who is also playing. Deliberately not
   styled as a button — it is the same label it always was, and dressing it up would
   put a second competing control next to the theme and home buttons. */
button.app-identity-btn {
  background: none;
  border: none;
  padding: 2px 4px;
  min-height: 36px;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  border-radius: var(--r-sm);
}

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

/* Stays lit in either mode: it is the only remaining sign that this device holds the
   organiser's token, and an organiser in player mode who can't see one has no reason
   to look for a way back. */
.app-organizer-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
}

.identity-backdrop {
  position: fixed;
  inset: 0;
  z-index: 40;
}

.identity-menu {
  position: absolute;
  top: calc(100% + var(--sp-2));
  right: 0;
  z-index: 41;
  min-width: 240px;
  max-width: min(300px, calc(100vw - var(--sp-4)));
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
}

.identity-menu .identity-option {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  background: none;
  border: none;
  color: var(--text);
  font-weight: 600;
  padding: 10px 12px;
}

.identity-menu .identity-option .list-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

@media (hover: hover) {
  .identity-menu .identity-option:hover:not(:disabled) {
    background: var(--surface-2);
  }
}

.identity-menu .identity-option.is-current {
  color: var(--accent);
}

/* A button that has to sit inside a sentence — the switch offered mid-paragraph on the
   scorecard and in setup. Everything else in the app is a block control, so this is the
   one place the button base has to be undone rather than adjusted. */
button.linkish {
  display: inline;
  min-height: 0;
  padding: 0;
  background: none;
  border: none;
  color: var(--accent);
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}

@media (hover: hover) {
  button.linkish:hover:not(:disabled) {
    background: none;
    color: var(--accent-hover);
  }
}

.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(--beta-h, 0px) + 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;
}

/* A pasted link is long and has no spaces to break on, so it gets a box that wraps
   rather than a single-line input that turns into a horizontal scroll trap on a phone.
   Monospace because what you're checking is whether it got cut short. */
.paste-box {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  line-height: 1.5;
  resize: vertical;
  word-break: break-all;
}

.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);
  }

  /* A .list-row that is itself a button. Needs the same shape as the base button
     hover rule to beat it — see the note on specificity above `button:hover`;
     without it, hovering a saved course turns the whole row accent green. */
  .list-row-tap: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;
  /* Wraps because there is now more than one of these, and two labels don't fit side
     by side on a 320px phone. */
  flex-wrap: wrap;
  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;
}

/* The side names are player names joined with "+", so they are the part that has to
   give. Without `min-width: 0` a flex item refuses to shrink below its content, which
   on a narrow phone pushes the score off the right-hand edge of the card. */
.contest-side-label {
  min-width: 0;
  overflow-wrap: anywhere;
}

.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);
}

/* "That's me" — which roster name the organiser is playing under. Sits with the
   remove button at the end of the row, sized the same, because both are per-player
   actions and a row of matching squares reads faster than two shapes. */
.seat-btn {
  min-width: 48px;
  width: 48px;
  padding: 0;
  flex: 0 0 auto;
  color: var(--muted);
}

.seat-btn.is-seat {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
}

@media (hover: hover) {
  .seat-btn.is-seat:hover:not(:disabled) {
    background: var(--accent-hover);
  }
}

/* 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;
}

/* "Add tee" sits in the same row as the tee tabs but isn't one of them — it doesn't
   stretch to share the row, so the tabs still read as a set of choices. */
.tee-tabs button.ghost {
  flex: 0 0 auto;
  color: var(--muted);
}

/* --- 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);
}

/* --- Screen: clubhouse ------------------------------------------------------ */

/*
 * The screen you prop up at the bar.
 *
 * Every length below is a multiple of --club-scale, which the screen sets from the
 * viewport and the size of the field (see `clubScale`). That is the whole responsive
 * strategy: one number, no breakpoints, so a 13" laptop and a 55" television get the
 * same layout at different sizes rather than two layouts that have to be kept in
 * agreement.
 */

main.bare {
  padding: 0;
}

/* Set on <html> by the clubhouse screen for as long as it is mounted. `#app`'s 620px
   cap is the right measure for a page held in one hand and the wrong one for a page
   across a room. */
:root.clubhouse-mode #app {
  max-width: none;
}

.clubhouse {
  --club-scale: 1;
  --club-gap: calc(10px * var(--club-scale));
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--club-gap);
  min-height: 100vh;
  min-height: 100dvh;
  padding: calc(22px * var(--club-scale)) calc(30px * var(--club-scale));
  background: var(--bg);
  overflow: hidden;
}

.clubhouse.is-loading {
  align-items: center;
  justify-content: center;
  color: var(--muted);
}

.club-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--club-gap);
}

.club-eyebrow {
  display: flex;
  align-items: center;
  gap: calc(10px * var(--club-scale));
  font-size: calc(13px * var(--club-scale));
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Says whether the numbers below are one round or the whole game. On a screen that
   changes itself every fifteen seconds, whoever just looked up has no idea which
   panel they've arrived in the middle of. */
.club-scope {
  padding: calc(3px * var(--club-scale)) calc(9px * var(--club-scale));
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  letter-spacing: 0.1em;
}

.club-title {
  margin: calc(2px * var(--club-scale)) 0 0;
  font-size: calc(34px * var(--club-scale));
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.club-head-right {
  display: flex;
  align-items: center;
  gap: calc(14px * var(--club-scale));
  flex: none;
}

/* Where the group is. Sits beside the code because it is the other thing about this
   screen that stays true all afternoon. */
.club-progress-read {
  font-size: calc(19px * var(--club-scale));
  font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}

.club-code {
  flex: none;
  padding: calc(6px * var(--club-scale)) calc(14px * var(--club-scale));
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: calc(20px * var(--club-scale));
  letter-spacing: 0.16em;
  color: var(--muted);
}

.club-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  animation: club-panel 420ms var(--ease-out) both;
}

@keyframes club-panel {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

/* --- panel: standings --- */

/* Capped and centred rather than edge to edge: a name at one side of a 55" screen and
   its points at the other is two separate things to look at, not one row. */
.club-board {
  display: flex;
  flex-direction: column;
  gap: calc(8px * var(--club-scale));
  width: 100%;
  max-width: calc(1000px * var(--club-scale));
  margin: 0 auto;
}

.club-row {
  display: grid;
  grid-template-columns: calc(64px * var(--club-scale)) 1fr auto auto;
  align-items: center;
  gap: calc(16px * var(--club-scale));
  padding: calc(12px * var(--club-scale)) calc(20px * var(--club-scale));
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  animation: lb-enter 420ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 8) * 60ms);
}

.club-row.leader {
  border-color: var(--gold);
  background: var(--gold-soft);
}

/* The row that just took a score. A tint rather than a flash: this is on a wall in
   somebody's peripheral vision, and a hard blink at that size is an irritation. */
.club-row.struck {
  border-color: var(--accent);
  background: var(--accent-soft);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.club-pos {
  font-family: var(--font-mono);
  font-size: calc(40px * var(--club-scale));
  font-weight: 700;
  line-height: 1;
  color: var(--muted);
  text-align: center;
}

.club-row.leader .club-pos {
  color: var(--gold);
}

.club-name {
  font-size: calc(32px * var(--club-scale));
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.club-move {
  display: inline-flex;
  align-items: center;
  gap: calc(4px * var(--club-scale));
  font-size: calc(20px * var(--club-scale));
  font-weight: 700;
  animation: cue-in 260ms var(--ease-out) both;
}

.club-row.moved-up .club-move {
  color: var(--tone-birdie);
}

.club-row.moved-down .club-move {
  color: var(--tone-double);
}

.club-points {
  display: flex;
  align-items: baseline;
  gap: calc(8px * var(--club-scale));
  font-family: var(--font-mono);
  font-size: calc(40px * var(--club-scale));
  font-weight: 700;
  line-height: 1;
}

.club-projected {
  font-size: calc(20px * var(--club-scale));
  font-weight: 600;
  color: var(--accent);
}

/* --- panel: round scoreboard --- */

/*
 * Laid out as a golf scoreboard rather than as the app's own points board: position,
 * who, to par, thru, strokes. The grid template is shared by the header and the rows
 * so the columns line up without a table, which a table would have made harder to
 * animate.
 */
.club-score-head,
.club-score-row {
  display: grid;
  grid-template-columns:
    calc(56px * var(--club-scale)) 1fr calc(112px * var(--club-scale))
    calc(112px * var(--club-scale)) calc(84px * var(--club-scale));
  align-items: center;
  gap: calc(12px * var(--club-scale));
}

/* Gross and net side by side, for a round where the handicap changes the answer. */
.club-score-head.has-net,
.club-score-row.has-net {
  grid-template-columns:
    calc(56px * var(--club-scale)) 1fr calc(112px * var(--club-scale))
    calc(112px * var(--club-scale)) calc(84px * var(--club-scale))
    calc(84px * var(--club-scale));
}

.club-score-head {
  padding: 0 calc(20px * var(--club-scale)) calc(6px * var(--club-scale));
  font-size: calc(13px * var(--club-scale));
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
}

/* Every cell, including the name — `.club-name` carries the row's 32px from the
   standings board and would otherwise render this header's "Player" at full size. */
/*
 * `> span` rather than `> *`, and the difference is not cosmetic.
 *
 * A bare `> *` scores the same as the single-class `.club-topar` / `.club-thru` rules
 * further down this file, so source order decided it — and those come later. The
 * result was a header where "Pos" and "Player" were small (their rules sit above this
 * one) while "To par" and "Thru" rendered at row size and collided. Adding the element
 * name outranks a lone class and settles it regardless of where the rules sit.
 */
.club-score-head > span {
  font-family: var(--font);
  font-size: calc(13px * var(--club-scale));
  font-weight: 600;
  color: var(--faint);
  /* A two-line "To / par" over a one-line column is a wobble in the only part of this
     screen that is supposed to hold still. */
  white-space: nowrap;
}

.club-score-row {
  padding: calc(11px * var(--club-scale)) calc(20px * var(--club-scale));
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  animation: lb-enter 420ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 8) * 60ms);
}

.club-score-row.leader {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.club-score-row.leader .club-pos {
  color: var(--gold);
}

.club-score-row.struck {
  border-color: var(--accent);
  background: var(--accent-soft);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* The handicap rides on the name so the row keeps to five columns — it's context for
   the score beside it, not a figure anybody reads down the page. */
/* A bordered chip rather than "(11)": at a glance a number in brackets beside a name
   reads as another score, which on this row is the one thing it must not. */
.club-hcp {
  margin-left: calc(10px * var(--club-scale));
  padding: calc(1px * var(--club-scale)) calc(8px * var(--club-scale));
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: calc(15px * var(--club-scale));
  font-weight: 600;
  color: var(--muted);
  vertical-align: middle;
}

.club-grid-hcp {
  color: var(--muted);
}

/* The figure the panel exists for, so it carries the most weight on the row. */
.club-topar {
  font-family: var(--font-mono);
  font-size: calc(36px * var(--club-scale));
  font-weight: 700;
  line-height: 1;
  text-align: right;
}

.club-topar.under {
  color: var(--tone-birdie);
}

.club-topar.over {
  color: var(--tone-bogey);
}

.club-thru {
  font-size: calc(19px * var(--club-scale));
  color: var(--muted);
  text-align: right;
  white-space: nowrap;
}

/* Gross is context for the net figure on a handicap round, so it steps back; on a
   gross round it is the stroke count and stands on its own. */
.club-gross {
  font-family: var(--font-mono);
  font-size: calc(30px * var(--club-scale));
  font-weight: 600;
  text-align: right;
}

.club-score-row.has-net .club-gross {
  font-size: calc(24px * var(--club-scale));
  color: var(--muted);
}

.club-net {
  font-family: var(--font-mono);
  font-size: calc(30px * var(--club-scale));
  font-weight: 700;
  text-align: right;
}

/* --- panel: live round --- */

.club-grid-wrap {
  overflow: auto;
}

/*
 * Fixed layout, so eighteen holes divide the width they have instead of taking the
 * width they want. An auto-laid table sized itself off its content and ran straight
 * off the side of the screen — and a wall display has nobody there to scroll it, so
 * the back nine simply wasn't on it.
 */
.club-grid {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: calc(22px * var(--club-scale));
}

.club-grid th,
.club-grid td {
  padding: calc(7px * var(--club-scale)) calc(6px * var(--club-scale));
  text-align: center;
  border-bottom: 1px solid var(--line);
}

.club-grid thead th {
  color: var(--muted);
  font-weight: 600;
}

.club-grid-par th {
  font-size: calc(15px * var(--club-scale));
  color: var(--faint);
  border-bottom-color: var(--line-strong);
}

.club-grid-name {
  width: calc(190px * var(--club-scale));
  text-align: left !important;
  font-family: var(--font);
  font-size: calc(24px * var(--club-scale));
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.club-grid-total {
  width: calc(72px * var(--club-scale));
  font-weight: 700;
  border-left: 1px solid var(--line-strong);
}

/* Gross, handicap and net are one group at the end of the card, so the rule that
   separates them from the holes belongs only in front of the first of them. */
.club-grid-total + .club-grid-total {
  border-left: none;
}

.club-grid td.struck {
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 2px var(--accent-line);
}

/* --- panel: rounds so far --- */

.club-rounds {
  display: flex;
  flex-direction: column;
  gap: calc(10px * var(--club-scale));
  width: 100%;
  max-width: calc(1000px * var(--club-scale));
  margin: 0 auto;
}

.club-round {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: calc(18px * var(--club-scale));
  padding: calc(16px * var(--club-scale)) calc(22px * var(--club-scale));
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  animation: lb-enter 420ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 8) * 60ms);
}

.club-round.is-done {
  border-color: var(--accent-line);
}

.club-round-name {
  font-size: calc(28px * var(--club-scale));
  font-weight: 600;
}

.club-round-state {
  font-size: calc(18px * var(--club-scale));
  color: var(--muted);
  white-space: nowrap;
}

.club-round-leader {
  font-size: calc(26px * var(--club-scale));
  font-weight: 600;
  text-align: right;
  color: var(--gold);
}

.club-round-leader small {
  font-family: var(--font-mono);
  font-size: calc(18px * var(--club-scale));
  font-weight: 600;
  color: var(--muted);
}

/* --- rotation progress and controls --- */

.club-progress {
  height: calc(4px * var(--club-scale));
  border-radius: var(--r-pill);
  background: var(--surface-2);
  overflow: hidden;
}

.club-progress-fill {
  height: 100%;
  width: 0;
  background: var(--accent);
}

.club-progress-fill.is-running {
  /* Duration is set inline from PANEL_MS, so the bar can't drift out of step with the
     timer it is reporting on. The value here is only the fallback. */
  animation: club-progress 15s linear forwards;
}

@keyframes club-progress {
  to {
    width: 100%;
  }
}

/*
 * The controls fade out because there is no app bar here to hold them, and a row of
 * buttons is the one thing on this screen that isn't worth looking at from across a
 * room. They come back on any pointer, key or touch.
 */
.club-controls {
  position: absolute;
  right: calc(20px * var(--club-scale));
  bottom: calc(20px * var(--club-scale));
  display: flex;
  align-items: center;
  gap: calc(6px * var(--club-scale));
  padding: calc(6px * var(--club-scale));
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--bg-elev);
  box-shadow: var(--shadow-2);
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
}

.club-controls.is-up {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.club-controls-hint {
  padding-right: calc(10px * var(--club-scale));
  font-size: calc(13px * var(--club-scale));
  color: var(--faint);
  white-space: nowrap;
}

@media (max-width: 599px) {
  /* On a phone the hint is the least useful thing here and the first to go. */
  .club-controls-hint {
    display: none;
  }

  /* Eighteen columns divided by a phone's width is unreadable, so here the grid gets
     its natural width back and the wrapper scrolls — there is a thumb on this one. */
  .club-grid {
    table-layout: auto;
    width: max-content;
    min-width: 100%;
  }

  .club-grid th,
  .club-grid td {
    padding-left: calc(9px * var(--club-scale));
    padding-right: calc(9px * var(--club-scale));
  }

  .club-round {
    grid-template-columns: 1fr auto;
  }

  .club-round-leader {
    grid-column: 1 / -1;
    text-align: left;
  }

  /* Strokes is the first column to go on a phone: to-par is the figure worth the
     width, and the gross is still one panel away on the hole-by-hole grid. */
  .club-score-head,
  .club-score-row,
  .club-score-head.has-net,
  .club-score-row.has-net {
    grid-template-columns:
      calc(46px * var(--club-scale)) 1fr calc(92px * var(--club-scale))
      calc(96px * var(--club-scale));
  }

  /* To par is the figure worth the width on a phone; the raw counts are one panel away
     on the hole-by-hole grid. */
  .club-score-head .club-gross,
  .club-score-row .club-gross,
  .club-score-head .club-net,
  .club-score-row .club-net {
    display: none;
  }

  .club-progress-read {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The screen still updates; it just stops moving. The rotation is already halted in
     JavaScript, so this is the progress bar that would otherwise keep sweeping under
     a panel that is never going to change. */
  .club-stage,
  .club-row,
  .club-score-row,
  .club-round,
  .club-move {
    animation: none;
  }

  .club-progress {
    display: none;
  }
}

/* --- 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;
  }
}

/* Live distance to the green.
   ---------------------------------------------------------------------------
   Every rule here hangs off .green-distance / .gd-* / .hole-plan and nothing
   else, and the spacing is on those elements rather than on .hole-card. That's
   deliberate: with no coordinates for the hole, no permission, or no fix, the
   component renders nothing at all, and the hole card has to be identical to
   what it was before this feature existed — no gap, no leftover margin, no
   collapsed box still holding its padding. Putting the space on the parent
   would leave a seam behind the missing child. */
.green-distance,
.gd-enable,
.gd-notice {
  margin-top: var(--sp-3);
}

.green-distance {
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}

/* The one-time ask, and it only ever appears on a hole that has been traced.
   Deliberately quieter than the buttons below the card: it's an offer, not the
   thing anyone opened this screen to do. */
.gd-enable {
  width: 100%;
  min-height: 42px;
  font-size: 0.9rem;
  color: var(--muted);
}

.gd-live {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  /* Chips drop to a second line rather than squeezing "yd to green" into three. A weak
     fix widens its chip to "±30 yd · weak", and a simulated one adds another beside it;
     on a narrow phone that was enough to crush the unit label under the number. */
  flex-wrap: wrap;
}

/* The number and its unit are one thing and never split across the wrap. */
.gd-yards,
.gd-unit {
  flex: 0 0 auto;
}

/* Read at arm's length, in sun, by someone wearing a glove — so this is the
   second biggest number on the screen, behind the hole number itself. */
.gd-yards {
  font-size: 2.2rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.gd-unit {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
}

.gd-unit small {
  display: block;
  font-size: inherit;
  letter-spacing: 0.06em;
}

/* Phone GPS is roughly ±5 yards at best, and worse under trees. The figure is
   always shown rather than implied, and turns amber when the fix is loose —
   this is a guide to a club, not a rangefinder. */
.gd-plan-toggle {
  min-height: 32px;
  width: 32px;
  color: var(--muted);
}

.gd-plan-toggle .icon {
  transition: rotate 200ms var(--ease);
}

.gd-plan-toggle.is-open .icon {
  rotate: 180deg;
}

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

/* The hole, drawn from the traced outlines — vector, no tiles, no imagery,
   nothing fetched. Satellite is what the course was traced *from*; it is not
   what anyone wants to read one-handed, in sun, with a group waiting.
   Rotated so the hole plays bottom to top, the way a yardage book draws it,
   rather than north-up — which would have you rotating the picture in your head
   on the tee. Both axes share one scale, so the drawing is true.

   Styled as an illustration rather than as a chart: solid fills with darker
   outlines, layered rough -> fairway -> water -> tee -> green -> bunker. The
   first version used the app's translucent --accent-soft washes over the card,
   which turned to mud and lost the one thing this drawing is for — telling
   ground types apart at a glance. */
.hole-plan {
  display: block;
  width: 100%;
  /* Tall, because the drawing is: a hole is many times longer than it is wide and both
     axes share one scale, so height is what the picture is actually made of. Kept to
     roughly the height of the hole card above it so it reads as part of the card
     rather than as a slab dropped on top, and it sits behind a toggle so nobody pays
     for it who didn't ask. */
  height: 260px;
  margin-top: var(--sp-3);
  border-radius: var(--r-md);
  /* The rough, and the only thing that reliably fills the box.
     
     The drawing is letterboxed inside this element by `preserveAspectRatio`, so a rect
     drawn in viewBox units — however oversized — cannot be trusted to reach the edges:
     three times the width of a narrow hole is still narrower than the card. A CSS
     background has no such problem, and the picture itself is clipped to its frame so
     nothing else strays into the bars. */
  background: var(--course-rough);
  overflow: hidden;
}

.hole-plan path:not(.hp-cartpath),
.hole-plan ellipse,
.hole-plan circle,
.hole-plan line {
  /* Strokes stay the same weight however far the drawing is scaled to fit. The cart
     path is excluded on purpose: its width is a real number of yards, and pinning it
     to a pixel weight is exactly what this rule would do. */
  vector-effect: non-scaling-stroke;
  /* Nothing on a golf course has a mitred corner. */
  stroke-linejoin: round;
  stroke-linecap: round;
}

.hp-fairway {
  fill: var(--course-fairway);
  stroke: var(--course-fairway-line);
  stroke-width: 2.5;
}

.hp-green {
  fill: var(--course-green);
  stroke: var(--course-green-line);
  stroke-width: 3;
}

.hp-bunker {
  fill: var(--course-bunker);
  stroke: var(--course-bunker-line);
  stroke-width: 2;
}

.hp-water {
  fill: var(--course-water);
  stroke: var(--course-water-line);
  stroke-width: 2;
}

.hp-tee {
  fill: var(--course-tee);
  stroke: var(--course-tee-line);
  stroke-width: 2;
}

/* The pair of markers you play between. Small on purpose — it is a legend for the
   shape underneath it, not furniture in its own right. */
/* A golf tee, upright, at the middle of each traced box.

   It replaced a pair of marker discs laid across the line of play. Those needed the
   box's long axis derived from its own geometry, to answer a question the picture never
   asks — the plan is already rotated so the hole plays up the page — and at the size a
   tee box occupies on a card they read as two specks rather than as anything. */
.hp-tee-marker {
  fill: var(--course-tee-marker);
  stroke: var(--course-rough);
  stroke-width: 0.5;
  stroke-linejoin: round;
  paint-order: stroke;
}

/* The collar round the green. Between the fairway and the putting surface in tone as
   well as in position, so the eye reads it as the step between them. */
.hp-apron {
  fill: var(--course-apron);
  stroke: var(--course-apron-line);
  stroke-width: 2;
}

.hp-path {
  fill: none;
  stroke: var(--course-path);
  stroke-width: 2;
  stroke-dasharray: 4 4;
}

/* The holes either side, for orientation.

   A hole drawn on its own is every hole on the course — one fairway, one green, a
   frame of rough. What tells a player the drawing is of the ground they are standing
   on is what's *around* it: the hole running back the other way, the green tucked in
   behind, the shape of the corner. So the neighbours are drawn, and drawn faintly.

   Faint is the whole design. The one thing this picture must never be is ambiguous
   about which hole you are playing, and the fade — plus no flag and no tee markers on
   anything but the hole in hand — is what keeps the answer instant. The opacity is on
   the group rather than the shapes so overlapping outlines within one neighbour don't
   stack into something darker than the hole itself. */
.hp-other-ground {
  opacity: 0.34;
}

/* Course hazards that reach into the frame but sit outside the playing corridor.

   Held back, but deliberately less far back than a neighbouring hole's ground: a
   bunker is a bunker wherever the club files it, and the reason it's quieter here is
   only that it isn't on your line. A hazard *inside* the corridor isn't in this group
   at all — it's painted with the hole's own ground, at full strength, which is why a
   lake between the 5th and the 6th now reads as live on both instead of belonging to
   whichever one someone traced it onto.

   On the group for the same reason as .hp-other-ground: hazards overlap each other
   constantly, and per-element opacity would stack a cluster of bunkers into a blot. */
.hp-far {
  opacity: 0.62;
}

/* Small, quiet, and only found when looked for — enough to turn "some hole" into
   "the 7th" without pulling the eye off the flag. */
.hp-other-num {
  font-size: 7px;
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: central;
  fill: var(--course-pin);
  paint-order: stroke;
  stroke: var(--course-rough);
  stroke-width: 2;
  opacity: 0.55;
}

/* The flag marks the center of the green — the point every yardage in the strip
   above is measured to. Worth a real flag rather than a dot: it's the one thing
   in the picture that names what the numbers mean. */
.hp-pin line {
  stroke: var(--course-pin);
  stroke-width: 2;
}

.hp-pin path {
  fill: var(--course-pin-flag);
  stroke: none;
}

.hp-pin circle {
  fill: var(--course-pin);
  stroke: none;
}

.hp-accuracy {
  fill: var(--course-me-halo);
  stroke: none;
}

.hp-me-dot {
  fill: var(--course-me);
  stroke: #fff;
  stroke-width: 2;
}

/* ==========================================================================
   Course-map builder (/builder)

   Only ever loaded by that page — it is a separate bundle from the app, and no
   app screen carries a `bx-` class. Prefixed rather than split into its own
   stylesheet so the design tokens above are shared rather than duplicated: the
   builder should look like the same product, just in a full-bleed tool layout.
   ========================================================================== */

.builder-body {
  margin: 0;
  height: 100dvh;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
}

#builder {
  height: 100%;
}

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

.bx-error {
  color: var(--danger);
}

/* --- key gate ------------------------------------------------------------ */

.bx-gate {
  max-width: 26rem;
  margin: 0 auto;
  padding: 4rem 1.25rem;
}

.bx-gate form {
  display: flex;
  gap: 0.5rem;
  margin-top: 1rem;
}

.bx-gate input {
  flex: 1;
}

/* --- course picker ------------------------------------------------------- */

/* The page itself never scrolls.

   This is the single biggest thing wrong with the old shell: with the whole editor in
   one scrolling column, reaching a tool meant scrolling past the map, and the map moved
   while you did it. Chrome is fixed, and anything that needs more room scrolls inside
   its own pane. The picker is the one screen that is genuinely a list, so it keeps its
   scroll. */
.bx-shell {
  height: 100%;
  overflow: hidden;
}

.bx-picker {
  height: 100%;
  overflow-y: auto;
  padding: 2rem 1.25rem 4rem;
}

.bx-picker > * {
  max-width: 46rem;
  margin-inline: auto;
}

.bx-picker-head h1 {
  margin-bottom: 0.25rem;
}

.bx-group {
  margin-top: 2rem;
}

.bx-group h2 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  margin-bottom: 0.5rem;
}

.bx-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.7rem 0.85rem;
  margin-bottom: 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  text-align: left;
  color: inherit;
  font: inherit;
}

.bx-row-tap {
  cursor: pointer;
}

.bx-row-tap:hover:not(:disabled) {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* An orphaned map is the one state here that needs acting on, so it is the one
   state that gets colour. */
.bx-row-warn {
  border-color: var(--warn);
  background: var(--warn-soft);
}

.bx-select {
  max-width: 14rem;
}

.bx-status-line {
  max-width: 46rem;
  margin: 1rem auto 0;
  padding: 0.6rem 0.85rem;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--text);
}

/* --- editor shell -------------------------------------------------------- */

.bx-editor {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.bx-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  flex: 0 0 auto;
}

.bx-title {
  font-size: 0.95rem;
}

.bx-spacer {
  flex: 1;
}

.bx-status {
  font-size: 0.85rem;
  color: var(--muted);
  max-width: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bx-body {
  display: flex;
  flex: 1;
  min-height: 0;
}

/* --- hole rail ----------------------------------------------------------- */

.bx-rail {
  flex: 0 0 3.25rem;
  overflow-y: auto;
  border-right: 1px solid var(--line);
  background: var(--surface);
}

.bx-hole {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
  padding: 0.55rem 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.bx-hole.is-on {
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 600;
}

.bx-hole-n {
  min-width: 1.2rem;
  text-align: right;
}

/* Three states, because "started" and "finished" are genuinely different when a
   course is traced over several sittings: no center, some green points, done. */
.bx-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--line-strong);
}

.bx-dot.is-part {
  background: var(--warn);
}

.bx-dot.is-done {
  background: var(--accent);
}

/* --- map ----------------------------------------------------------------- */

.bx-map-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}

.bx-map {
  position: absolute;
  inset: 0;
  background: var(--surface-3);
}

/* --- the three views ------------------------------------------------------ */

.bx-views {
  display: flex;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}

/* Reads as a refinement of the view rather than a second view switcher: same control,
   quieter, and close enough to the three buttons to be read as belonging to them. */
.bx-course-shows {
  margin-left: -0.35rem;
  background: transparent;
  border-style: dashed;
}

.bx-view {
  padding: 0.3rem 0.6rem;
  border: 0;
  border-radius: calc(var(--r-sm) - 2px);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.bx-view.is-on {
  background: var(--accent);
  color: var(--accent-ink);
}

/* A preview lies over the satellite map, which stays mounted underneath. Opaque, so
   what's behind it can't bleed through and confuse a judgement about the drawing.
   Above .bx-map-overlay's 500 and the map controls' 600, and it takes pointer events
   back — the overlay it lives in has them off so pans reach Leaflet. */
/* Leaflet's own zoom buttons and attribution live inside .bx-map and carry a z-index
   of their own, so they punch straight through a panel laid over the top. The map is
   still there and still holding its view — it just has nothing to say while a preview
   is up. */
.bx-map-wrap.is-covered .leaflet-control-container {
  display: none;
}

.bx-preview {
  position: absolute;
  inset: 0;
  z-index: 700;
  overflow-y: auto;
  padding: var(--sp-3);
  background: var(--bg);
  pointer-events: auto;
}

.bx-preview-hole {
  max-width: 30rem;
  margin: 0 auto;
}

/* The card the strip will actually sit on, so it is judged against the right
   background rather than against the builder's chrome. */
.bx-preview-card .hole-meta {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.bx-preview-course {
  max-width: 56rem;
  margin: 0 auto;
}

.bx-preview-empty {
  max-width: 26rem;
  margin: 15vh auto 0;
  text-align: center;
}

/* --- the position simulator ----------------------------------------------- */

.bx-sim h3 {
  margin: 1rem 0 0.4rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.bx-sim h3:first-child {
  margin-top: 0;
}

.bx-sim-row {
  display: block;
  margin-bottom: 0.6rem;
  font-size: 0.8rem;
}

.bx-sim-row input[type="range"] {
  display: block;
  width: 100%;
  margin-top: 0.25rem;
  accent-color: var(--accent);
}

/* The one thing on the distance strip that must never be missed. */
.gd-sim {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.68rem;
}

/* Sits above Leaflet's panes but lets pans and zooms through; only the drawn
   handles inside it take pointer events. */
.bx-map-overlay {
  position: absolute;
  inset: 0;
  z-index: 500;
  pointer-events: none;
}

.bx-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.bx-layer-toggle {
  position: absolute;
  right: 0.5rem;
  top: 0.5rem;
  z-index: 600;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem;
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

.bx-layer-toggle button {
  padding: 0.25rem 0.5rem;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 0.8rem;
  cursor: pointer;
}

.bx-layer-toggle button.is-on {
  background: var(--accent);
  color: var(--accent-ink);
}

.bx-zoom {
  padding: 0 0.35rem;
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* --- overlay marks ------------------------------------------------------- */

.bx-point circle {
  pointer-events: auto;
  cursor: grab;
  stroke: #fff;
  stroke-width: 2;
}

.bx-point text {
  font-size: 11px;
  font-weight: 600;
  fill: #fff;
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.65);
  stroke-width: 3;
  pointer-events: none;
}

/* Front/center/back are told apart by colour rather than by reading the label,
   because they are placed in quick succession and sit yards apart. */
.bx-point-greenFront circle { fill: #ffd400; }
.bx-point-greenCenter circle { fill: #ff3b30; }
.bx-point-greenBack circle { fill: #0a84ff; }

.bx-shape path {
  pointer-events: auto;
  cursor: pointer;
  stroke-width: 2;
  fill-opacity: 0.28;
}

.bx-shape.is-selected path {
  stroke-width: 3;
  stroke-dasharray: 6 3;
}

.bx-shape.is-draft path {
  fill: none;
  stroke-dasharray: 5 4;
}

.bx-shape-tee path { fill: #b07cff; stroke: #b07cff; }
.bx-shape-fairway path { fill: #4cd964; stroke: #4cd964; }
.bx-shape-green path { fill: #00e0a4; stroke: #00e0a4; }
.bx-shape-apron path { fill: #a8e05f; stroke: #a8e05f; }
/* Course-level hazards, drawn on every hole. Fainter than the hole's own outlines by
   design: they are context for the hole being traced, not the subject of it — but a
   selected one comes up to full strength, because then it *is* what you are working on. */
.bx-hazard path { opacity: 0.62; }
.bx-hazard.is-selected path { opacity: 1; }

.bx-shape-bunker path { fill: #ffd79a; stroke: #f0b860; }
.bx-shape-water path { fill: #3aa0ff; stroke: #3aa0ff; }
.bx-shape-path path { fill: none; stroke: #d0d0d0; stroke-dasharray: 4 4; }

/* A candidate routing's hole lines, while the pointer is over it in the build panel.

   At a resort the hole numbers repeat across courses and the ways carry no club name, so
   seeing which chain runs where on the imagery is the only way to tell four courses
   apart. Deliberately unlike every shape colour on the map — this is not part of the
   course, it is a question being asked — and inert, since there is nothing here to
   grab. */
.bx-routing-lines {
  pointer-events: none;
}

.bx-routing-lines path {
  fill: none;
  stroke: #7ad9ff;
  stroke-width: 2.5;
  stroke-dasharray: 8 5;
  stroke-linecap: round;
  opacity: 0.9;
}


.bx-vertex {
  pointer-events: auto;
  cursor: grab;
  fill: #fff;
  stroke: #111;
  stroke-width: 1.5;
}

.bx-vertex.is-draft {
  pointer-events: none;
}

/* --- box selection -------------------------------------------------------- */

/* Nothing drawn may take a pointer while the band tool is armed. The band starts on
   whatever ground the pointer lands on, and that ground is covered by exactly the
   shapes it exists to select — so leaving them clickable meant a drag begun over a
   cart path never began at all. */
.bx-overlay.is-selecting * {
  pointer-events: none;
}

/* The rubber band. Solid dragged left-to-right (takes only what it encloses), dashed
   right-to-left (takes anything it touches) — the same visual language CAD has used
   for the same two behaviours since before any of this existed. */
.bx-band {
  fill: rgba(120, 190, 255, 0.12);
  stroke: #78beff;
  stroke-width: 1.5;
  pointer-events: none;
}

.bx-band.is-crossing {
  fill: rgba(120, 255, 170, 0.12);
  stroke: #6dffab;
  stroke-dasharray: 6 4;
}

/* What the band has caught, marked before anything is deleted. */
.bx-shape.is-picked path,
.bx-cartpath.is-picked path {
  stroke: #ff6b5e;
  stroke-width: 4;
}

.bx-landmark.is-picked circle {
  stroke: #ff6b5e;
  stroke-width: 3;
}

/* Stacked for the same reason the import rows are: the inspector is 17rem and
   "4 cart paths, 2 outlines and 1 landmark selected" beside two buttons is three
   cramped lines of wrapped text. */
.bx-selected {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin: 0.5rem 0;
  padding: 0.45rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-size: 0.82rem;
}

.bx-selected span {
  flex: 1 1 100%;
}

.bx-selected button {
  margin-left: auto;
}

.bx-selected button + button {
  margin-left: 0;
}

/* --- imported candidates -------------------------------------------------- */

/* An offer, not a shape. Unfilled and dashed so it can never be mistaken for something
   already on the map — it is drawn from a source that puts ninety-one greens on an
   eighteen-hole course, and the whole point is that a person looks at it against the
   imagery before it becomes real. */

/* Hovering a row in the inspector lights up what it means on the map, and the reverse.
   A row saying "23 bunkers" lights all twenty-three; finding out which shapes a list
   entry stands for is otherwise guesswork against the imagery. */

/* The ghost handle halfway along each segment: grab it and it becomes a real vertex.
   Hollow and half-lit so an outline's actual points still read as the outline — these
   are an offer, not part of the shape. */
.bx-midpoint {
  pointer-events: auto;
  cursor: copy;
  fill: rgba(255, 255, 255, 0.35);
  stroke: #111;
  stroke-width: 1.25;
  stroke-dasharray: 2 2;
  opacity: 0.7;
}

.bx-midpoint:hover {
  fill: #fff;
  opacity: 1;
  stroke-dasharray: none;
}

/* --- inspector ----------------------------------------------------------- */

.bx-inspector {
  flex: 0 0 17rem;
  overflow-y: auto;
  padding: 0.85rem;
  border-left: 1px solid var(--line);
  background: var(--surface);
}

.bx-inspector h2 {
  margin: 0 0 0.6rem;
  font-size: 1.05rem;
}

.bx-inspector h3 {
  margin: 1rem 0 0.4rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.bx-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-bottom: 0.6rem;
}

.bx-tool {
  padding: 0.35rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: inherit;
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.bx-tool.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.bx-tick {
  margin-left: 0.25rem;
  opacity: 0.75;
}

/* The shortcut printed on its own tool. There are a dozen of these and nobody learns
   them from a help panel they'd have to go and open. */

/* Stacked, because the inspector is 17rem and a row is a name, a reason, a distance and
   two buttons. Side by side that wraps "Tee box" onto two lines and the reason onto
   three; the name earns the full width and the buttons take the line below. */


/* On the drawings themselves, so the credit travels with the picture. */
.hp-credit,
.cp-credit {
  font-size: 5px;
  text-anchor: end;
  fill: var(--course-pin);
  stroke: none;
  opacity: 0.45;
}

.bx-key {
  margin-left: 0.35rem;
  padding: 0 0.25rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  font: inherit;
  font-size: 0.72em;
  line-height: 1.4;
  text-transform: uppercase;
  opacity: 0.7;
}

.bx-tool.is-on .bx-key {
  border-color: currentColor;
  background: transparent;
  opacity: 0.85;
}

.bx-hint {
  margin: 0.4rem 0 0.8rem;
  font-size: 0.8rem;
  color: var(--muted);
}

.bx-points,
.bx-shapes {
  list-style: none;
  margin: 0;
  padding: 0;
}

.bx-points li,
.bx-shapes li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
}

.bx-shapes li.is-selected {
  background: var(--accent-soft);
}

.bx-shape-pick {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
}

.bx-swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: 0 0 auto;
}

.bx-swatch-tee { background: #b07cff; }
.bx-swatch-fairway { background: #4cd964; }
.bx-swatch-green { background: #00e0a4; }
.bx-swatch-apron { background: #a8e05f; }
.bx-swatch-bunker { background: #ffd79a; }
.bx-swatch-water { background: #3aa0ff; }
.bx-swatch-path { background: #d0d0d0; }

.bx-issues {
  margin-top: 0.8rem;
  padding: 0.5rem 0.6rem;
  border-radius: var(--r-sm);
  background: var(--warn-soft);
  color: var(--text);
  font-size: 0.8rem;
}

.bx-issues p {
  margin: 0 0 0.3rem;
}

/* The one-time stop over several greens on one hole. Sits directly above the list it
   is holding back, and keeps its buttons on one row so "draw a line" and "take them
   all anyway" read as the two answers to the same question. */
.bx-greens-held .bx-tools {
  margin-top: 0.4rem;
}

.bx-coverage {
  margin-top: 1rem;
  font-size: 0.8rem;
}

/* --- phone light-edit ---------------------------------------------------- */

/* Deliberately not a smaller version of the desktop tool. The rail becomes a
   horizontal strip, the inspector drops under the map, and the drawing tools are
   gone entirely — the phone exists to fix a green point while standing on it. */
.bx-editor.is-phone .bx-body {
  flex-direction: column;
}

.bx-editor.is-phone .bx-rail {
  flex: 0 0 auto;
  display: flex;
  overflow-x: auto;
  border-right: 0;
  border-bottom: 1px solid var(--line);
}

.bx-editor.is-phone .bx-hole {
  flex: 0 0 auto;
  width: 2.75rem;
  border-bottom: 0;
  border-right: 1px solid var(--line);
}

.bx-editor.is-phone .bx-map-wrap {
  flex: 1 1 55%;
  min-height: 45vh;
}

.bx-editor.is-phone .bx-inspector {
  flex: 0 0 auto;
  max-height: 40vh;
  border-left: 0;
  border-top: 1px solid var(--line);
}

.bx-editor.is-phone .bx-point circle {
  r: 11;
}

/* Jump-to-place. Sits top-left over the map, clear of Leaflet's zoom control. */
.bx-jump {
  position: absolute;
  left: 3.25rem;
  top: 0.5rem;
  z-index: 600;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  max-width: min(24rem, calc(100% - 9rem));
}

.bx-jump input {
  min-width: 0;
  flex: 1;
  padding: 0.3rem 0.5rem;
  font-size: 0.82rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-1);
}

.bx-jump.is-bad input {
  border-color: var(--danger);
}

.bx-jump button {
  padding: 0.3rem 0.6rem;
  font-size: 0.82rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  box-shadow: var(--shadow-1);
}

.bx-jump-error {
  flex: 1 0 100%;
  padding: 0.35rem 0.5rem;
  border-radius: var(--r-sm);
  background: var(--danger-soft);
  color: var(--text);
  font-size: 0.75rem;
}

/* Address-lookup matches, under the jump box. A menu only when there is a choice to
   make — one unambiguous answer moves the map without ever appearing here. Scrolls
   rather than growing, because a vague query ("golf club") comes back with five long
   names and this floats over the map being traced. */
.bx-jump-results {
  flex: 1 0 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 11rem;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

.bx-jump-results li + li {
  border-top: 1px solid var(--line);
}

.bx-jump-results button {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  font-size: 0.78rem;
  line-height: 1.3;
  padding: 0.4rem 0.5rem;
}

.bx-jump-results small {
  color: var(--muted);
}

@media (hover: hover) {
  .bx-jump-results button:hover {
    background: var(--surface-2);
  }
}

/* Fill front/center/back from the traced green outline. Full width and above the
   point list it populates, because on a hole with an outline it is the action
   worth taking and placing three points by hand is the fallback. */
.bx-derive {
  width: 100%;
  margin-bottom: 0.5rem;
  font-size: 0.82rem;
}

/* The offers row when there is no live yardage: seeing the hole and turning
   distances on. Stacked rather than side by side because both are full-width
   ghost buttons and a 50/50 split makes each read as half a control. */
.gd-offers,
.gd-plan-only {
  margin-top: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Inside the offers row the buttons already carry the spacing. */
.gd-offers .gd-enable,
.gd-offers .gd-notice,
.gd-plan-only .gd-enable {
  margin-top: 0;
}

/* Shown instead of a yardage when the fix is nowhere near this hole. Quiet, because
   it is the expected state the night before a round rather than a problem. */
.gd-away {
  margin: 0;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
}

/* Capture-date picker, only present on the dated archive layer. */
.bx-wayback {
  min-height: 0;
  padding: 0.2rem 0.3rem;
  font-size: 0.78rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* No yardage to show: an em dash in the same slot the number lives in, so the strip
   keeps its shape whether or not you're standing on the hole. Muted and lighter than
   a real reading, because it is the absence of one. */
.gd-yards.is-unknown {
  color: var(--muted);
  font-weight: 600;
  letter-spacing: 0;
}

/* Sits where the accuracy chip goes, saying why there is no number. */
.gd-reason {
  color: var(--muted);
  text-transform: none;
}

/* Inside the strip the enable button carries its own spacing. */
.green-distance .gd-enable {
  margin-top: var(--sp-2);
}

/* Cart paths in the builder. Drawn as a line with a real width rather than an
   outline, because that is what one is — see CoursePath. */
.bx-cartpath path {
  pointer-events: auto;
  cursor: pointer;
  fill: none;
  stroke: #e0e0e0;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Opaque on purpose: two stretches that meet or run alongside each other would
     otherwise double up and show a bright seam exactly at the join. */
  opacity: 1;
}

/* The run a click will grow. Obvious at a glance, because the whole mode is built
   around picking a path back up rather than starting a new one by accident. */
.bx-cartpath.is-active path {
  stroke: #ffd400;
  stroke-width: 4;
  opacity: 1;
}

.bx-cartpath.is-selected path {
  stroke-dasharray: 6 3;
}

/* Endpoints are fatter: they are the grab targets for continuing a path. */
.bx-vertex.is-end {
  fill: #ffd400;
  stroke: #111;
}

.bx-width {
  width: 3.4rem;
  min-height: 0;
  padding: 0.15rem 0.3rem;
  font-size: 0.78rem;
  text-align: right;
}

/* Cart path on the hole diagram, at its real width. */
.hp-cartpath {
  fill: none;
  stroke: var(--course-path);
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* While the cart path tool is armed the drawn path must not eat clicks: the stroke
   sits exactly where you click to carry a path on, and swallowing that click is what
   stopped a path ever being continued. */
.bx-cartpath.is-passthrough path {
  pointer-events: none;
}

/* --- landmarks ----------------------------------------------------------- */

/* Builder markers. A filled disc with its name beside it: at tracing zoom there is
   room for the word, and a word beats a glyph nobody has to decode. */
.bx-landmark circle {
  pointer-events: auto;
  cursor: grab;
  fill: #ffffff;
  stroke: #111;
  stroke-width: 2;
}

.bx-landmark text {
  font-size: 11px;
  font-weight: 600;
  fill: #fff;
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.65);
  stroke-width: 3;
  pointer-events: none;
}

.bx-landmark.is-selected circle {
  stroke: #ffd400;
  stroke-width: 3;
}

/* Colour-coded so a course peppered with markers still reads at a glance. */
.bx-lm-restroom circle { fill: #7cc4ec; }
.bx-lm-food circle { fill: #ffb84d; }
.bx-lm-water circle { fill: #6fe3d0; }
.bx-lm-clubhouse circle { fill: #ff8a7a; }
.bx-lm-shop circle { fill: #c99bff; }
.bx-lm-practice circle { fill: #9ee37d; }
.bx-lm-parking circle { fill: #b9c4cc; }
.bx-lm-other circle { fill: #ffffff; }

.bx-tool-lm {
  font-size: 0.76rem;
}

.bx-lm-label {
  flex: 1;
  min-width: 0;
  min-height: 0;
  padding: 0.15rem 0.35rem;
  font-size: 0.78rem;
}

.bx-swatch-lm {
  border-radius: 50%;
}

.bx-swatch-lm-restroom { background: #7cc4ec; }
.bx-swatch-lm-food { background: #ffb84d; }
.bx-swatch-lm-water { background: #6fe3d0; }
.bx-swatch-lm-clubhouse { background: #ff8a7a; }
.bx-swatch-lm-shop { background: #c99bff; }
.bx-swatch-lm-practice { background: #9ee37d; }
.bx-swatch-lm-parking { background: #b9c4cc; }
.bx-swatch-lm-other { background: #ffffff; }

/* On the hole diagram. Sized in plan units, which are stable across holes because
   the viewBox always normalises to the same long side. */
.hp-landmark circle {
  fill: var(--course-landmark);
  stroke: var(--course-landmark-ink);
  stroke-width: 1.5;
}

.hp-landmark text {
  font-size: 7px;
  font-weight: 700;
  text-anchor: middle;
  fill: var(--course-landmark-ink);
  paint-order: stroke;
  stroke: var(--course-landmark);
  stroke-width: 2.5;
}

/* --- the whole course ---------------------------------------------------- */

/* One property in one frame, north up. Its own class prefix rather than the hole
   diagram's, because the two are at wildly different scales: an outline drawn to the
   hole plan's weight would be nine yards of ink here and every shape would vanish
   under its own edge. Same palette throughout, so the two read as one family. */
.course-plan {
  display: block;
  width: 100%;
  /* Height follows the viewBox rather than being fixed. A property's shape is its own —
     a links course strung along a coastline is wide and short, a parkland loop is nearly
     square — and a fixed height would letterbox one of them into bands of empty rough.
     Capped so a narrow course can't run off the bottom of the screen. */
  height: auto;
  max-height: 70vh;
  margin-top: var(--sp-3);
  border-radius: var(--r-md);
  /* The ground, for the same reason as the hole diagram: once `max-height` binds, the
     drawing letterboxes inside this element and a rect in viewBox units cannot reach
     the bars. */
  background: var(--course-rough);
  overflow: hidden;
}

/* Ground outlines: a constant hairline however far in you zoom, so an edge stays an
   edge rather than becoming a band. Cart paths are excluded from both rules — their
   width is a real number of yards and has to scale with everything else. */
.course-plan path:not(.cp-cartpath):not(.cp-cartpath-casing),
.course-plan circle,
.course-plan ellipse {
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.course-plan path:not(.cp-cartpath):not(.cp-cartpath-casing) {
  stroke-width: 1;
}

.cp-fairway { fill: var(--course-fairway); stroke: var(--course-fairway-line); }
.cp-apron { fill: var(--course-apron); stroke: var(--course-apron-line); }
.cp-water { fill: var(--course-water); stroke: var(--course-water-line); }
.cp-tee { fill: var(--course-tee); stroke: var(--course-tee-line); }
.cp-green { fill: var(--course-green); stroke: var(--course-green-line); }
.cp-bunker { fill: var(--course-bunker); stroke: var(--course-bunker-line); }

.cp-path,
.cp-cartpath,
.cp-cartpath-casing {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cp-cartpath {
  stroke: var(--course-cartpath);
}

/* The darker line under the path. What makes a three-yard track read as a track at a
   scale where it is barely a pixel wide. */
.cp-cartpath-casing {
  stroke: var(--course-cartpath-line);
}

/* Legacy per-hole paths, from maps traced before cart paths moved to the course. */
.cp-path {
  stroke: var(--course-path);
}

.cp-landmark circle {
  fill: var(--course-landmark);
  stroke: var(--course-landmark-ink);
  stroke-width: 1;
}

.cp-landmark text {
  font-weight: 700;
  text-anchor: middle;
  fill: var(--course-landmark-ink);
  paint-order: stroke;
  stroke: var(--course-landmark);
}

/* The hole numbers. The whole reason for the picture: without them it is a green
   blob and with them it is "the 12th is over there". */
/* Size and halo come from attributes, not from here: they are divided by the zoom so
   a label holds its size on screen while the ground grows under it, and a CSS
   declaration would override that and make them balloon. */
.cp-hole text {
  font-weight: 800;
  text-anchor: middle;
  dominant-baseline: central;
  fill: var(--course-pin);
  paint-order: stroke;
  stroke: var(--course-rough);
}

.cp-block {
  margin-top: var(--sp-3);
}

.cp-frame {
  position: relative;
}

.course-plan {
  cursor: grab;
  /* Vertical page scroll keeps working while the plan sits at rest, so a drag over it
     doesn't trap the page. Once it's zoomed the gestures are the plan's — see
     `.is-zoomed` — and the reset button gives them back. */
  touch-action: pan-y;
}

.course-plan.is-zoomed {
  touch-action: none;
}

.course-plan:active {
  cursor: grabbing;
}

.cp-zoom {
  position: absolute;
  right: var(--sp-2);
  bottom: var(--sp-2);
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

.cp-zoom .icon-btn,
.cp-reset {
  min-height: 0;
  padding: 0.3rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}

.cp-reset {
  font-size: 0.76rem;
}

.cp-zoom .icon-btn:disabled {
  opacity: 0.4;
}

/* Matches the scorecard's plan toggle: same chevron, same rotation on open. */
.cp-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
}

.cp-toggle svg {
  transition: transform 0.15s ease;
}

.cp-toggle.is-open svg {
  transform: rotate(180deg);
}

/* The outer band of the import: close enough to show, not close enough to sweep up.
   Set apart from the confident list rather than badged inside it, because the question
   changes between the two — above, "is this right?"; below, "is this even mine?". */


/* ---------------------------------------------------------- building from OSM

   The panel that replaced accepting candidate shapes one at a time. Its job is to be
   readable in one glance before a decision and one glance after it, so everything here
   is counts and short rows rather than prose. */

.bx-build .primary {
  width: 100%;
  margin-top: 0.5rem;
}

.bx-routings {
  margin-top: 0.75rem;
}

/* One candidate routing. Hovering draws its hole lines on the imagery — which is the
   only way to tell Barefoot's four courses apart, since the ways carry no club name. */
.bx-routing {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  margin-bottom: 0.3rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* `:not(:disabled)` is not optional here — see the warning on `button:hover` above.
   That rule is (0,2,1), so a plain `.bx-routing.is-on` at (0,2,0) loses to it and the
   selected row turned solid accent green the moment the pointer was over it, taking its
   muted subtitle down to unreadable with it. Found in a browser, not in a test. */
.bx-routing.is-on:not(:disabled) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
}

@media (hover: hover) {
  .bx-routing:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent) 8%, var(--surface-2));
  }

  .bx-routing.is-on:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent) 20%, var(--surface-2));
  }
}

/* The hole count, big enough to scan a list by. */
.bx-routing-n {
  flex: 0 0 2rem;
  font-size: 1.15rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.bx-report {
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
}

.bx-report-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0 0 0.5rem;
}

/* Shapes that matched no hole. Never silently dropped: a discarded green leaves a hole
   with no distance and nothing on screen saying so. */
.bx-unclaimed {
  margin: 0.5rem 0;
  font-size: 0.88rem;
  color: var(--muted);
}

.bx-warnings {
  margin: 0.5rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.88rem;
  color: var(--muted);
}

.bx-warnings li {
  margin-bottom: 0.25rem;
}

.bx-warnings .link {
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.bx-error {
  margin: 0.6rem 0 0;
  font-size: 0.9rem;
  color: var(--warn, #d9534f);
}

/* ODbL. The geometry is redistributed in a public branch and rendered on every phone,
   so the credit has to be somewhere a person can see it. */
.bx-credit {
  margin: 0.75rem 0 0;
  font-size: 0.78rem;
  color: var(--muted);
}

/* Naming a hole OpenStreetMap never routed.

   Offered only where it can do something — this hole has no green and the last build
   left shapes unplaced — because a button that cannot act is worse than no button. */
.bx-name-hole {
  margin: 0.75rem 0;
  padding: 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}

.bx-name-hole button {
  width: 100%;
}

.bx-name-hole .bx-hint {
  margin: 0.5rem 0 0;
}

/* ------------------------------------------------------------------- the ribbon

   Tools in a fixed band across the top rather than stacked down the inspector. The
   inspector is 17rem wide, so a column of tool buttons there was three or four rows of
   scrolling before anything else could be reached — and the thing you wanted to reach
   was usually the drawing you had just made. */
/* ------------------------------------------------------------------- the ribbon

   One shallow row of icon buttons, grouped and captioned — the shape a CAD package
   uses, and for the same reason: the map is the work, and a toolbar that eats a
   quarter of the screen is a toolbar competing with it.

   It replaced a band of text buttons ("Set points from green", "Tee box", and eight
   landmark names spelled out) that stood three times this tall and still ran off the
   end, pushing Select past the edge of the screen. */
.bx-ribbon {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  flex: 0 0 auto;
  flex-wrap: wrap;
  row-gap: 0.2rem;
  padding: 0.25rem 0.6rem 0.15rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  /* Wraps to a second row on a narrow window rather than scrolling sideways. It has to
     not be a scroll container: `overflow-x: auto` forces overflow-y to auto too, which
     clipped the landmarks panel off at the ribbon's bottom edge and made it unusable. */
  overflow: visible;
}

/* One group of tools, captioned by what they act on.

   `bx-rib-group` rather than `bx-group`: that name belongs to the course picker's
   sections, which carry a 2rem top margin — and the ribbon wearing the same class was
   quietly taking that margin too, which is a third of what made it so tall.

   The caption is doing real work: the The caption is doing real work: the
   Hole / Course split is where a shape is *stored*, and naming it at the moment of
   drawing is cheaper than expecting anyone to remember it. */
.bx-rib-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  padding-right: 0.4rem;
  border-right: 1px solid var(--line);
}

.bx-rib-group:last-child {
  border-right: 0;
  padding-right: 0;
}

.bx-rib-group-name {
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
  white-space: nowrap;
  line-height: 1;
  padding-bottom: 0.1rem;
}

.bx-ribbon .bx-tools {
  display: flex;
  flex-wrap: nowrap;
  /* Centred, not stretched: without this the buttons grow to whatever height the
     tallest group is and the row of squares turns into a row of pills. */
  align-items: center;
  gap: 0.2rem;
  margin: 0;
  min-height: 3.3rem;
}

/* Two button sizes, which is the split every CAD ribbon makes and the reason one works:
   the commands you reach for constantly get a large button with its name under the
   glyph, and the repairs and once-a-course tools get a small icon beside them. All
   icons and no names was legible but anonymous — every tool the same size says every
   tool matters the same amount, which is the opposite of what a ribbon is for. */
.bx-icon-tool {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  /* Out of the global 48px touch target. That rule is right for a phone held on a tee
     box and wrong here: the builder is a desktop tool driven with a trackpad, and 48px
     squares would make this band half again as tall as the hole rail beside it. */
  min-height: 0;
  padding: 0;
  flex: 0 0 auto;
}

/* Primary: glyph over name. Wide enough for "Cart path" without wrapping, which is
   what sets the width of the whole row. */
.bx-icon-tool.is-large {
  flex-direction: column;
  gap: 0.1rem;
  width: auto;
  min-width: 3.4rem;
  height: 3.3rem;
  padding: 0.3rem 0.4rem 0.25rem;
}

.bx-tool-name {
  font-size: 0.62rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.bx-icon-tool.is-small {
  width: 1.75rem;
  height: 1.75rem;
  align-self: center;
}

.bx-icon-tool .bx-icon {
  display: block;
}

/* Whether this green point has been placed. On the icon rather than after a label,
   because there is no label any more. */
.bx-icon-tool .bx-tick {
  position: absolute;
  right: 1px;
  top: 0;
  font-size: 0.62rem;
  line-height: 1;
  color: var(--accent);
}

/* ------------------------------------------------------------- landmark flyout

   Eight tools, each placed at most once per course. Along the band they took more room
   than every drawing tool put together; behind one button they cost a click that nobody
   makes twice in a session. */
.bx-flyout-anchor {
  position: relative;
}

.bx-flyout {
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0;
  z-index: 700;
  display: grid;
  grid-template-columns: repeat(2, minmax(8rem, 1fr));
  gap: 0.1rem;
  padding: 0.2rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: var(--shadow-2, 0 6px 20px rgb(0 0 0 / 0.18));
}

.bx-flyout-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.4rem;
  min-height: 0;
  padding: 0.3rem 0.45rem;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text);
  font-size: 0.78rem;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.bx-flyout-item.is-on {
  background: var(--accent-soft, var(--surface-2));
  color: var(--accent-ink, var(--text));
}

@media (hover: hover) {
  .bx-flyout-item:hover {
    background: var(--surface-2);
  }
}

/* ---------------------------------------------------------------- status bar

   State that changes as you work, on one line that never moves. All of this used to be
   a paragraph somewhere in the inspector, which is how a panel of tools turned into a
   panel of prose. */
.bx-statusbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex: 0 0 auto;
  padding: 0.3rem 0.75rem;
  border-top: 1px solid var(--line);
  background: var(--surface);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
  overflow-x: auto;
}

/* Shapes the last build could not place. Worth colour: a green nobody notices is
   missing is a hole with no distance and nothing on screen saying so. */
.bx-status-warn {
  color: var(--warn, #d9534f);
  font-weight: 600;
}

/* The course-search diagnostic's report (Admin). Raw JSON, read once when something is
   wrong, so it only has to stay legible and stop long values widening the page. */
.diagnostic-report {
  overflow-x: auto;
  margin: 0.75rem 0 0;
  padding: 0.75rem;
  border-radius: var(--radius, 8px);
  background: var(--surface-2, rgba(127, 127, 127, 0.12));
  font-size: 0.8rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
