/*
 * Traman.
 *
 * Palette is lifted from the DrillScript prototype so diagrams sit natively in the app
 * rather than looking embedded from elsewhere: the court green and its line colour, the
 * prototype's blue as the interactive accent, ball yellow reserved for favourites and
 * draft state, team red/blue used only inside diagrams.
 *
 * Neutrals are biased slightly green rather than pure grey, so they read as chosen. The
 * typeface is the system UI stack on purpose — this is an app, and it should look like it
 * belongs on the device rendering it.
 *
 * One file. Propshaft does not bundle, so splitting means a link tag per file, which is
 * not worth it at this size.
 */

:root {
  color-scheme: light dark;

  --bg: #f7f8f5;
  --surface: #ffffff;
  --surface-2: #f1f3ee;
  --line: #e3e7e0;
  --line-strong: #c8d0c4;
  --text: #16191b;
  --muted: #5e6a65;
  --accent: #1e66c9;
  --accent-soft: #e6eefb;
  --accent-ink: #ffffff;
  --court: #2f6b45;
  --court-line: #8fbfa2;
  --danger: #b3392f;

  /* One mono stack, one rule about where it goes. It is for code -- the DrillScript editor,
     the guide's samples, parser errors -- plus the agenda's dates, which are a fixed-width
     column of aligned glyphs and the one piece of UI where it earns its place. Everything
     else that shows a number uses the sans face with tabular figures: a slashed zero at
     heading size reads as a code font in a place that is not code. */
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --danger-soft: #fae8e7;
  --ok: #2f7d4f;
  --ok-soft: #e3f2e8;
  --ball: #a8760d;
  --ball-soft: #fdf3dd;

  /* Diagram palette. Named separately from the UI tokens because these belong to the
     court, not the chrome — the renderer reads them straight out of CSS so a diagram
     follows the theme without being told which one is active. */
  --ds-court: #2f6b45;
  --ds-court-line: #8fbfa2;
  --ds-team-a: #b93b34;
  --ds-team-b: #2f6fc0;
  --ds-ball: #a8760d;
  --ds-attack: #c2453f;
  --ds-block: #6b7770;
  --ds-move: #5f6b66;
  --ds-equipment: #7a6a2e;
  /* A coach is on nobody's side, so they get the one colour that is not a team's. */
  --ds-coach: #6b6f76;
  /* A named place, held back below everything on the floor: a spot is where things happen,
     not a thing anybody can pick up. */
  --ds-spot: #8a8f86;
  /* The 3D view's backdrop and playing surface. 2D draws no backdrop and its court is
     --ds-court; the solid view has a horizon and a lit floor of its own, and both used to
     be hardcoded dark, which left a light page's red figures and their labels muddy on a
     night-time stage. Plain hex on purpose: three.js reads these straight into THREE.Color,
     and a color-mix() here would come out black. */
  --ds-stage: #e9efe9;
  --ds-floor: #2f6b45;

  --radius: 10px;
  --radius-sm: 7px;
  --shadow: 0 1px 2px rgb(20 24 26 / 5%), 0 10px 28px -16px rgb(20 24 26 / 18%);

  /* The focus ring is one token, read by every :focus-visible rule, so the app has one ring
     rather than the two it had (an outline on links, a box-shadow on inputs). */
  --ring: color-mix(in srgb, var(--accent) 45%, transparent);
  --ring-width: 3px;

  --step: 0.5rem;
  --measure: 68ch;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e1216;
    --surface: #161b21;
    --surface-2: #1d232b;
    --line: #2a323b;
    --line-strong: #3c4650;
    --text: #e7ebe9;
    --muted: #8c9791;
    --accent: #4d9fff;
    --accent-soft: #17293f;
    --accent-ink: #08121f;
    --court: #1f4a30;
    --court-line: #4f7f63;
    --danger: #ff6b6b;
    --danger-soft: #331a1a;
    --ok: #4fbf7f;
    --ok-soft: #16291d;
    --ball: #ffd166;
    --ball-soft: #33290f;
    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 12px 32px -18px rgb(0 0 0 / 70%);
    --ring: color-mix(in srgb, var(--accent) 55%, transparent);

    --ds-court: #1a3a26;
    --ds-court-line: #5a8a6e;
    --ds-team-a: #ff6b6b;
    --ds-team-b: #6ba8ff;
    --ds-ball: #ffd166;
    --ds-attack: #ff5252;
    --ds-block: #cfd8dc;
    --ds-move: #8a93a3;
    --ds-equipment: #d8c47a;
    --ds-coach: #b6bcc6;
    --ds-spot: #9aa39b;
    --ds-stage: #0a1419;
    --ds-floor: #2e7a48;
  }
}

*,
*::before,
*::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* ---------- typography ---------- */

h1, h2, h3 {
  margin: 0 0 calc(var(--step) * 1.5);
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h1 { font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 660; }
h2 { font-size: 1.2rem; font-weight: 640; margin-top: calc(var(--step) * 5); }
h3 { font-size: 1rem; font-weight: 620; }

p { margin: 0 0 var(--step); max-width: var(--measure); }

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

small {
  display: inline-block;
  font-size: 0.8125rem;
  color: var(--muted);
  max-width: var(--measure);
  line-height: 1.5;
}

:focus-visible {
  outline: var(--ring-width) solid var(--ring);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---------- app bar ---------- */

.appbar {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 10;
}

.appbar-inner {
  max-width: 1080px;
  margin: 0 auto;
  padding: calc(var(--step) * 1.5) calc(var(--step) * 3);
  display: flex;
  align-items: center;
  gap: calc(var(--step) * 2);
  flex-wrap: wrap;
}

/* On a laptop the identity wrapper dissolves: brand, chip and account menu become items of
   .appbar-inner in their own right, which is what lets the menu's auto margin push it to the
   far edge, after the nav. Kept as a flex box only on a phone (below), where it is the one
   row that stays pinned and its own width is what gives that margin room. Inside a
   shrink-to-fit wrapper the margin had nothing to absorb, and the name sat between the club
   chip and "Trainings" reading as a nav item. */
.appbar-identity { display: contents; }

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--step);
  font-weight: 660;
  font-size: 0.95rem;
  letter-spacing: -0.015em;
  color: var(--text);
  text-decoration: none;
}

/* A miniature court: green, with a net line down the middle. */
.brand-mark {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--court);
  border: 1px solid var(--court-line);
  position: relative;
  flex: none;
}

.brand-mark::after {
  content: "";
  position: absolute;
  inset: 2px auto 2px 50%;
  width: 1px;
  background: var(--court-line);
}

/* A label, never a switcher — the session was bound to this club at sign-in. */
.club-chip {
  font-size: 0.8125rem;
  color: var(--muted);
  padding: 2px calc(var(--step) * 1.25);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  white-space: nowrap;
}

.nav {
  display: flex;
  gap: calc(var(--step) * 0.5);
  flex-wrap: wrap;
}

.nav a {
  font-size: 0.875rem;
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.5);
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-decoration: none;
}

.nav a:hover { background: var(--surface-2); color: var(--text); }

.nav a[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 560;
}

.account-menu { position: relative; margin-left: auto; order: 9; }

.account-menu summary {
  list-style: none;
  cursor: pointer;
  font-size: 0.8125rem;
  color: var(--muted);
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.25);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
.account-menu summary::-webkit-details-marker { display: none; }
.account-menu summary:hover, .account-menu[open] summary { background: var(--surface-2); color: var(--text); }

.account-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: calc(var(--step) * 0.75);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  z-index: 20;
}
.account-menu-list a, .account-menu-list .btn-ghost {
  display: block;
  width: 100%;
  text-align: left;
  padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-size: 0.875rem;
}
.account-menu-list a:hover, .account-menu-list .btn-ghost:hover { background: var(--surface-2); }
/* button_to wraps Sign out in a form; block so its row's hover background is as wide as
   Connected apps' rather than as wide as the word. */
.account-menu-list form { display: block; }

/* The public switcher. Same disclosure as .account-menu, and deliberately the same shape: one
   pattern for "a summary you tap and a short list underneath". */
.language-menu {
  position: relative;
}

.language-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--step) * 0.75);
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.25);
  font-size: 0.875rem;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
}

.language-menu > summary::-webkit-details-marker { display: none; }

.language-menu-list {
  position: absolute;
  right: 0;
  z-index: 10;
  display: grid;
  gap: calc(var(--step) * 0.5);
  min-width: 100%;
  margin-top: calc(var(--step) * 0.5);
  padding: calc(var(--step) * 0.75);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}

.language-menu-list a {
  display: flex;
  align-items: center;
  gap: calc(var(--step) * 0.75);
  padding: calc(var(--step) * 0.5) calc(var(--step) * 0.75);
  font-size: 0.875rem;
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--radius-sm);
}

.language-menu-list a:hover { background: var(--surface-2); }

/* ---------- marketing chrome ---------- */

.marketing { background: var(--surface); border-bottom: 1px solid var(--line); }
.marketing-inner {
  max-width: 1080px; margin: 0 auto; padding: calc(var(--step) * 1.5) calc(var(--step) * 3);
  display: flex; align-items: center; gap: calc(var(--step) * 2); flex-wrap: wrap;
}
.marketing-nav { display: flex; gap: calc(var(--step) * 0.5); }
.marketing-nav a {
  font-size: 0.875rem; padding: calc(var(--step) * 0.75) calc(var(--step) * 1.5);
  border-radius: var(--radius-sm); color: var(--muted); text-decoration: none;
}
.marketing-nav a:hover { background: var(--surface-2); color: var(--text); }
.marketing-end { margin-left: auto; display: flex; gap: var(--step); align-items: center; }
.site-footer {
  max-width: 1080px; margin: calc(var(--step) * 6) auto 0; padding: calc(var(--step) * 3);
  border-top: 1px solid var(--line); display: flex; gap: calc(var(--step) * 2); flex-wrap: wrap;
  font-size: 0.8125rem; color: var(--muted);
}
.site-footer .brand { font-weight: 660; color: var(--text); margin-right: auto; }
.site-footer a { color: var(--muted); text-decoration: none; }
.site-footer a:hover { color: var(--text); }
@media (max-width: 640px) {
  .marketing-inner { padding-inline: calc(var(--step) * 2); gap: var(--step); }
  .marketing-nav { order: 3; width: 100%; }
  .marketing-nav a { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ---------- pricing ---------- */

/* Shared by the pricing page and (later) the billing page, via the one "home/pricing"
   partial -- one set of rules for both. */
.pricing { width: 100%; }
.tiers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: calc(var(--step) * 2);
  margin-top: calc(var(--step) * 3);
}
@media (max-width: 720px) {
  .tiers { grid-template-columns: 1fr; }
}
.tier {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: calc(var(--step) * 3);
  display: flex;
  flex-direction: column;
  gap: var(--step);
}
.tier.is-current { border-color: var(--accent); border-width: 2px; }
.tier.is-soon { border-style: dashed; opacity: 0.7; }
.tier-name { font-weight: 660; }
.tier-price {
  font-size: 1.75rem;
  font-weight: 660;
  font-variant-numeric: tabular-nums;
}
.tier-price small { display: block; font-size: 0.8125rem; font-weight: 500; color: var(--muted); }
.tier ul { margin: 0; padding-left: 1.1em; color: var(--muted); font-size: 0.9375rem; }
.tier ul li + li { margin-top: calc(var(--step) * 0.5); }
.pricing-note { margin-top: calc(var(--step) * 3); color: var(--muted); font-size: 0.9375rem; }
.is-disabled { opacity: 0.45; pointer-events: none; }

/* The billing page's headline: the club's tier and one line on what it means. */
.billing-tier {
  padding: calc(var(--step) * 2);
  margin-bottom: calc(var(--step) * 2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 1.125rem;
}

.billing-tier small { display: block; font-size: 0.9375rem; color: var(--muted); margin-top: calc(var(--step) * 0.5); }

/* ---------- landing ---------- */

/* Six feature rows, two screen plays, the language, the price and one way in. The rows
   alternate sides so the eye zigzags down the page rather than reading a table; every screen
   is a real screenshot with the same frame (border, radius, shadow) so the page looks like one
   product and not a collage. Vertical rhythm is the one thing that is not per-section: a
   landing page's sections are far apart, and the same gap between each pair is what keeps a
   long scroll from reading as a stack of unrelated panels. */
.landing-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: calc(var(--step) * 6);
  align-items: center;
  padding-block: calc(var(--step) * 6) calc(var(--step) * 4);
}
.landing-hero h1 { font-size: clamp(1.75rem, 4.5vw, 2.5rem); margin-bottom: calc(var(--step) * 2); }
.hero-text .lede { font-size: 1.0625rem; max-width: 52ch; }
.hero-text .actions { margin-top: calc(var(--step) * 3); margin-bottom: calc(var(--step) * 2); }
/* Framed like the screenshots below it, so the live diagram and the still screens read as
   one set. No bar and no buttons: the hero plays on its own and the playground is one click
   away for anyone who wants the controls. */
.hero-court {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: calc(var(--step) * 1.5);
}

.feature {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: calc(var(--step) * 6);
  align-items: center;
  margin-top: calc(var(--step) * 10);
}
/* Swapped by order, not by direction: `direction: rtl` on the row would also flip the text. */
.feature.is-flipped .feature-shot { order: 2; }
.feature-shot { min-width: 0; }
.feature-shot img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
/* A phone screenshot at the width of a laptop one is a wall; at a handset's width it is a
   phone, and the empty space either side says so. */
.feature.is-phone .feature-shot img { max-width: 280px; margin: 0 auto; border-radius: 18px; }
.feature-text { min-width: 0; }
.feature-text h2 { margin-top: 0; font-size: 1.375rem; }
.feature-text p { color: var(--muted); }
.feature-text a { font-weight: 560; text-decoration: none; }
.feature-text a:hover { text-decoration: underline; }

/* The AI feature's "screenshot": the screen is whichever chat client the coach uses, so it
   is a transcript, framed like the others. Reused as the "Ask Claude" frame of a screen play. */
.chat {
  display: flex;
  flex-direction: column;
  gap: var(--step);
  padding: calc(var(--step) * 2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: 0.9375rem;
}
.msg {
  margin: 0;
  max-width: 88%;
  padding: calc(var(--step) * 1.25) calc(var(--step) * 1.75);
  border-radius: var(--radius-sm);
  line-height: 1.45;
}
.msg.me { background: var(--accent-soft); align-self: flex-end; }
.msg.ai { background: var(--surface-2); border: 1px solid var(--line); align-self: flex-start; }

.landing-h2 { margin-top: calc(var(--step) * 12); font-size: 1.5rem; }

/* A screen play: framed screens stacked on one stage, each fading in for its turn while the
   step beside it lights up. Pure CSS -- the frames share one cycle and are staggered by their
   index, so nothing here needs a controller. Each frame holds for 3s; the cycle is that times
   the count, which the partial writes as --frames. The keyframes have to know the count too --
   a frame's share is 100/N percent and a keyframe cannot read a custom property -- so there is
   one set per count in use, picked by data-frames. A new count needs a new set, and the frames
   simply stay dark for it until one is written. */
.screen-play {
  /* Defaults for the two values the partial writes inline, so a frame with no index is still
     well-formed rather than a dropped declaration. */
  --frames: 1;
  --i: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 14rem;
  gap: calc(var(--step) * 3);
  margin-top: calc(var(--step) * 3);
  padding: calc(var(--step) * 2);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.play-stage { position: relative; aspect-ratio: 16 / 10; min-width: 0; }
.play-frame {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  opacity: 0;
  animation-duration: calc(var(--frames) * 3s);
  animation-delay: calc(var(--i) * 3s);
  animation-iteration-count: infinite;
}
.screen-play[data-frames="5"] .play-frame { animation-name: play-frames-5; }
.screen-play[data-frames="4"] .play-frame { animation-name: play-frames-4; }
/* Visible at rest, so the stage shows a screen before the animation has started and still
   shows one when reduced motion has stopped it. The running animation overrides this. */
.play-frame:first-child { opacity: 1; }
.play-frame img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.play-frame .chat { max-width: 26rem; }
@keyframes play-frames-5 {
  0% { opacity: 0; }
  4%, 20% { opacity: 1; }
  24%, 100% { opacity: 0; }
}
@keyframes play-frames-4 {
  0% { opacity: 0; }
  5%, 25% { opacity: 1; }
  30%, 100% { opacity: 0; }
}
.play-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 0.75);
  counter-reset: play-step;
  align-self: center;
}
.play-title { font-weight: 660; margin-bottom: var(--step); }
.play-step {
  display: flex;
  gap: var(--step);
  align-items: baseline;
  color: var(--muted);
  font-size: 0.9375rem;
  animation-duration: calc(var(--frames) * 3s);
  animation-delay: calc(var(--i) * 3s);
  animation-iteration-count: infinite;
}
.play-step::before {
  counter-increment: play-step;
  content: counter(play-step);
  flex: none;
  width: 1.25rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--accent);
}
.play-title + .play-step { color: var(--text); }
.screen-play[data-frames="5"] .play-step { animation-name: play-steps-5; }
.screen-play[data-frames="4"] .play-step { animation-name: play-steps-4; }
@keyframes play-steps-5 {
  0% { color: var(--muted); }
  4%, 20% { color: var(--text); }
  24%, 100% { color: var(--muted); }
}
@keyframes play-steps-4 {
  0% { color: var(--muted); }
  5%, 25% { color: var(--text); }
  30%, 100% { color: var(--muted); }
}

.landing-drillscript {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: calc(var(--step) * 6);
  align-items: center;
  margin-top: calc(var(--step) * 12);
}
/* A <pre>, not the guide's textarea: nothing here is meant to be edited, so no resize grip. */
.landing-drillscript .code-sample { resize: none; box-shadow: var(--shadow); }
.landing-drillscript h2 { margin-top: 0; font-size: 1.375rem; }
.landing-drillscript p { color: var(--muted); }

.landing-close {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--step) * 3);
  flex-wrap: wrap;
  margin-top: calc(var(--step) * 12);
  padding: calc(var(--step) * 3) calc(var(--step) * 4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.landing-close p { margin: 0; color: var(--muted); }

@media (max-width: 720px) {
  .landing-hero,
  .feature,
  .landing-drillscript { grid-template-columns: 1fr; gap: calc(var(--step) * 3); }
  .landing-hero { padding-block: calc(var(--step) * 2); }
  .feature { margin-top: calc(var(--step) * 7); }
  /* The screen stays on top on a phone, whichever side it took on a laptop. */
  .feature.is-flipped .feature-shot { order: 0; }
  .screen-play { grid-template-columns: 1fr; }
  .play-steps { align-self: start; }
  .landing-h2, .landing-drillscript, .landing-close { margin-top: calc(var(--step) * 8); }
}

/* ---------- page ---------- */

.page {
  max-width: 1080px;
  margin: 0 auto;
  padding: calc(var(--step) * 4) calc(var(--step) * 3) calc(var(--step) * 10);
}


/* The diagram editor. Two panes side by side need more than the reading width a page of
   prose is capped at — and unlike prose, code does not get harder to read as it gets wider.
   Still centred: it inherits margin: 0 auto from .page, so wide screens keep real gutters
   rather than going full-bleed. */
.page-wide { max-width: 1440px; }

/* ---------- flash and errors ---------- */

.flash {
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
  border-radius: var(--radius);
  margin-bottom: calc(var(--step) * 3);
  border: 1px solid;
  font-size: 0.9375rem;
}

.flash-notice { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); }
.flash-alert { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

/* The verification banner: a warm note, not an error -- the app works, one action waits. */
.verification-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--step) * 2);
  flex-wrap: wrap;
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
  margin-bottom: calc(var(--step) * 3);
  border: 1px solid var(--ball);
  border-radius: var(--radius);
  background: var(--ball-soft);
  font-size: 0.9375rem;
}

.verification-banner p { margin: 0; }

.errors {
  margin: 0 0 calc(var(--step) * 3);
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2) calc(var(--step) * 1.5) calc(var(--step) * 4);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  color: var(--danger);
  font-size: 0.9375rem;
}

/* ---------- forms ---------- */

form { max-width: 40rem; }

label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 560;
  color: var(--muted);
  margin-bottom: calc(var(--step) * 0.5);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="datetime-local"],
input[type="url"],
select,
textarea {
  display: block;
  width: 100%;
  max-width: none;
  padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5);
  margin-bottom: calc(var(--step) * 2);
  font: inherit;
  font-size: 0.9375rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
}

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

input::placeholder { color: var(--muted); opacity: 0.75; }

/* Explanatory text sits tight under the control it explains. */
input + small, select + small { margin: calc(var(--step) * -1.5) 0 calc(var(--step) * 2); }

.field { margin-bottom: calc(var(--step) * 2); }

/* ---------- buttons ---------- */

input[type="submit"],
button,
.btn {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--step) * 0.75);
  padding: calc(var(--step) * 1.25) calc(var(--step) * 2);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 560;
  color: var(--accent-ink);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

input[type="submit"]:hover,
button:hover,
.btn:hover { filter: brightness(1.08); }

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

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--muted);
  padding-inline: calc(var(--step) * 1.25);
}

.btn-ghost:hover { background: var(--surface-2); color: var(--text); filter: none; }

/* A control that is unavailable has to look it. The beat arrows are disabled while a diagram
   plays -- the sequence runs to the end and gives the controls back -- and without this they
   would sit there looking pressable while doing nothing, which reads as broken rather than as
   busy. */
button:disabled,
input[type="submit"]:disabled,
.btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

button:disabled:hover,
.btn:disabled:hover { filter: none; background: inherit; }

.btn-danger {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--danger);
}

/* Quieter than .btn-danger, which still carries a border and so still reads as a sibling of
   the buttons beside it. An irreversible action should not look like the control next to it:
   colour marks it, nothing else does, and .actions-danger puts it somewhere else entirely.
   Used where the destructive control shares a screen with one a coach taps in a hurry. */
.btn-danger-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--danger);
  padding-inline: calc(var(--step) * 1.25);
}

.btn-danger-ghost:hover { background: var(--danger-soft); filter: none; }

/* Destructive controls get their own row below a divider. Same row means same weight however
   the button is styled, and on the session screen the delete was sitting directly beside the
   control that starts the evening. */
.actions-danger {
  display: flex;
  align-items: center;
  gap: var(--step);
  flex-wrap: wrap;
  margin-top: calc(var(--step) * 2);
  padding-top: calc(var(--step) * 1.5);
  border-top: 1px solid var(--line);
}

/* button_to renders a form; keep it inline with neighbouring controls. */
form.button_to { display: inline-block; margin: 0; max-width: none; }

.actions {
  display: flex;
  align-items: center;
  gap: var(--step);
  flex-wrap: wrap;
  margin-top: calc(var(--step) * 2);
}

/* ---------- tables ---------- */

.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: calc(var(--step) * 3);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

th {
  text-align: left;
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

td {
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  vertical-align: middle;
}

tbody tr:last-child td { border-bottom: none; }
td:first-child { color: var(--text); font-weight: 560; }
td:empty::after { content: "—"; color: var(--line-strong); }

/* ---------- pills and empty states ---------- */

.pill {
  display: inline-block;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px calc(var(--step) * 1.25);
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--muted);
}

.pill-owner { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.pill-you { background: transparent; border-style: dashed; }

.empty {
  padding: calc(var(--step) * 5);
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  margin-bottom: calc(var(--step) * 3);
}

/* An aside inside a form — not an empty state. .empty was being borrowed for this and then
   corrected with inline styles at every call site. */
.note {
  padding: calc(var(--step) * 2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--muted);
  font-size: 0.8125rem;
  margin-bottom: calc(var(--step) * 2);
}

/* Rows and lines that sit directly under something and need no margin of their own. */
.tight { margin: 0; }

/* A plan's notes, on its own screen. pre-line so the line breaks a coach typed survive. */
.notes {
  margin: 0 0 calc(var(--step) * 3);
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.55;
  white-space: pre-line;
}

/* ---------- signed-out screens ---------- */

.centered {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: calc(var(--step) * 3);
}

.centered .content { width: 100%; max-width: 460px; }

/* A page of prose for a visitor. The centred column is sized for a sign-in card; a
   reading page widens it to the measure the rest of the app uses for text. */
main.centered.reading .content { max-width: var(--measure); }

/* The guide is page-wide for a member (an editor pane needs more than reading width) and
   the same class reaches a visitor now that the page is public -- .centered's 460px would
   otherwise clip every example's two-column layout down to the sign-in card's width. */
main.centered.page-wide .content { max-width: 1440px; }

/* The pricing page's three-column tier grid needs more than reading's prose measure but
   nowhere near the guide's page-wide editor -- its own modifier rather than overloading
   either. */
main.centered.pricing-page .content { max-width: 1080px; }

/* The landing page is a long scroll, not a card. .centered's grid would still hold it -- a
   row grows to its content -- but "place-items: center" is a statement about a sign-in box,
   and the page it would be centring is five screens tall. Plain flow, the width the marketing
   header and footer already use, and the rules for the page itself live under "landing". */
main.centered.landing { display: block; }
main.centered.landing .content { max-width: 1080px; margin-inline: auto; }

.card {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  padding: calc(var(--step) * 4);
  box-shadow: var(--shadow);
}

.card form { max-width: none; }
.card input, .card select { max-width: none; }

.auth-links {
  margin-top: calc(var(--step) * 3);
  font-size: 0.875rem;
  color: var(--muted);
}

/* Two modes on one form. A radio group drawn as a segmented control; the checked half is
   filled, the other reads as a tab. Native radios so the choice posts without JavaScript. */
.segment {
  display: flex; margin: 0 0 calc(var(--step) * 2); padding: 0; border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm); overflow: hidden;
}
.segment label { flex: 1; position: relative; }
.segment input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.segment span {
  display: block; text-align: center; padding: calc(var(--step) * 1.1) var(--step);
  font-size: 0.875rem; font-weight: 560; color: var(--muted); cursor: pointer;
}
.segment input:checked + span { background: var(--accent-soft); color: var(--accent); }
.segment input:focus-visible + span { outline: var(--ring-width) solid var(--ring); outline-offset: -3px; }
.auth-mode-hint { font-size: 0.875rem; color: var(--muted); margin: var(--step) 0 0; }

/* ---------- drill library ---------- */

.page-head {
  display: flex;
  align-items: center;
  gap: calc(var(--step) * 2);
  flex-wrap: wrap;
  margin-bottom: calc(var(--step) * 3);
}

.page-head h1 { margin: 0; }
.page-head .btn { margin-left: auto; }

.lede { color: var(--muted); font-size: 1rem; margin-bottom: calc(var(--step) * 2); }

.library {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  gap: calc(var(--step) * 4);
  align-items: start;
  max-width: none;
}

.facets {
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 3);
  position: sticky;
  top: calc(var(--step) * 10);
}

/* The disclosure's summary is shown only where collapsing is worth offering. On a laptop the
   column has room and the control would be noise. */
.facets > summary {
  display: none;
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  padding: calc(var(--step) * 1.5) 0;
  min-height: 44px;
  align-items: center;
}

@media (max-width: 900px) {
  .facets > summary { display: flex; }
}

.facet { display: flex; flex-direction: column; gap: calc(var(--step) * 0.75); }
.facet input, .facet select { margin-bottom: 0; max-width: none; }

.facet-label {
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.check {
  display: flex;
  align-items: center;
  gap: calc(var(--step) * 1.25);
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--muted);
  margin: 0;
  cursor: pointer;
}

.check input { width: auto; margin: 0; }

.check-row {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--step) * 1.25) calc(var(--step) * 3);
}

/* The leave screen's draft list: one row per draft, its fate chosen beside it. */
.draft-settlement {
  list-style: none;
  margin: 0 0 calc(var(--step) * 2);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.draft-settlement li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--step) * 2);
  flex-wrap: wrap;
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
}

.draft-settlement li + li { border-top: 1px solid var(--line); }
.draft-settlement small { display: block; color: var(--muted); }

/* The account page's list of clubs still to leave. */
.clubs-to-leave {
  list-style: none;
  margin: 0 0 calc(var(--step) * 2);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.clubs-to-leave li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--step) * 2);
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
}

.clubs-to-leave li + li { border-top: 1px solid var(--line); }

.library-main { min-width: 0; display: flex; flex-direction: column; gap: calc(var(--step) * 2); }
.result-count { font-size: 0.8125rem; color: var(--muted); margin: 0; }

.drill-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: calc(var(--step) * 2);
}

.drill-card {
  display: flex;
  flex-direction: column;
  gap: var(--step);
  padding: calc(var(--step) * 2.5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.drill-card-head { display: flex; align-items: flex-start; gap: var(--step); }
.drill-card h3 { margin: 0; font-size: 0.9375rem; line-height: 1.3; }
.drill-card h3 a { color: var(--text); text-decoration: none; }
.drill-card h3 a:hover { color: var(--accent); }
.star { margin-left: auto; color: var(--ball); font-size: 0.875rem; }

.drill-objective {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.45;
}

.drill-meta {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--step) * 1.5);
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.tags { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 0.75); align-items: center; }

/* Three tagging layers, three visual weights: shared, private, state. */
.pill-skill { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.pill-mine { background: transparent; border-style: dashed; }
.pill-draft { background: var(--ball-soft); border-color: transparent; color: var(--ball); }
.pill-published { background: transparent; border-color: currentColor; color: var(--ok); }

/* ---------- drill detail ---------- */

/* One card, main content and a sidebar sharing an edge — the sidebar is attached rather
   than floating, which is what makes it read as part of the drill instead of beside it. */
.detail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 250px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.detail-main {
  min-width: 0;
  padding: calc(var(--step) * 3) calc(var(--step) * 3.5);
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 3);
}

.detail-side {
  border-left: 1px solid var(--line);
  background: var(--surface-2);
  padding: calc(var(--step) * 3);
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 3);
}

/* Title and actions on one line: what the drill is, and what you can do with it, without
   scrolling between them. */
.detail-head {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--step) * 2);
  flex-wrap: wrap;
}

.detail-title {
  flex: 1 1 240px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--step);
}

.detail-actions { display: flex; gap: var(--step); flex-wrap: wrap; }

.detail h1 { margin: 0; font-size: 1.35rem; font-weight: 650; letter-spacing: -0.02em; }
.detail .objective { margin: 0; font-size: 0.875rem; color: var(--muted); }

/* Sections are labelled, not headed. A drill is a set of short fields to scan, and headings
   at heading size would shout over the content they name. */
.detail .field { display: flex; flex-direction: column; gap: calc(var(--step) * 0.6); margin: 0; }
.detail .field p { margin: 0; font-size: 0.875rem; color: var(--muted); line-height: 1.55; }
.detail .field ul { margin: 0; padding-left: calc(var(--step) * 2.25); font-size: 0.875rem; color: var(--muted); }
.detail .field li { margin-bottom: 3px; }

.label {
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.sidefield { display: flex; flex-direction: column; gap: calc(var(--step) * 0.75); }
.sidefield-link { font-size: 0.8125rem; }

.kv { display: flex; justify-content: space-between; gap: var(--step); font-size: 0.8125rem; }
.kv span:first-child { color: var(--muted); }
.kv span:last-child { font-weight: 560; font-variant-numeric: tabular-nums; text-align: right; }

.drill-images { display: flex; flex-wrap: wrap; gap: var(--step); }
.drill-images img { max-width: 100%; border-radius: var(--radius-sm); border: 1px solid var(--line); }

/* Two short fields side by side on a laptop -- minutes and players, name and code -- and
   stacked on a phone. The 420px single column was a phone layout with margins. */
.field-row { display: grid; grid-template-columns: 1fr; gap: 0 calc(var(--step) * 2); }
@media (min-width: 720px) { .field-row { grid-template-columns: 1fr 1fr; } }
.field-row input, .field-row select { max-width: none; }

.tag-form { display: flex; gap: var(--step); align-items: center; max-width: none; }
.tag-form input { margin: 0; max-width: none; }

/* An inline remove control inside a pill. */
.tag-remove {
  padding: 0 0 0 calc(var(--step) * 0.5);
  font-size: 0.6875rem;
  min-height: 0;
  color: inherit;
  opacity: 0.65;
}

.tag-remove:hover { background: transparent; opacity: 1; }

/* ---------- diagrams ---------- */

/* The court is portrait and to scale — 9m across, 18m long, each half square — so it is
   taller than any pane is wide. Sized by height, therefore, and centred by the SVG's own
   aspect handling.

   No background colour here on purpose: the green belongs to the court rectangle the
   renderer draws. Painting the whole canvas green would make the empty space either side
   look like part of the court, which is the very thing the wrong ratio was doing before. */
/* The court is landscape and framed per beat, so width fills the pane and the height
   follows from the viewBox — no fixed height, which would only letterbox it. */
.court-canvas {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
}

.viewer {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: calc(var(--step) * 3);
  background: var(--surface);
}

.variation { margin-top: calc(var(--step) * 2); }
.variation h3 { margin: 0 0 calc(var(--step) * 0.5); }

.viewer-bar, .editor-bar {
  display: flex;
  align-items: center;
  gap: var(--step);
  padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}

/* The controls keep their size; the caption gives way. Otherwise a long beat title
   squeezes the 2D/3D control until it is clipped. */
.viewer-bar > *, .editor-bar > * { flex: none; }
.editor-bar .label { white-space: nowrap; }

/* The editor's diagram switcher: main, each variation, rotation, and the way to add one. */
.diagram-tabs {
  display: flex;
  gap: calc(var(--step) * 0.5);
  flex-wrap: wrap;
  margin-bottom: calc(var(--step) * 1.5);
}

.diagram-tabs a {
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.5);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9375rem;
}

.diagram-tabs a[aria-current="page"] { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.diagram-tabs a.diagram-tab-add { border-style: dashed; }

/* The editor's "written under an older release" line. */
.language-note { margin-bottom: calc(var(--step) * 1.5); }

.spacer { flex: 1; }

/* Segmented 2D/3D control. Both options stay visible with the active one filled, so it
   reads as a state rather than as a button that does something unknown. */
.seg {
  display: inline-flex;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.seg button {
  padding: calc(var(--step) * 0.5) calc(var(--step) * 1.5);
  font-size: 0.75rem;
  font-weight: 560;
  border: none;
  border-radius: 0;
  background: var(--surface);
  color: var(--muted);
}

.seg button.on { background: var(--accent); color: var(--accent-ink); }
.seg button:hover:not(.on) { background: var(--surface-2); color: var(--text); filter: none; }

/* The beat caption sits at the far end of the bar, away from the controls — and it is the
   one thing allowed to shrink, so a long beat title never clips the 2D/3D control. */
.beatbar {
  flex: 1 1 auto;
  min-width: 0;
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.viewer .court-canvas { border-radius: 0; }

/* A toggle carries its state in aria-pressed and nothing else, so the look and the thing a
   screen reader announces cannot drift apart. Off is dimmed rather than hidden: the control
   still has to say what pressing it would do. */
.toggle[aria-pressed="false"] { opacity: 0.45; }

/* Filling the screen gives the space to the diagram, not to the frame around it. The bar is
   inside the element that goes fullscreen, so play, pause and step still work while the court
   is filling a wall -- which is the whole point of showing it to a team. */
.viewer:fullscreen {
  display: flex;
  flex-direction: column;
  margin: 0;
  border: 0;
  border-radius: 0;
}

/* The SVG letterboxes itself through its viewBox; the 3D canvas cannot, which is why it is
   given a real box here and told to resize on fullscreenchange. */
.viewer:fullscreen .court-canvas,
.viewer:fullscreen .court-stage {
  flex: 1;
  min-height: 0;
  height: auto;
}


/* Works on SVG as well as HTML, unlike the [hidden] attribute. */
.is-hidden { display: none !important; }

/* The 3D canvas needs a real height — WebGL cannot infer one the way an SVG viewBox can. */
.court-stage {
  height: min(60vh, 480px);
  background: #0a1419;
}

.court-stage canvas { display: block; width: 100%; height: 100%; }

/* Authoring is a desktop activity — nobody writes a DSL on a phone — so the split pane
   collapses rather than trying to be usable at 380px. */
/* One card split by a single border, not two cards with a gap — source and preview are two
   views of the same thing, and a gutter between them makes them read as separate tools. */
/* Forms cap at reading width, which is right for a column of fields and wrong for this: the
   editor *is* a form, so 68ch was squeezing the whole split pane into a third of the page. */
.diagram-editor { max-width: none; }
.diagram-editor .actions { justify-content: center; }

/* Bounded by the viewport so the Save button below stays on screen. Both panes scroll
   inside it rather than the page growing under them. */
.editor {
  display: grid;
  grid-template-columns: minmax(0, 46fr) minmax(0, 54fr);
  height: clamp(380px, calc(100vh - 290px), 780px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: calc(var(--step) * 3);
}

/* min-height: 0 on both. A grid item defaults to min-height: auto, which refuses to shrink
   below its content — so without this the panes grow to fit the code, the editor overflows
   its own height, and the scrolling escapes to the page instead of staying in the pane. */
.editor-src {
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--line);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.editor-prev {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--surface-2);
}

/* The bars keep their size; only the code and the preview scroll. */
.editor-src > .editor-bar,
.editor-prev > .editor-bar { flex: none; }

/* The error strip is bounded, and scrolls once it is full.
   It sits in a flex column whose only flexible child is .code-surface, and that child carries
   `min-height: 0` so the pane scrolls internally instead of pushing the page around -- which
   also makes it the one child that absorbs any shortfall. An uncapped strip therefore does not
   overflow, it evicts: fourteen refusals took the whole pane and left the code surface at
   literally zero, so the coach was shown a list of things to fix and no way to fix them.
   Proportional rather than a pixel count, because the pane itself is a clamp(). */
.editor-src > [data-drillscript-editor-target="errors"] {
  flex: none;
  max-height: 40%;
  overflow-y: auto;
}

.prev-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: calc(var(--step) * 2);
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 1.5);
}

.prev-body p { margin: 0; }

/* An amber strip under the source, not a red box: a diagram with a bad line still saves and
   still draws, so this is a warning about one line rather than a failure of the whole. */
.errline {
  padding: calc(var(--step) * 1.25) calc(var(--step) * 2);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  background: var(--ball-soft);
  color: var(--ball);
  border-top: 1px solid var(--line);
}

/* The preview summary reads as a sentence fragment — "red · 6 players" — so it keeps its
   case, unlike the pills that label a thing. */
.prev-body .pill { text-transform: none; font-size: 0.75rem; letter-spacing: 0; }

.pill-warn { background: transparent; border-color: var(--danger); color: var(--danger); }

/* ---------- the code editor ---------- */

/* A highlighted <pre> under a transparent <textarea>. The two only stay aligned if they
   render identically, so every metric that affects layout is declared once, here, and
   inherited by both. Changing any of it in one place only is how these drift apart. */
.code-layer {
  margin: 0;
  border: 0;
  padding: calc(var(--step) * 1.75) calc(var(--step) * 2);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  line-height: 1.7;
  letter-spacing: 0;
  tab-size: 2;

  /* Code does not wrap. A soft-wrapped line looks like two lines of DrillScript, and the
     line numbers in error messages refer to the real ones. */
  white-space: pre;
  overflow-wrap: normal;
  word-break: normal;
}

.code-surface {
  flex: 1;
  min-height: 0;
  display: flex;
  overflow: auto;
  background: var(--surface);
}

.code-area { position: relative; flex: 1; min-width: 0; }

.code-layer:first-child {
  /* The painted layer. It sets the size of the box; the textarea is laid over it. */
  color: var(--text);
  min-width: 100%;
  width: max-content;
}

.code-input {
  position: absolute;
  inset: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  resize: none;
  overflow: hidden;
  background: transparent;
  color: transparent;
  caret-color: var(--accent);
  border-radius: 0;
}

.code-input:focus { outline: none; box-shadow: none; }
.code-input::selection { background: var(--accent-soft); color: transparent; }

/* Line numbers. Sticky so they stay put when a long line scrolls the pane sideways. */
.code-gutter {
  position: sticky;
  left: 0;
  z-index: 1;
  flex: none;
  padding: calc(var(--step) * 1.75) calc(var(--step) * 1.25);
  background: var(--surface-2);
  border-right: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  line-height: 1.7;
  text-align: right;
  color: var(--line-strong);
  user-select: none;
}

.code-gutter .ln { font-variant-numeric: tabular-nums; }

/* The line an error is on, marked in the same amber as the message below. */
.code-gutter .ln.has-error {
  color: var(--ball);
  font-weight: 700;
}

/* ---------- autocomplete ---------- */

/* Positioned inside .code-area, which scrolls with the code — so the popup follows the
   caret without any scroll arithmetic. */
.completions {
  position: absolute;
  z-index: 5;
  margin: 0;
  padding: calc(var(--step) * 0.5);
  list-style: none;
  min-width: 200px;
  max-height: 260px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}

.completion {
  display: flex;
  align-items: baseline;
  gap: calc(var(--step) * 1.5);
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.25);
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
}

.completion-value {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text);
}

.completion-detail { margin-left: auto; font-size: 0.6875rem; color: var(--muted); }

.completion.is-selected { background: var(--accent); }
.completion.is-selected .completion-value,
.completion.is-selected .completion-detail { color: var(--accent-ink); }

/* ---------- syntax colours ---------- */

.tok-key { color: var(--accent); font-weight: 600; }
.tok-str { color: var(--ok); }
.tok-comment { color: var(--muted); font-style: italic; opacity: 0.8; }
.tok-red { color: var(--ds-team-a); }
.tok-blue { color: var(--ds-team-b); }
.tok-verb { color: var(--text); }
.tok-pos { color: var(--ball); }

.template-picker { margin: 0; max-width: 210px; font-size: 0.8125rem; padding: calc(var(--step) * 0.75) var(--step); }

.diagram-errors { margin: 0; border: none; border-top: 1px solid var(--line); border-radius: 0; }

/* ---------- playground ---------- */

.playground-head { margin-bottom: calc(var(--step) * 2); }
.playground-layout { display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: calc(var(--step) * 3); align-items: start; }
.playground-examples { display: flex; flex-direction: column; gap: 2px; position: sticky; top: calc(var(--step) * 2); }
.playground-examples h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0 0 var(--step); }
.playground-example {
  font-size: 0.875rem; padding: calc(var(--step) * 0.75) var(--step); border-radius: var(--radius-sm);
  color: var(--muted); text-decoration: none;
}
.playground-example:hover { background: var(--surface-2); color: var(--text); }
.playground-example.is-current { background: var(--accent-soft); color: var(--accent); font-weight: 560; }
.playground-editor { display: flex; flex-direction: column; gap: calc(var(--step) * 2); min-width: 0; }
.playground-save { display: flex; align-items: center; gap: calc(var(--step) * 1.5); max-width: none; }
.playground-save small { color: var(--muted); }
/* No main.playground rule: main.centered.page-wide .content (above, in "signed-out screens")
   already widens .content to 1440px, and .playground isn't scanned by the stylesheet guard --
   it arrives on <main> through content_for(:page_modifier), which the guard's regexes only
   ever see after ERB has been stripped from a static class="..." attribute. */
@media (max-width: 900px) {
  .playground-layout { grid-template-columns: 1fr; }
  .playground-examples { position: static; flex-direction: row; flex-wrap: wrap; }
}

/* ---------- the tutorial ---------- */

/* Below 900px this is just two stacked blocks -- the TOC above the sections, as it always
   was. From 900px up it becomes a sidebar, sticky so it stays reachable down a page long
   enough to need a contents list at all. */
.guide-layout { display: grid; grid-template-columns: 1fr; gap: calc(var(--step) * 2); }

.guide-toc {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--step) * 0.5);
  padding: calc(var(--step) * 2) 0;
  border-bottom: 1px solid var(--line);
  margin-bottom: calc(var(--step) * 2);
}

.guide-toc a {
  font-size: 0.8125rem;
  padding: calc(var(--step) * 0.75) calc(var(--step) * 1.5);
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-decoration: none;
  background: var(--surface-2);
}

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

@media (min-width: 900px) {
  .guide-layout { grid-template-columns: 12rem minmax(0, 1fr); gap: calc(var(--step) * 4); }

  .guide-toc {
    position: sticky;
    top: calc(var(--step) * 2);
    flex-direction: column;
    align-items: flex-start;
    border-bottom: none;
    padding: 0;
    align-self: start;
    /* A sticky element only scrolls with the page once its own column runs out, so a
       15-entry list taller than a short viewport (a laptop lid, a tablet in landscape)
       would leave its last few links below the fold with no way to reach them. */
    max-height: calc(100dvh - calc(var(--step) * 4));
    overflow-y: auto;
  }
}

/* Named only so the layout grid above has something to point at -- the sections inside
   carry their own rules. */
.guide-body { min-width: 0; }

/* Prose stays at a readable width even though the page is wide for the examples. */
.page-wide section > p,
.page-wide section > h2,
.page-wide section > h3 { max-width: var(--measure); }

.page-wide section { scroll-margin-top: calc(var(--step) * 2); }

.guide-example {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(260px, 28vw, 400px);
  gap: calc(var(--step) * 3);
  align-items: start;
  margin: calc(var(--step) * 2) 0 calc(var(--step) * 5);
}

/* A textarea, not a <pre>: the examples are meant to be edited. Styled to still read as a
   code block so the page doesn't look like a form. */
.code-sample {
  display: block;
  width: 100%;
  max-width: none;
  margin: 0;
  padding: calc(var(--step) * 2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--text);
  tab-size: 2;
  resize: vertical;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

.code-sample:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

.guide-source { display: flex; flex-direction: column; gap: var(--step); min-width: 0; }
.guide-hint { margin: 0; display: flex; align-items: center; gap: var(--step); flex-wrap: wrap; }
.guide-hint .btn-ghost { font-size: 0.75rem; padding: 2px calc(var(--step) * 1.25); }
.guide-source .diagram-errors { margin: 0; border-radius: var(--radius); border: 1px solid var(--danger); }

/* The way out of an example, for whichever visitor is reading it -- a member takes it to a
   drill of their own, a visitor to the playground. Same footing as Reset beside them. */
.guide-open, .guide-use { margin-left: auto; }

.guide-diagram {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}

/* The guide's diagram column is a third the width of the drill page's, and its bar gained the
   2D/3D control. Every child of .viewer-bar is flex: none and .guide-diagram clips its
   overflow, so the play controls were pushed past the edge and disappeared outright rather
   than wrapping or scrolling. The caption takes its own row here and the controls wrap under
   it; the spacer that pushes them right on a wide bar has nothing to do on a narrow one. */
.guide-diagram .viewer-bar { flex-wrap: wrap; row-gap: calc(var(--step) * 0.75); }
.guide-diagram .viewer-bar .spacer { display: none; }
.guide-diagram .viewer-bar .facet-label { flex: 1 1 100%; }

.guide-diagram .court-canvas { border-radius: 0; }

/* Two examples side by side, for comparing one word against another. */
.guide-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: calc(var(--step) * 3);
}

.guide-pair .guide-example { grid-template-columns: minmax(0, 1fr); margin-bottom: calc(var(--step) * 2); }
.guide-pair h3 { margin-bottom: 0; }

.detail code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--surface-2);
  padding: 1px 4px;
  border-radius: 3px;
}

.card-diagram {
  margin: calc(var(--step) * -1) calc(var(--step) * -1) 0;
  /* Fixed, so a card with a diagram and one without are the same height. The grid used to run
     ragged because the media area sized to its content and only some drills have a diagram. */
  height: 132px;
  display: grid;
  place-items: center;
  overflow: hidden;
  line-height: 0;
  background: var(--surface-2);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* Contained rather than stretched: the SVG keeps its viewBox aspect and letterboxes inside the
   fixed box, so a half court and a full one both sit correctly without distortion. */
.card-diagram .court-canvas {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* The edit form's look at a diagram: the card thumbnail beside the way to change it. */
.diagram-preview { display: flex; gap: var(--step); align-items: flex-start; }
.diagram-preview .note { flex: 1; }
.diagram-preview-court {
  flex: 0 0 200px;
  width: 200px;
  height: 120px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.diagram-preview-court .court-canvas { border-radius: var(--radius-sm); }

.card-diagram.is-empty span {
  font-size: 0.75rem;
  line-height: 1;
  color: var(--muted);
}

@media (max-width: 900px) {
  .editor, .guide-example { grid-template-columns: minmax(0, 1fr); }

  /* Stacked, the fixed viewport height and overflow: hidden clipped the preview pane out of
     existence — the source pane sized to its code and pushed it past the edge, with no way
     to scroll to it. 768px tablet portrait hits this, and clipped is broken rather than
     merely cramped. Height goes back to the content; each pane keeps its own scroll. */
  .editor { height: auto; overflow: visible; }
  .editor-src, .editor-prev { max-height: 70vh; overflow: auto; }

  .library, .detail { grid-template-columns: minmax(0, 1fr); }
  .facets { position: static; }

  /* On a phone the sidebar becomes a strip under the content rather than a column. */
  .detail-side { border-left: none; border-top: 1px solid var(--line); }
}

@media (max-width: 640px) {
  /* Only the identity row stays stuck to the top. At 375px the whole bar is four rows —
     brand and club, account, then six nav links wrapping — and pinning all of it spends a
     fifth of the viewport on chrome that never changes. */
  .appbar { position: static; }
  .appbar-identity {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 2);
    width: 100%;
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--surface);
  }

  .appbar-inner { padding-inline: calc(var(--step) * 2); gap: var(--step); }
  .page { padding-inline: calc(var(--step) * 2); }
  .nav { order: 3; width: 100%; }
  .nav a {
    padding-inline: var(--step);
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .account-menu summary { min-height: 44px; display: inline-flex; align-items: center; }

  /* Thumb-sized. These are all controls the product expects to be used one-handed in a
     hall: the picker's filter chips, the add-drill link, the template form's remove button,
     and the 2D/3D switch. */
  .filter-chip,
  .clear-all,
  .add-drill,
  .block-remove,
  .seg button,
  .tag-remove {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .filter-chip { padding: 0 calc(var(--step) * 1.5); }
  .seg button { padding-inline: calc(var(--step) * 2); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* --- training composer -------------------------------------------------------
   One column at every width. A two-pane plan-beside-library is faster on a
   laptop and impossible on a phone, and one composer to build, style and test
   is worth more than a faster one maintained twice. */

.composer {
  max-width: none;
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 3);
}

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

.composer-head h1 { margin: 0; }
.composer-meta { margin: calc(var(--step) * 0.5) 0 0; color: var(--muted); }

.composer-total {
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--muted);
  font-size: 0.85rem;
}
.composer-total .big {
  display: block;
  font-size: 1.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.composer-total.is-over .big { color: var(--danger); }

.blocks { display: flex; flex-direction: column; gap: calc(var(--step) * 1.5); }

.block {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}

.block-head {
  display: flex;
  align-items: baseline;
  gap: var(--step);
  padding: calc(var(--step) * 1.5);
  background: var(--surface-2);
  cursor: pointer;
}

.block-name { font-weight: 650; }
.block-count { color: var(--muted); font-size: 0.85rem; }

.block-budget {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--ok);
  font-weight: 600;
  flex: none;
}
.block-budget.is-over { color: var(--danger); }

.block-body {
  padding: calc(var(--step) * 1.5);
  display: flex;
  flex-direction: column;
  gap: var(--step);
  border-top: 1px solid var(--line);
}
.block.is-collapsed .block-body { display: none; }

.block-empty { margin: 0; color: var(--muted); font-size: 0.9rem; }

.meter { height: 3px; border-radius: 999px; background: var(--line); overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--accent); }
.meter i.is-over { background: var(--danger); }

.items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }

.item {
  display: flex;
  align-items: center;
  gap: var(--step);
  padding: calc(var(--step) * 0.75) var(--step);
  border-radius: var(--radius-sm);
}
.item:nth-child(odd) { background: var(--surface-2); }
/* Two buttons rather than a drag handle. 44px so they work with a thumb, and they are the
   only reordering affordance — nothing here promises a gesture it does not implement. */
.move { display: flex; flex: none; gap: 2px; }
button.move-up,
button.move-down {
  background: none;
  border: 1px solid var(--line);
  color: var(--muted);
  border-radius: var(--radius-sm);
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  font-size: 0.9rem;
}
button.move-up:hover,
button.move-down:hover { border-color: var(--accent); color: var(--accent); filter: none; }
.item-name { flex: 1; min-width: 0; }
.item-name small { color: var(--muted); }

/* On a phone the row is two 44px buttons, a minutes field and Remove before the name gets
   any width -- about 45px at 375px, less than one word -- so the name ran under the field.
   It takes the whole first line and the controls share the second. Here rather than in the
   phone block above, which precedes .item-name and would lose to it. */
@media (max-width: 640px) {
  .item { flex-wrap: wrap; }
  .item-name { flex-basis: 100%; order: -1; }
}

.item-dur { flex: none; }
.item-dur input[type="number"] {
  width: 4.5rem;
  min-height: 44px;
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  text-align: right;
}

/* button_to puts the class on the <button>, not on the <form> it wraps it in — so these are
   element-qualified, never descendant selectors. Written the other way they match nothing
   and every one of these falls through to the primary button style. */
button.item-remove {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.8rem;
  cursor: pointer;
  padding: 2px 6px;
}
button.item-remove:hover { color: var(--danger); }

/* The Plans library. Structurally the drills index — a list of reusable things — so it
   borrows .drill-card's shape rather than inventing a third card. */
.training-summary {
  padding: calc(var(--step) * 2.5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: calc(var(--step) * 1.5);
}

.training-summary h2 { margin: 0 0 var(--step); font-size: 0.9375rem; line-height: 1.3; }
.training-summary h2 a { color: var(--text); text-decoration: none; }
.training-summary h2 a:hover { color: var(--accent); }
.training-summary p { margin: 0; font-size: 0.8125rem; color: var(--muted); }

.minutes {
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
}
.minutes.is-over { color: var(--danger); font-weight: 600; }

/* Training templates. Same card as a plan — a template is the shape a plan is stamped
   from, so they should not look like different kinds of object. */
.template {
  padding: calc(var(--step) * 2.5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: calc(var(--step) * 1.5);
}

.template h2 { margin: 0 0 var(--step); font-size: 0.9375rem; }
.template h2 small { color: var(--muted); font-weight: 400; font-size: 0.78rem; }
.template > p { margin: 0 0 var(--step); font-size: 0.8125rem; color: var(--muted); }

/* Archived, not deleted: still readable, visibly out of rotation. */
.template.archived { opacity: 0.62; }

.block-list { margin: 0 0 var(--step); padding-left: calc(var(--step) * 3); }
.block-list li { font-size: 0.8125rem; line-height: 1.9; }
.block-list small { color: var(--muted); margin-left: 4px; }
.block-list .minutes { float: right; color: var(--muted); }

/* The training-data switch on the settings screen: the explanation and the toggle side by
   side, so the button reads next to what it does rather than below a wall of text. */
.switch-row { display: flex; gap: calc(var(--step) * 2); align-items: flex-start; }
.switch-row > div { flex: 1; }
.switch-row small { display: block; }

/* The club's templates, summarised on the settings screen. A run-on list of names, not a
   bulleted column — settings is a page of answers, not an outline. */
.inline-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--step) * 0.75) calc(var(--step) * 2);
}
.inline-list li { font-size: 0.8125rem; }
.inline-list small { color: var(--muted); }

/* Block controls sit in the body, under the drills. Both rows wrap on a phone rather than
   scrolling, and every control clears 44px so it can be used with a thumb in a hall. */
/* The header is a button so it can be reached by keyboard and report its state — but it
   should still look like a header. `all: unset` drops the global button styling, which would
   otherwise render every block header as a full-width solid accent bar. */
button.block-toggle {
  all: unset;
  display: flex;
  align-items: center;
  gap: var(--step);
  width: 100%;
  min-height: 44px;
  cursor: pointer;
  box-sizing: border-box;
}
button.block-toggle:focus-visible { outline: var(--ring-width) solid var(--ring); outline-offset: 2px; }

.block-controls,
.add-block {
  display: flex;
  flex-wrap: wrap;
  gap: var(--step);
  align-items: center;
  margin-top: calc(var(--step) * 1.5);
}

/* Folded in from a second `.add-block` block thirty-two lines further down, where two sets of
   declarations for one class could disagree without either looking wrong (2026-09-29 review,
   finding 41). The dashed border is what says "this is where a block gets added" rather than
   "this is a block". */
.add-block {
  padding: calc(var(--step) * 2);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
}

.block-controls { padding-top: calc(var(--step) * 1.5); }

/* The editing chrome is disclosed, not permanent: with three or four blocks, six controls each
   was most of the screen, sitting under the drills that are the actual content. */
.block-settings { margin-top: calc(var(--step) * 1.5); border-top: 1px solid var(--line); }

.block-settings > summary {
  padding-top: calc(var(--step) * 1.5);
  font-size: 0.8125rem;
  color: var(--muted);
  cursor: pointer;
  /* A 44px row rather than the height of the word: this is a phone target. */
  min-height: 44px;
  display: flex;
  align-items: center;
}

.block-settings > summary:hover { color: var(--text); }
.block-settings[open] > summary { color: var(--text); }

.block-retime { display: flex; flex-wrap: wrap; gap: var(--step); align-items: center; }
.block-retime input[type="text"] { flex: 1 1 8rem; min-width: 0; }
.block-retime input[type="number"] { flex: 0 0 5rem; }

.add-block input[type="text"] { flex: 1 1 10rem; min-width: 0; }
.add-block select { flex: 0 0 9rem; }
.add-block input[type="number"] { flex: 0 0 5rem; }

.block-controls input,
.block-controls select,
.block-controls button,
.add-block input,
.add-block select,
.add-block button { min-height: 44px; }

@media (max-width: 560px) {
  .block-retime,
  .add-block { width: 100%; }
  .block-retime input[type="text"],
  .add-block input[type="text"] { flex-basis: 100%; }
}

/* --- agenda -------------------------------------------------------------------
   A list, not a calendar. Two or three sessions a week is a lot of empty grid,
   and a month view needs a different phone screen — which would break the
   one-screen-both-widths rule the composer already follows. */

.agenda-group { margin-bottom: calc(var(--step) * 4); }

.agenda-group h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  margin-bottom: var(--step);
}

.session-row {
  display: flex;
  align-items: center;
  gap: var(--step);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--step);
  margin-bottom: 4px;
  background: var(--surface);
}
.session-row.is-next { border-color: var(--accent); background: var(--accent-soft); }

/* The textual half of the "next" signal. The border says it to people who can see it; this
   says it to everyone else. */
.next-flag {
  display: inline-block;
  margin-left: var(--step);
  padding: 1px calc(var(--step) * 0.75);
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.7rem;
  font-weight: 660;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  vertical-align: 1px;
}

/* The row's primary fact, on the screen most often read walking into a hall. The mockups'
   run-mode revision records that small grey text is the first thing to fail at arm's length,
   and this was 0.78rem muted. The muting stays where it belongs — on the secondary line. */
.session-when {
  flex: 0 0 5.5rem;
  font-family: var(--font-mono);
  font-size: 0.875rem;
  color: var(--text);
  line-height: 1.35;
}
.session-when b { display: block; font-weight: 660; }

.session-what { flex: 1; min-width: 0; }
.session-meta { font-size: 0.8rem; color: var(--muted); }

.session-marks { display: flex; gap: var(--step); align-items: center; flex: none; }
.session-marks .score { color: var(--ball); font-variant-numeric: tabular-nums; font-size: 0.85rem; }
.session-marks .ran {
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  color: var(--muted);
}

/* The phone drops the durations rather than shrinking them — the cut the mockup
   makes, not a smaller copy of the same row. */
@media (max-width: 560px) {
  .session-row { flex-wrap: wrap; }
  .session-when { flex: 0 0 auto; }
  .session-marks { margin-left: auto; }
  .session-marks .ran { display: none; }
}

/* --- session screen ----------------------------------------------------------
   The handover from desk to hall: the plan in full, the total against the
   template, and one button that starts it. */

.session-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--step);
  flex-wrap: wrap;
}
.session-head h1 { margin: 0; }
.session-head .session-meta { margin: calc(var(--step) * 0.5) 0 0; }

.status {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  border-radius: 999px;
  padding: 2px 9px;
  border: 1px solid var(--line-strong);
  color: var(--muted);
  flex: none;
}
.status-running { border-color: var(--accent); color: var(--accent); }
.status-completed { border-color: var(--ok); color: var(--ok); }

.plan-phase { margin-bottom: calc(var(--step) * 3); }
.plan-phase h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.plan-target { float: right; font-variant-numeric: tabular-nums; }

.plan-items { list-style: none; margin: 0; padding: 0; }

.plan-item {
  display: flex;
  align-items: baseline;
  gap: var(--step);
  padding: calc(var(--step) * 0.6) var(--step);
  border-radius: var(--radius-sm);
}
.plan-item:nth-child(odd) { background: var(--surface-2); }

.plan-number {
  flex: 0 0 1.4rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-size: 0.85rem;
}
.plan-name { flex: 1; min-width: 0; }
.plan-dur {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  flex: none;
}

.plan-total {
  border-top: 1px solid var(--line);
  padding-top: var(--step);
  color: var(--muted);
}
.plan-total .big {
  font-variant-numeric: tabular-nums;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--text);
}
.plan-total.is-over .big { color: var(--danger); }

/* --- after-session review -----------------------------------------------------
   Every number is a pair. A skipped drill stays visible, struck through: what
   was dropped tells you as much as what was run. */

.review-total {
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--step);
}
.review-total .big {
  font-variant-numeric: tabular-nums;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--text);
}
.review-total .big.is-over { color: var(--danger); }
.review-total .score { color: var(--ball); }

.review-phase { margin-bottom: calc(var(--step) * 3); }
.review-phase h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.review-phase-total {
  float: right;
  font-variant-numeric: tabular-nums;
  color: var(--ok);
}
.review-phase-total.is-over { color: var(--danger); }

.review-items { list-style: none; margin: 0; padding: 0; }

.review-item {
  display: flex;
  align-items: baseline;
  gap: var(--step);
  padding: calc(var(--step) * 0.6) var(--step);
  border-radius: var(--radius-sm);
}
/* nth-child, not nth-of-type: the note used to be a sibling <li> and was counted, so every
   row after a note took the wrong shade. It lives inside its item now, and nth-child stays
   correct if anything else is ever added to the list. */
.review-item:nth-child(odd) { background: var(--surface-2); }
.review-item.is-skipped { opacity: 0.55; }
.review-item.is-skipped .review-name { text-decoration: line-through; }

.review-number {
  flex: 0 0 1.4rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-size: 0.85rem;
}
.review-name { flex: 1; min-width: 0; }
.review-rating {
  color: var(--ball);
  font-variant-numeric: tabular-nums;
  flex: none;
  font-size: 0.85rem;
}
.review-actual, .review-planned {
  font-variant-numeric: tabular-nums;
  flex: none;
}
.review-planned { color: var(--muted); }

/* Sits on its own line inside the item's flex row. */
.review-note {
  flex-basis: 100%;
  margin: 4px 0 0;
  padding-left: calc(var(--step) * 3.5);
  font-size: 0.82rem;
  color: var(--muted);
  font-style: italic;
}

/* --- drill picker ------------------------------------------------------------
   The filters are visible and removable on purpose. Four constraints applied
   silently would make a three-result list look like an empty library, and the
   coach would never learn which one to loosen. */

.add-drill {
  align-self: flex-start;
  font-size: 0.9rem;
  color: var(--accent);
  text-decoration: none;
}
.add-drill:hover { text-decoration: underline; }

.picker-page { display: flex; flex-direction: column; gap: calc(var(--step) * 2); }

.picker-head { display: flex; align-items: center; gap: var(--step); flex-wrap: wrap; }
.picker-head h1 { margin: 0; flex: 1; font-size: 1.35rem; }
.picker-done { margin-left: auto; }

.picker-context {
  display: flex; flex-wrap: wrap; gap: calc(var(--step) * 0.5) calc(var(--step) * 1.5);
  align-items: baseline; margin: 0; color: var(--muted); font-size: 0.9rem;
  padding-bottom: var(--step); border-bottom: 1px solid var(--line); max-width: none;
}
.picker-parent { color: var(--text); font-weight: 560; }
.picker-budget { margin-left: auto; font-variant-numeric: tabular-nums; }
.picker-budget b { color: var(--text); }
.picker-budget.is-over b { color: var(--danger); }

.picker-body { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; gap: calc(var(--step) * 3); align-items: start; }
.picker-results { display: flex; flex-direction: column; gap: calc(var(--step) * 1.5); min-width: 0; }

.picker-search input[type="search"] { width: 100%; }

.filters { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.78rem;
  border-radius: 999px;
  padding: 2px 9px;
  border: 1px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.filter-chip .x { color: var(--muted); font-weight: 400; }
.filter-chip:hover { background: var(--accent); color: var(--accent-ink); }
.filter-chip:hover .x { color: var(--accent-ink); }

.clear-all { font-size: 0.78rem; color: var(--muted); margin-left: auto; white-space: nowrap; }

.picker-count, .picker-empty { font-size: 0.78rem; color: var(--muted); margin: 0; }

/* On a phone four chips wrap to three rows before a single result appears, so they
   scroll on one line instead. Same filters, same results, less furniture.

   The fade is the part that was missing: with the scrollbar hidden and no edge treatment, a
   filter off-screen narrowed the results with nothing on screen to say so — which also made
   the "3 of 47 drills" count unexplainable from what the coach could see. */
@media (max-width: 560px) {
  .filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
  .filters::-webkit-scrollbar { display: none; }
  .clear-all { margin-left: 0; }
}

/* Results are cards, the library's own, not a text list -- judging a drill for a block wants
   the same picture a coach browses the library by. */

.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: calc(var(--step) * 2);
}

.pick-card {
  display: flex; flex-direction: column; gap: var(--step);
  padding: calc(var(--step) * 1.5);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.pick-card.is-added { border-color: var(--ok); }
.pick-card.is-open { grid-column: 1 / -1; }
.pick-name { margin: 0; font-size: 0.9375rem; line-height: 1.3; }
.pick-facts { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 1.25); margin: 0; font-size: 0.8125rem; color: var(--muted); }
.pick-actions { display: flex; align-items: center; gap: var(--step); flex-wrap: wrap; margin-top: auto; }
.pick-add { display: inline-block; margin: 0; max-width: none; }
.pick-added { color: var(--ok); font-weight: 560; font-size: 0.875rem; }
.pick-already { color: var(--muted); font-size: 0.8125rem; }
.pick-again { font-size: 0.8125rem; }
.pick-preview { margin-left: auto; }

.picker-rail {
  position: sticky; top: calc(var(--step) * 2);
  display: flex; flex-direction: column; gap: var(--step);
  padding-left: calc(var(--step) * 2); border-left: 1px solid var(--line);
  font-size: 0.875rem;
}
.rail-head { margin: 0; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); font-weight: 600; }
.rail-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: calc(var(--step) * 0.5); }
.rail-item, .rail-other { display: flex; gap: var(--step); align-items: baseline; }
.rail-item-name { flex: 1; min-width: 0; }
.rail-item-min { color: var(--muted); font-variant-numeric: tabular-nums; }
.rail-empty { color: var(--muted); font-style: italic; }
.rail-total { margin: 0; padding-top: var(--step); border-top: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.rail-total b { font-size: 1.1rem; }
.rail-total.is-over b { color: var(--danger); }
.rail-left { color: var(--muted); margin-left: var(--step); }

/* A second 640px block rather than folding into the one further up: this file's own
   convention is a breakpoint declared again beside the component it reshapes (the filters'
   and the preview's own @media (max-width: 560px) do the same), and it has to be -- the
   generic block sits earlier in the file, so an unconditional .picker-body/.picker-rail
   declared after it would win the cascade at every width, media query or not. */
@media (max-width: 640px) {
  /* The rail becomes a footer: only the total line shows, stuck to the bottom, so the number
     a coach is filling towards is always in view while the cards scroll. */
  .picker-body { grid-template-columns: minmax(0, 1fr); }
  .picker-rail {
    position: sticky; top: auto; bottom: 0; order: 2;
    padding: var(--step) calc(var(--step) * 2);
    padding-bottom: calc(var(--step) + env(safe-area-inset-bottom, 0px));
    margin-inline: calc(var(--step) * -2);
    border-left: 0; border-top: 1px solid var(--line);
    background: var(--surface);
    flex-direction: row; align-items: baseline; gap: calc(var(--step) * 1.5);
  }
  .picker-rail .rail-head, .picker-rail .rail-items, .picker-rail .meter { display: none; }
  .picker-rail .rail-total { padding-top: 0; border-top: 0; }
  .picker-rail .rail-total::before { content: attr(data-block) " · "; color: var(--muted); }
  .picker-done { min-height: 44px; }
  /* The title on its own line; Back and Done share the next. Between the two it was three
     lines of four-word title. */
  .picker-head h1 { flex-basis: 100%; order: -1; }
}

.preview {
  display: flex;
  flex-direction: column;
  gap: var(--step);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface);
  padding: var(--step);
}

/* The playable viewer inside a preview keeps the card's width, so the beat bar wraps. */
.preview .viewer { margin: 0; }

.preview-section .label { display: block; margin-bottom: 4px; }
.preview-section p, .preview-section ul { margin: 0; font-size: 0.9rem; }
.preview-section ul { padding-left: 1.1em; }
.preview-history { padding-top: 6px; border-top: 1px dashed var(--line); }
.preview-teams { list-style: none; padding: 0; font-size: 0.85rem; }
.preview-teams .is-this-team { color: var(--accent); }
.preview-note { font-size: 0.85rem; color: var(--muted); font-style: italic; }

.preview-detail { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-objective { margin: 0; font-size: 0.9rem; }

.preview-facts {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 0.78rem;
  color: var(--muted);
}

.preview-actions { display: flex; gap: 6px; align-items: center; margin-top: 2px; }
.preview-open { font-size: 0.82rem; color: var(--muted); }

@media (max-width: 560px) {
  .preview { flex-direction: column; }
}

/* --- nested block fields ------------------------------------------------------ */

.block-fields { display: flex; flex-direction: column; gap: var(--step); }

.block-row {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--step);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 0.75);
}
.block-row[hidden] { display: none; }

.variation-row {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--step);
  margin: 0 0 var(--step);
  display: flex;
  flex-direction: column;
  gap: calc(var(--step) * 0.75);
}
.variation-row[hidden] { display: none; }

.block-remove {
  align-self: flex-start;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.8rem;
  cursor: pointer;
  padding: 2px 0;
}
.block-remove:hover { color: var(--danger); }

.block-add {
  margin-top: var(--step);
  background: none;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--accent);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  padding: calc(var(--step) * 0.75) var(--step);
}
.block-add:hover { border-color: var(--accent); background: var(--accent-soft); }

/* ---------------------------------------------------------------------------
   Run-mode — the two gym screens
   ---------------------------------------------------------------------------
   Sized for a phone held at arm's length in a lit hall, read between rallies
   with a ball in the air. Everything here is bigger than the rest of the app
   on purpose: rows are thumb-sized, the clock is glanceable, and coaching
   points are body text rather than the small grey text they would be on a
   desk screen.

   The layout opts out of dark mode (see layouts/run.html.erb), so these rules
   deliberately use fixed light values rather than the themed tokens.
--------------------------------------------------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.run-body {
  background: var(--bg);
  margin: 0;
}

.run {
  max-width: 44rem;
  margin: 0 auto;
  padding: calc(var(--step) * 2);
  /* Notch and home-indicator safe. A footer button under the home bar is a
     button that takes two taps. */
  padding-top: max(calc(var(--step) * 2), env(safe-area-inset-top));
  padding-bottom: max(calc(var(--step) * 2), env(safe-area-inset-bottom));
}

.run-head { margin-bottom: calc(var(--step) * 2); }
.run-head h1 { margin: calc(var(--step) * 0.5) 0; font-size: 1.5rem; line-height: 1.2; }
.run-head-meta { color: var(--muted); font-size: 0.9rem; }
.run-budget { color: var(--muted); font-size: 0.95rem; }
.run-budget b { color: var(--text); }

.run-block { margin-bottom: calc(var(--step) * 2.5); }

.run-block-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0 calc(var(--step) * 0.5) calc(var(--step) * 0.5);
  border-bottom: 1px solid var(--line);
  margin-bottom: calc(var(--step) * 0.5);
}
.run-block-target { color: var(--muted); font-size: 0.9rem; }

.run-item {
  display: flex;
  align-items: center;
  gap: var(--step);
  /* 60px, comfortably over the 44px floor. A mis-tap here opens the wrong
     drill in the middle of a session. */
  min-height: 60px;
  padding: calc(var(--step) * 0.75) var(--step);
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--line);
  margin-bottom: calc(var(--step) * 0.5);
  color: var(--text);
  text-decoration: none;
}
.run-item:hover { border-color: var(--line-strong); }

/* Pressed state, not just hover: this row is tapped on a phone in a hall, where hover does not
   exist and the only confirmation a tap landed is the row reacting to it. */
.run-item:active { background: var(--surface-2); }

.run-item-go { color: var(--muted); font-size: 1.25rem; line-height: 1; }
.run-item:hover .run-item-go { color: var(--text); }

.run-item-mark { width: 1.25rem; text-align: center; color: var(--muted); font-size: 1.1rem; }
.run-item-name { flex: 1; font-size: 1.05rem; }
.run-item-time { color: var(--muted); font-variant-numeric: tabular-nums; }

.run-item.is-running {
  border-color: var(--accent);
  background: var(--accent-soft);
  /* Two signals, not one: colour alone fails for anyone who cannot separate
     these two backgrounds, and the mark carries the same fact. */
  box-shadow: inset 3px 0 0 var(--accent);
}
.run-item.is-running .run-item-mark,
.run-item.is-running .run-item-time { color: var(--accent); font-weight: 600; }

.run-item.is-done { background: var(--ok-soft); border-color: var(--ok); }
.run-item.is-done .run-item-mark { color: var(--ok); }

.run-item.is-skipped { opacity: 0.6; }
.run-item.is-skipped .run-item-name { text-decoration: line-through; }

.run-resume {
  display: block;
  text-align: center;
  position: sticky;
  bottom: calc(var(--step) + env(safe-area-inset-bottom));
  margin-top: calc(var(--step) * 2);
  box-shadow: var(--shadow);
}

.run-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--step);
  margin-top: calc(var(--step) * 3);
  padding-top: calc(var(--step) * 2);
  border-top: 1px solid var(--line);
}

/* --- the drill screen ---------------------------------------------------- */

.run-drill-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--step);
}

.run-clock {
  font-size: 2rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  line-height: 1;
}

.run-drill-where { color: var(--muted); font-size: 0.9rem; }
.run-drill h1 { margin: calc(var(--step) * 0.5) 0 var(--step); font-size: 1.6rem; line-height: 1.2; }

.run-diagram {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: calc(var(--step) * 2);
}
.run-diagram .court-canvas { display: block; width: 100%; height: auto; }

.run-points {
  margin: 0 0 calc(var(--step) * 2);
  padding-left: calc(var(--step) * 2.5);
}
.run-points li {
  /* Body size. Small grey text at arm's length is the first thing to fail,
     and this is the part being read between rallies. */
  font-size: 1.05rem;
  line-height: 1.5;
  margin-bottom: calc(var(--step) * 0.75);
}

.run-gone { color: var(--muted); font-style: italic; }

.diagram-chips { display: flex; gap: calc(var(--step) * 0.75); flex-wrap: wrap; margin: var(--step) 0; }
.diagram-chips a[aria-current="page"] { background: var(--accent); color: var(--accent-ink); border-color: transparent; }

.run-drill-foot {
  position: sticky;
  bottom: 0;
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding: var(--step) 0 max(var(--step), env(safe-area-inset-bottom));
  margin-top: calc(var(--step) * 2);
}

.run-next {
  color: var(--muted);
  font-size: 0.9rem;
  margin-bottom: calc(var(--step) * 0.75);
}
.run-next span { font-variant-numeric: tabular-nums; }

.run-capture { margin-top: var(--step); }

.run-rate { display: flex; gap: calc(var(--step) * 0.25); border: 0; padding: 0; margin: 0 0 calc(var(--step) * 0.5); }
.run-rate legend { padding: 0; }

.run-star {
  /* The radio itself is hidden rather than removed: it stays focusable and
     still announces its label, so the rating is reachable by keyboard and by
     screen reader even though what is drawn is a star. */
  cursor: pointer;
  font-size: 1.75rem;
  line-height: 1;
  color: var(--line-strong);
  padding: calc(var(--step) * 0.25);
}
.run-star input { position: absolute; opacity: 0; width: 0; height: 0; }
.run-star.is-set { color: var(--ball); }
.run-star:focus-within { outline: var(--ring-width) solid var(--ring); outline-offset: 2px; border-radius: var(--radius-sm); }

.run-capture textarea {
  width: 100%;
  font: inherit;
  padding: calc(var(--step) * 0.75);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  resize: vertical;
}

.run-actions { display: flex; gap: var(--step); }
.run-actions form { flex: 1; }
.run-actions .btn, .run-actions button { width: 100%; min-height: 52px; font-size: 1.05rem; }

@media (max-width: 30rem) {
  .run { padding-left: var(--step); padding-right: var(--step); }
  .run-head h1, .run-drill h1 { font-size: 1.35rem; }
}

.session-run { display: flex; align-items: center; gap: var(--step); margin: calc(var(--step) * 2) 0; }
.session-run small { color: var(--muted); }

/* --- OAuth consent ------------------------------------------------------- */

.oauth-consent { max-width: 34rem; margin: calc(var(--step) * 4) auto; }
.oauth-consent h1 { font-size: 1.4rem; margin-bottom: calc(var(--step) * 0.5); }
.oauth-club { color: var(--muted); margin-bottom: calc(var(--step) * 2); }

/* The free-tier line on the consent screen and the connected-apps page: a note, not an error. */
.tier-notice {
  padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
  margin-bottom: calc(var(--step) * 2);
  border: 1px solid var(--ball);
  border-radius: var(--radius);
  background: var(--ball-soft);
  font-size: 0.9375rem;
}

.oauth-scopes { list-style: none; padding: 0; margin: 0 0 calc(var(--step) * 3); }
.oauth-scope {
  padding: var(--step);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: calc(var(--step) * 0.5);
  background: var(--surface);
}
/* The destructive one reads differently, because it is different. */
.oauth-scope-manage { border-color: var(--danger); background: var(--danger-soft); }

.oauth-actions { display: flex; gap: var(--step); align-items: center; }

/* --- Connected apps ------------------------------------------------------ */

.connected-list { list-style: none; padding: 0; margin: 0; }
.connected-app {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--step);
  padding: var(--step);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: calc(var(--step) * 0.5);
}
.connected-what { flex: 1; min-width: 0; }
.connected-meta { color: var(--muted); font-size: 0.9rem; margin-top: calc(var(--step) * 0.25); }
.pill-danger { background: var(--danger-soft); color: var(--danger); }

/* The address a coach copies into a client. Mono, selectable, and one line. */
.urlbox {
  display: flex; align-items: center; gap: var(--step);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5); background: var(--surface-2);
  margin-bottom: var(--step);
}
.urlbox-url {
  font-family: var(--font-mono); font-size: 0.875rem; user-select: all;
  overflow-wrap: anywhere; background: none; border: 0; padding: 0;
}

/* ---------- legal ---------- */

.legal h2 { margin-top: calc(var(--step) * 4); }
