/*
 * The shared component library.
 *
 * Every screen is built from these. A screen that needs a control this file does not have adds
 * it here rather than styling one locally — that is the whole difference between a design
 * system and a folder of stylesheets.
 *
 * All values come from styles/tokens.css. If a rule below contains a hex, a pixel font size or
 * a hard-coded duration, it is a bug.
 */

/* ── Buttons ───────────────────────────────────────────────────────────────────────
   Order matters here, and it is the one rule to keep when adding a variant:
   GEOMETRY FIRST, THEN COLOUR. Every modifier below is a single class, so they all have
   the same specificity and the last one in the file wins. When `.btn--icon` (a shape) sat
   after `.btn--primary` (a colour) it quietly took the fill off every primary icon button —
   which on the Playbooks bar meant a white `+` on a white app bar, present, focusable,
   tappable and invisible. */

/*
 * THE DEFAULT BUTTON IS SECONDARY, NOT FILLED.
 *
 * It used to arrive as a filled `--surface-3` slab, which meant every interactive surface in the
 * app looked like a button and none of them looked like THE button: Load, Save as Formation,
 * Flip Play, Show Grid, Center and Done all carried the same weight as the one action a coach
 * came to the screen for. An outline states "this is a control" without competing for the eye,
 * and the one filled shape on a screen is then unmistakably its primary action.
 *
 * The levels, in order of weight: `--primary` (filled, one per screen or sheet) · `--tonal`
 * (accent, not THE action) · plain `.btn` (outlined) · `--plain` (borderless, for bars) ·
 * a `.group-row` with a chevron (navigation) · a switch (boolean) · `--danger` (destructive).
 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: 0 var(--space-4);
  border-radius: var(--radius-sm);
  background: transparent;
  border: var(--border-width) solid var(--hairline-strong);
  color: var(--text);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  white-space: nowrap;
  user-select: none;
  -webkit-user-select: none;
  transition: background var(--dur-fast) var(--ease-standard), transform var(--dur-instant) var(--ease-standard);
}

/* ---- Shape and size ---- */

.btn--block {
  width: 100%;
}

.btn--lg {
  min-height: var(--control-h-lg);
  font-size: var(--fs-label);
  border-radius: var(--radius-md);
}

/*
 * A small action in a wrapping row: the defensive action library, the team-defense calls.
 *
 * Sized to its label, not to an equal share of the row. `flex: 1 1 auto` with a min-width made
 * every button the same width and cut the long ones — "Zone coverage" needs 161px and was given
 * 102, so it read "Zone cover…", and "Protect Over The Top" and "Play Underneath" printed over
 * each other. A ragged right edge is the correct shape for a list of names.
 */
.btn--tiny {
  max-width: 100%;
  min-height: var(--control-h-sm);
  padding: 0 var(--space-3);
  font-size: var(--fs-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline-block;
  line-height: var(--control-h-sm);
  text-align: center;
  white-space: nowrap;
}

/* An icon button carries no outline: in a bar or a row a box round every glyph is the noise
   this pass exists to remove. Geometry, so it stays ahead of the colour modifiers. */
.btn--icon {
  width: var(--control-h);
  min-width: var(--control-h);
  padding: 0;
  background: transparent;
  border-color: transparent;
}

/* ---- Colour ---- */

/* The one filled button on a screen: its primary action. A second one on the same screen is a
   screen that has not decided what it is for. */
.btn--primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--text-on-accent);
}

/* Tonal: an accent action that is not THE action. Common on a row of two. */
.btn--tonal {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
}

/* Borderless, for a bar or a row where even an outline would be noise. */
.btn--plain {
  background: transparent;
  border-color: transparent;
}

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

/*
 * SELECTED IS NOT PRIMARY.
 *
 * `--active` used to be the same solid accent as `--primary`, so the tool a coach was holding
 * was drawn with exactly the weight of the one action on the screen — and in the editor, with
 * a mode selected, snap on and Add Player beside them, that was three identical accent blocks
 * and no answer to "what is this screen for". Selected is tonal: the accent, quieter, with the
 * label at bold. It still carries a fill and `aria-pressed`, so it is never colour alone.
 */
.btn--active {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
  font-weight: var(--fw-bold);
}

/* ---- Pressed ---- */

.btn:active {
  transform: scale(0.98);
  background: var(--surface-press);
}

.btn--icon:active,
.btn--plain:active {
  background: var(--surface-hover);
}

.btn--primary:active {
  background: var(--accent-press);
}

.btn--active:active {
  background: var(--accent-soft);
}

.btn[disabled] {
  opacity: 0.4;
  pointer-events: none;
}

.btn-row {
  display: flex;
  gap: var(--space-2);
}

.btn-row > .btn {
  flex: 1;
}

/* Several to a row, wrapping rather than shrinking below a finger. */
.btn-row--wrap {
  flex-wrap: wrap;
}

/*
 * A wrapping row is a LIST of actions, not a row to divide equally.
 *
 * `.btn-row > .btn { flex: 1 }` is right for two buttons sharing a row and wrong here: with a
 * zero basis every button in the defensive action library came out the same width, so "Zone
 * coverage" — which needs 161px — was given 47 and read "Zon…". They size to their labels and
 * wrap to the next line, which is what a ragged right edge is for.
 */
.btn-row--wrap > .btn {
  flex: 0 1 auto;
}

/* ── Icons ─────────────────────────────────────────────────────────────────────────── */

/* SVG icons inherit the text colour and sit on the text baseline. One stroke weight, one
   corner style, one box — see ui/icons.js. */
.ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-md);
  height: var(--icon-md);
  flex-shrink: 0;
  color: inherit;
}

.ico svg {
  width: 100%;
  height: 100%;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A few marks read better filled than stroked (the football, a solid transport triangle). */
.ico svg [data-fill] {
  fill: currentColor;
  stroke: none;
}

.ico--sm {
  width: var(--icon-sm);
  height: var(--icon-sm);
}

.ico--lg {
  width: var(--icon-lg);
  height: var(--icon-lg);
}

.btn--icon .ico {
  width: var(--icon-lg);
  height: var(--icon-lg);
}

/* ── Surfaces ──────────────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: var(--border-width) solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}

/*
 * A notice: something happened that a coach has to decide about.
 *
 * A band across the screen rather than a card floating on it. It appears only when there IS a
 * decision — a sync conflict, access withdrawn, edits that can no longer be shared — so it
 * never becomes the banner nobody reads, and it carries the screen's own gutter so it lines up
 * with the content it is about at every width.
 */
.notice {
  padding: var(--space-3) var(--gutter);
  border-top: var(--border-width) solid var(--hairline);
  border-bottom: var(--border-width) solid var(--hairline);
  background: var(--accent-soft);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.notice[hidden] {
  display: none;
}

/* Inside a scroller that already has the gutter, the band cancels it rather than indenting
   twice: a notice is a band across the screen wherever it is mounted. */
.view-scroll > .notice {
  margin-inline: calc(var(--gutter) * -1);
  margin-top: calc(var(--space-4) * -1);
}

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

.stack--tight {
  gap: var(--space-2);
}

.stack--loose {
  gap: var(--space-5);
}

/* A control and the single line under it that says what it does: one step, not a gap. */
.stack--snug {
  gap: var(--space-1);
}

/* ── Lists ─────────────────────────────────────────────────────────────────────────── */

/*
 * `.list-row` itself is gone.
 *
 * It was a painted card-row — surface, hairline, 14px radius — that no view had used since the
 * components below grew their own shapes, sitting in the library inviting the next person to
 * reuse exactly the treatment this pass removed. What survives is the four parts of it that
 * other components genuinely borrow: a title, a meta line, a thumbnail and a chevron.
 */
.list-row__title {
  font-size: var(--fs-label);
  line-height: var(--lh-label);
  font-weight: var(--fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list-row__meta {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-1);
  color: var(--text-2);
  font-size: var(--fs-secondary);
  line-height: var(--lh-secondary);
  flex-wrap: wrap;
}

.list-row__thumb {
  width: 92px;
  height: 60px;
  border-radius: var(--radius-xs);
  overflow: hidden;
  flex-shrink: 0;
  background: var(--surface-sunken);
}

/* The size a picker row uses: a preset row is shorter than a library row, so its preview is too. */
.list-row__thumb--sm {
  width: 76px;
  height: 50px;
}

.list-row__thumb canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.list-row__chevron {
  color: var(--text-3);
  display: inline-flex;
  flex-shrink: 0;
}

.row-actions {
  display: flex;
  gap: var(--space-1);
  flex-shrink: 0;
}

/*
 * Grouped list: rows under a section heading, divided by hairlines.
 *
 * THIS IS NOT A CARD. It used to be one — a filled, bordered, rounded rectangle per group —
 * which is how Settings ended up as eight floating slabs, the Share tab as a slab of eight
 * document rows, and Team Defense as a slab containing exactly one row. A group is not an
 * object; it is a set of related rows, and the things that say so are the heading above it,
 * the rules between the rows and the space underneath. A rule costs one pixel to say what a
 * border, a radius, a fill and a shadow were saying together.
 *
 * There is deliberately no way back. An object that needs a boundary — a playbook, a play, a
 * formation, a shared-playbook invitation — has a component of its own that carries one, and a
 * documented escape hatch on this rule is a thing the next person in a hurry reaches for.
 */
/* The rows inside adapt to the GROUP's width, not the window's: the same list is a full-width
   settings screen on a phone and a 320px docked inspector in the editor, and a media query
   cannot tell those apart. */
.group {
  container-type: inline-size;
}

.group > * + * {
  border-top: var(--border-width) solid var(--hairline);
}

/*
 * A row reaches the gutter; only its pressed highlight overhangs.
 *
 * With the card gone the row's text has to line up with the heading above it and with the
 * screen's margin, so the inline padding is zero — and a press state with no inset looks like
 * a rectangle drawn round the words. The negative margin gives the highlight its bleed back
 * without moving a single glyph.
 */
.group-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: calc(100% + var(--space-2) * 2);
  min-height: var(--control-h-lg);
  padding: var(--space-3) var(--space-2);
  margin-inline: calc(var(--space-2) * -1);
  border-radius: var(--radius-xs);
  text-align: left;
  color: var(--text);
  background: transparent;
  transition: background var(--dur-fast) var(--ease-standard);
}

button.group-row:active {
  background: var(--surface-hover);
}

.group-row__body {
  flex: 1;
  min-width: 0;
}

.group-row__title {
  font-size: var(--fs-label);
  line-height: var(--lh-label);
  font-weight: var(--fw-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.group-row__hint {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--text-2);
  margin-top: 2px;
}

/*
 * The current value, shown in the row rather than behind it.
 *
 * "Technique · Press · Inside · Over Top" beside the label is the difference between a coach
 * knowing what the defense is set to and a coach having to open four sections to find out.
 */
.group-row__value {
  font-size: var(--fs-secondary);
  line-height: var(--lh-secondary);
  color: var(--text-2);
  text-align: right;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Half the row at most. Past that the label loses so much width that its own text starts
     wrapping a word to a line, which is what a 320px docked inspector was doing. */
  max-width: 50%;
}

/*
 * Narrow group: the value drops to its own line.
 *
 * A long value does not fit across 320px beside a title, an icon and a chevron, and the two ways
 * to force it — wrapping the title or truncating the value — each destroy the thing the row
 * exists to say. Below the value goes instead, at full width, where it is read in one line.
 *
 * The threshold is 340px rather than the 380px it was written at: the row used to carry 16px of
 * card padding on each side, and taking the card away gave it that width back. At 380 a Settings
 * row on a 390px phone was three lines tall for a two-word value.
 */
@container (max-width: 340px) {
  .group-row {
    flex-wrap: wrap;
    row-gap: var(--space-1);
  }

  .group-row__value {
    flex-basis: 100%;
    max-width: 100%;
    text-align: left;
    order: 5;
  }

  /* A row with a leading icon wraps its value onto a line that starts at the row's edge, under
     the icon rather than under the label it belongs to. The general sibling combinator is
     enough to know an icon came first, so the wrapped line indents to meet the title. */
  .group-row > .ico ~ .group-row__value {
    margin-left: calc(var(--icon-md) + var(--space-3));
  }
}

/*
 * A row that exists to STATE something. Its title IS its heading.
 *
 * The inspector's rows are read far more often than they are opened — a coach wants "Press ·
 * Inside · Over Top" and "Own 35", not "Team Defense ›" and "Field Position ›". So the value
 * comes off the right-hand edge, where it was competing with the label for a shrinking share of
 * a 320px panel at caption size, and takes a line of its own at label size in the primary text
 * colour, under a quiet small-caps label:
 *
 *     FIELD POSITION
 *     Own 35                                    ›
 *
 * The label is inside the row rather than a `.section-label` above it, because a heading and a
 * row title one line apart end up saying the same thing at two different sizes — SHOW / On the
 * field / Offense + Defense is three lines of type for one fact. One row, one fact, two lines.
 *
 * This is the shape the container query below already forced on narrow rows. Here it is the
 * intent rather than the fallback, so the row reads the same in a phone sheet and in a 400px
 * docked panel.
 */
.group-row--stated {
  display: grid;
  /* Label / value / hint, and a column each for a badge and the chevron. The rows are explicit
     for the same reason the section head's are: a `1 / -1` span needs them to exist. */
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-rows: auto auto auto;
  align-items: center;
  column-gap: var(--space-3);
  row-gap: 2px;
}

.group-row--stated > .group-row__body {
  grid-column: 1;
  grid-row: 1;
}

/* The display role (base.css) plus the one thing that is this row's own. The six type
   declarations it used to restate live in the role now. `.group-row__title` above still sets a
   size of its own for every OTHER row, and does not reach this one: the role is listed in
   base.css under this same two-class selector, which outranks the single-class rule above
   whichever file it is written in. */
.group-row--stated .group-row__title {
  color: var(--text-3);
}

.group-row--stated .group-row__hint {
  grid-column: 1;
  grid-row: 3;
}

.group-row--stated > .group-row__value {
  grid-column: 1;
  grid-row: 2;
  max-width: 100%;
  text-align: left;
  font-size: var(--fs-label);
  line-height: var(--lh-label);
  font-weight: var(--fw-medium);
  color: var(--text);
  /* Wraps rather than truncating, for the same reason `.section__value` does: the row exists to
     state this, and a 310px docked panel is not wide enough for every team call on one line. */
  white-space: normal;
  overflow: visible;
  overflow-wrap: break-word;
}

/* The chevron and any badge span the row rather than pinning themselves to its first line —
   and they take a column each, so a row built with both does not draw them on top of each
   other. */
.group-row--stated > .tag {
  grid-column: 2;
  grid-row: 1 / -1;
}

.group-row--stated > .list-row__chevron {
  grid-column: 3;
  grid-row: 1 / -1;
}

.group-row--danger .group-row__title,
.group-row--danger > .ico {
  color: var(--danger);
}

/*
 * Signing out is an ACCOUNT action, not a destructive one.
 *
 * It borrows the danger colour so a coach can find it at a glance, and nothing else: no trash
 * glyph, no `.group--danger` rule above it, no move to the bottom of the screen. Erase all
 * local data and Delete Account keep all of that, which is what makes the difference between
 * the two legible — the colour marks the row, the treatment marks the consequence.
 */
.group-row--signout .group-row__title {
  color: var(--danger);
}

/* Destructive rows sit in their own group, separated from everything else — by a rule in the
   danger colour above them and by the space that puts them at the bottom of the screen, which
   is all a boundary was ever doing here. */
.group--danger {
  border-top: var(--border-width) solid var(--danger-line);
  padding-top: var(--space-1);
}

.group--danger > * + * {
  border-top-color: var(--danger-soft);
}

/* ── Disclosure sections ───────────────────────────────────────────────────────────
   The inspector's unit of progressive disclosure: a header that states the current value
   when closed, and the controls underneath when open. Built on <details>/<summary> so
   keyboard and screen-reader behaviour comes from the platform rather than from a div
   with a click handler. */

/*
 * A section is a divided row, not a box.
 *
 * Seven of these stacked in a 320px inspector used to be seven bordered, rounded, filled cards
 * one under another — a wall of identical rectangles in which Assignment, the thing a coach
 * selected the player for, looked exactly like Appearance. What separates them now is a rule
 * and the space around it, and what distinguishes them is the type.
 */
.section {
  border-top: var(--border-width) solid var(--hairline);
}

/* The first section in a panel needs no rule: the identity strip above it is the boundary. */
.section:first-of-type {
  border-top: 0;
}

/*
 * THE VALUE IS THE ROW; THE NAME IS THE LABEL ON IT.
 *
 * The header used to read "Technique" at label size with "Press · Inside · Over Top" squeezed
 * right-aligned at caption size — the name shouting and the call, which is the thing a coach
 * came to read, whispering and three pixels from clipping. They have swapped jobs: the name is
 * a quiet small-caps label on its own line, the call sits underneath at full width in the
 * primary colour, and neither has to fight the other for the row.
 */
.section__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* The rows are declared so the chevron can span them. `grid-row: 1 / -1` resolves `-1` against
     the EXPLICIT grid, and with no explicit rows that is line 1 — the span collapses and the
     chevron pins itself to the label's line, which it did on every section in the app. */
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: var(--space-3);
  row-gap: 2px;
  min-height: var(--control-h);
  padding: var(--space-3) var(--space-2);
  margin-inline: calc(var(--space-2) * -1);
  border-radius: var(--radius-xs);
  cursor: pointer;
  list-style: none;
  user-select: none;
  -webkit-user-select: none;
  transition: background var(--dur-fast) var(--ease-standard);
}

.section__head::-webkit-details-marker {
  display: none;
}

.section__head:active {
  background: var(--surface-hover);
}

/* The display role (base.css), its seat in the header grid, and its truncation. */
.section__name {
  grid-column: 1;
  grid-row: 1;
  color: var(--text-3);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * It has the whole width of the row, and where that still is not enough it wraps rather than
 * truncating. A value is the thing the header exists to say; "Normal · Head Up · Balanc…" in a
 * 310px docked panel is a header that has stopped saying it.
 */
.section__value {
  grid-column: 1;
  grid-row: 2;
  min-width: 0;
  font-size: var(--fs-label);
  line-height: var(--lh-label);
  color: var(--text);
  overflow-wrap: break-word;
}

.section__chevron {
  grid-column: 2;
  grid-row: 1 / -1;
  flex-shrink: 0;
  color: var(--text-3);
  transition: transform var(--dur-fast) var(--ease-standard);
}

.section[open] .section__chevron {
  transform: rotate(90deg);
}

/* The value is what the header is FOR when closed; once open the controls say it better, and
   the header collapses back to its one quiet label. */
.section[open] .section__value {
  display: none;
}

.section__body {
  padding: var(--space-1) 0 var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ── Switch ────────────────────────────────────────────────────────────────────────
   A boolean is a switch, not a two-option segmented control. `aria-checked` carries the
   state; the knob's position carries it visually, so it is legible without colour. */

.switch {
  position: relative;
  width: 51px;
  min-width: 51px;
  height: 31px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  border: var(--border-width) solid var(--hairline);
  flex-shrink: 0;
  transition: background var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard);
}

.switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 25px;
  height: 25px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: var(--elev-1);
  transition: transform var(--dur-base) var(--ease-emphasized);
}

.switch[aria-checked="true"] {
  background: var(--accent);
  border-color: var(--accent);
}

.switch[aria-checked="true"]::after {
  transform: translateX(20px);
}

/* ── Tags ──────────────────────────────────────────────────────────────────────────── */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-xs);
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.01em;
  background: var(--accent-soft);
  color: var(--accent);
}

.tag--muted {
  background: var(--surface-3);
  color: var(--text-2);
}

.tag--offense {
  background: var(--offense-soft);
  color: var(--offense);
}

.tag--defense {
  background: var(--defense-soft);
  color: var(--defense);
}

.tag--success {
  background: var(--surface-3);
  color: var(--success);
}

/*
 * The side, stated without a rectangle round it.
 *
 * `.tag` is right for a status that has to be noticed. Printing OFF on every one of twelve
 * cards in a grid is not that — it is a caption, and twelve tinted rounded rectangles down a
 * Plays tab is rule 10's "a UI full of pills looks generated". The word and the side colour
 * still carry it, so "never colour alone" holds with the fill gone.
 */
.tag--bare {
  background: none;
  padding-inline: 0;
}


/* ── Labels and helper text ────────────────────────────────────────────────────────── */

/* The display role plus a margin. The six declarations this used to restate — and that a
   section's name, a stated row's title, a screen's section head and a roster's side heading
   each restated too — now live once, in the display-role list in base.css, which is what the
   comment that used to sit here was asking for. */
.section-label {
  color: var(--text-3);
  margin: var(--space-1) 0 0;
}

/* Inside a stack the heading owns the space above it, so the stack's own gap does not double
   up with the margin and leave a group floating half an inch from the one before it. */
.stack > section + section > .section-label {
  margin-top: var(--space-3);
}

/* A short list of consequences under a heading. It used to carry its indent as an inline style
   at every call site. */
/* One change that needs a decision, in a sheet that already has a boundary of its own. */
.conflict-row + .conflict-row {
  border-top: var(--border-width) solid var(--hairline);
  padding-top: var(--space-4);
}

.bullets {
  margin: 0;
  padding-left: var(--space-5);
}

.bullets li + li {
  margin-top: var(--space-1);
}

/* A hint that takes the rest of the row it sits in. */
.form-row__grow {
  flex: 1;
  min-width: 0;
}

/* A share code is read out loud and typed in from hearing it, so every character has to be
   distinguishable on its own rather than from the shape of the word around it: a monospaced
   face, tracked out, in capitals. It keeps the field size — the 18px it used to carry was not
   on the scale, and the face is what does the work. */
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.field-label {
  display: block;
  font-size: var(--fs-secondary);
  line-height: var(--lh-secondary);
  font-weight: var(--fw-medium);
  color: var(--text-2);
  margin-bottom: var(--space-1);
}

/* Beside its field rather than above it: the row's gap is already the space. After the base
   rule, not before — one class each, so the later one is the one that lands. */
.field-label--inline {
  margin-bottom: 0;
}

/*
 * Helper text. It carries its own zero margin because it is nearly always the last thing in a
 * stack whose gap has already spaced it — every call site used to pass `style="margin: 0"` to
 * cancel the paragraph default, which is a hundred inline styles saying one thing.
 *
 * Use it for what a coach cannot work out from the control: what a change will NOT affect, who
 * a change will not reach, what is hidden rather than deleted. Not for narrating the control's
 * own label.
 */
.hint {
  font-size: var(--fs-secondary);
  line-height: var(--lh-secondary);
  color: var(--text-2);
  margin: 0;
}

/* A hint that ends its row rather than starting one, and a hint that is a failure. Both sit
   after `.hint` on purpose: they are single classes, so ahead of it they would lose. */
.hint--end {
  text-align: right;
}

.hint--danger {
  color: var(--danger);
}

/* A quiet heading inside a list of players: which side, and how many of the format's slots are
   filled. Never colour alone — it says "Offense" or "Defense" in words as well. */
.roster-side {
  margin-top: var(--space-2);
}

.roster-side[data-side="offense"] {
  color: var(--offense);
}

.roster-side[data-side="defense"] {
  color: var(--defense);
}

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

.form-row > * {
  flex: 1;
  min-width: 0;
}

.form-row > input[type="color"],
.form-row > .btn--icon,
.form-row > .switch,
.form-row > canvas,
.form-row > .logo-box {
  flex: 0 0 auto;
}

/* ── Segmented control ─────────────────────────────────────────────────────────────
   For two to four exclusive choices, and only that. A longer option set goes in a menu,
   a list or a sheet — cramming eight into this shape is what was clipping labels. */

.segmented {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  border: var(--border-width) solid var(--hairline);
}

.segmented > button {
  flex: 1;
  min-width: 0;
  min-height: 36px;
  border-radius: var(--radius-xs);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-secondary);
  color: var(--text-2);
  padding: 0 var(--space-2);
  /* Ellipsis rather than overflow: a label too long for the space is cut visibly, and its
     full text stays reachable as the button's accessible name. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.segmented > button[aria-pressed="true"] {
  background: var(--surface);
  color: var(--text);
  font-weight: var(--fw-bold);
  box-shadow: var(--elev-1);
}

/* ── Chips ─────────────────────────────────────────────────────────────────────────── */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/*
 * A chip is a pill once it is CHOSEN. At rest it is an outline.
 *
 * The resting `--surface-3` fill is what made five assignment options read as five grey pills,
 * and the call sheet's play library as forty of them — a wall of tinted rectangles where the
 * only thing that mattered was which one was picked. An outline says "this is a choice"; the
 * fill is spent on saying which choice was made.
 */
.chip {
  min-height: 36px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  background: transparent;
  border: var(--border-width) solid var(--hairline-strong);
  color: var(--text);
  font-weight: var(--fw-medium);
  font-size: var(--fs-secondary);
  transition: background var(--dur-fast) var(--ease-standard);
}

.chip[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
  font-weight: var(--fw-semibold);
}

/* ── Pickers ───────────────────────────────────────────────────────────────────────── */

.shape-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-2);
}

.shape-grid button {
  min-height: 52px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: var(--border-width) solid var(--hairline);
  display: flex;
  align-items: center;
  justify-content: center;
}

.shape-grid button[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.theme-row {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-bottom: var(--space-1);
  -webkit-overflow-scrolling: touch;
}

.theme-chip {
  flex-shrink: 0;
  width: 88px;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  border: 2px solid transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-2);
}

.theme-chip[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}

.theme-chip__swatch {
  width: 64px;
  height: 40px;
  border-radius: var(--radius-xs);
  position: relative;
  overflow: hidden;
}

/* "The playbook's own": the absence of a choice, struck through the way a null colour is. */
.theme-chip__swatch--inherit {
  background: var(--surface-3);
  border: var(--border-width) dashed var(--hairline-strong);
}

.theme-chip__swatch--inherit::after {
  display: none;
}

.theme-chip__swatch::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 3px;
  background: var(--los);
}

.logo-box {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-weight: var(--fw-bold);
  font-size: var(--fs-title);
  flex-shrink: 0;
  border: 2px solid transparent;
}

.logo-box img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-1);
}

/* ── Bottom sheet ──────────────────────────────────────────────────────────────────
   Detents rather than one near-full-screen height: a sheet that only needs to offer four
   things should not cover the field to do it. `data-detent` on the element picks one. */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 40;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
}

.sheet-backdrop.is-open {
  opacity: 1;
}

.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 41;
  max-height: var(--sheet-detent, min(72dvh, calc(100dvh - var(--safe-top) - var(--space-6))));
  background: var(--surface-2);
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  box-shadow: var(--elev-3);
  transform: translateY(100%);
  transition: transform var(--dur-slow) var(--ease-emphasized);
  display: flex;
  flex-direction: column;
  padding-bottom: var(--safe-bottom);
}

.sheet.is-open {
  transform: translateY(0);
}

/* Compact: a handful of rows. Medium: a form. Full: a long list that has to scroll. */
.sheet[data-detent="compact"] {
  --sheet-detent: min(42dvh, calc(100dvh - var(--safe-top) - var(--space-6)));
}

.sheet[data-detent="medium"] {
  --sheet-detent: min(62dvh, calc(100dvh - var(--safe-top) - var(--space-6)));
}

.sheet[data-detent="full"] {
  --sheet-detent: calc(100dvh - var(--safe-top) - var(--space-6));
}

.sheet__grip {
  width: 36px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--hairline-strong);
  margin: var(--space-2) auto 0;
  flex-shrink: 0;
}

.sheet__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  flex-shrink: 0;
}

.sheet__title {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  font-weight: var(--fw-bold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sheet__body {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 0 var(--space-4) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

@media (min-width: 900px) {
  .sheet {
    left: 50%;
    right: auto;
    bottom: var(--space-6);
    width: 480px;
    transform: translate(-50%, 120%);
    border-radius: var(--radius-xl);
  }

  .sheet.is-open {
    transform: translate(-50%, 0);
  }
}

/* ── Toast ─────────────────────────────────────────────────────────────────────────── */

.toast-host {
  position: fixed;
  left: 50%;
  bottom: calc(96px + var(--safe-bottom));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
  max-width: calc(100vw - var(--space-8));
}

.toast {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  border: var(--border-width) solid var(--hairline);
  color: var(--text);
  font-weight: var(--fw-medium);
  font-size: var(--fs-secondary);
  box-shadow: var(--elev-3);
  animation: toast-in var(--dur-base) var(--ease-standard);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* ── App brand mark ────────────────────────────────────────────────────────────────── */

/* The product's logo. The artwork is finished — its own glass rim, its own highlights, its own
   corners — so this adds nothing to it: no radius, no border, no drop shadow. `contain` on a
   box that is square for square artwork is a no-op today and the guarantee that the coach, the
   clipboard and the rim are never cropped or stretched if either ever stops being square. */
.app-logo {
  display: block;
  flex-shrink: 0;
  object-fit: contain;
}

/* In the app bar's leading slot, which is `--control-h` wide for the back button it replaces.
   Centred in that width so the title stays optically centred between the two ends. */
.appbar__brand {
  width: var(--control-h);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Logo beside the app's name — Settings. A row, not a hero: this identifies the app, it does
   not advertise it. */
.app-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-inline: var(--space-1);
}

.app-brand__name {
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  font-weight: var(--fw-bold);
  color: var(--text);
}

/* A div rather than a <p>: `p` carries a bottom margin from base.css, which would push the
   text block off the logo's centre line. */
.app-brand__tagline {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--text-2);
  max-width: 34ch;
  margin-top: 2px;
}

/* Quieter than the tagline: it is a fact to look up, not something to read. */
.app-brand__version {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  color: var(--text-3, var(--text-2));
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}

/* ── Empty states ──────────────────────────────────────────────────────────────────── */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-8) var(--space-6);
  color: var(--text-2);
}

.empty h2 {
  color: var(--text);
}

.empty p {
  max-width: 42ch;
}

/* An outlined mark, not a 44px emoji — and not a tinted rounded rectangle behind a glyph
   either, which is what the --surface-3 fill was: one more near-identical pale-grey box. */
.empty__icon {
  width: 56px;
  height: 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-3);
  margin-bottom: var(--space-1);
}

.empty__icon .ico {
  width: 28px;
  height: 28px;
}

/* The welcome screen's logo takes the glyph's place at its own size. Wider bottom margin than
   `.empty__icon` because the artwork carries to its own edge where the outlined mark did not. */
.empty__brand {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-2);
}

.empty__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
  max-width: 280px;
  margin-top: var(--space-2);
}

/*
 * The front door. Centred rather than top-aligned, because it is the whole screen rather than
 * an empty state inside one.
 *
 * `--safe-top` is not optional here: it is applied in exactly two places in the app, the app
 * bar and the nav rail, and this is the only route that has neither. Without it the logo sits
 * under the status bar on a notched iPhone.
 */
.welcome {
  min-height: 100%;
  justify-content: center;
  padding-block-start: calc(var(--space-8) + var(--safe-top));
}

.welcome__tagline {
  color: var(--text-2);
  margin: 0;
}

/* ── Hero / tabs / menu ────────────────────────────────────────────────────────────── */

.hero {
  padding: var(--space-4) var(--gutter) var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

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

/*
 * The hero's heading keeps the screen-title role rather than overriding it down to title size.
 *
 * It used to be `--fs-title`, which is exactly the size and weight of `.appbar__title` two rows
 * above it printing the same string — so the identity of the playbook was stated twice, at the
 * same volume, and the screen had no anchor. Two lines before it gives up, because a team name
 * at 26px bold runs out of a phone's width after about twenty characters and "Northside Junior
 * Wildca…" is a worse heading than the name over two lines.
 */
.hero h1 {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.hero__meta {
  font-size: var(--fs-secondary);
  line-height: var(--lh-secondary);
  color: var(--text-2);
  margin-top: var(--space-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Underline tabs: a section switcher inside a screen, distinct from a segmented control,
   which picks a value. Scrolls horizontally rather than squeezing five labels into 320px. */
.tabs {
  display: flex;
  gap: var(--space-1);
  padding: 0 var(--gutter);
  border-bottom: var(--border-width) solid var(--hairline);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  flex-shrink: 0;
  /* Centred while they fit — four short labels pinned left left a phone-width gap on the
     right that read as a missing fifth tab. `safe` is doing the real work: it drops back to
     flex-start the moment the labels overflow, so a long set still starts at the leading edge
     instead of overflowing equally off both ends with the first one unreachable. A browser
     that cannot parse it discards the line and keeps today's left-aligned strip. */
  justify-content: safe center;
}

.tabs::-webkit-scrollbar {
  display: none;
}

.tabs > button {
  flex: 0 0 auto;
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-body);
  color: var(--text-2);
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
}

.tabs > button[aria-selected="true"] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* The layer that catches the tap that closes a menu. One below the menu itself, above
   everything else on the screen. */
.menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 49;
}

.menu {
  position: fixed;
  z-index: 50;
  min-width: 232px;
  /* A menu never grows past the screen. The editor's menu gains an entry every time the app
     does; without this the last of them simply fall off a phone. */
  max-height: calc(100dvh - var(--space-4));
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--surface-2);
  border: var(--border-width) solid var(--hairline);
  border-radius: var(--radius-md);
  box-shadow: var(--elev-3);
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
  animation: menu-in var(--dur-fast) var(--ease-standard);
}

@keyframes menu-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

.menu button {
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  text-align: left;
  font-weight: var(--fw-medium);
  font-size: var(--fs-body);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.menu button:active {
  background: var(--surface-hover);
}

.menu button.is-danger {
  color: var(--danger);
}

/* A menu item's trailing mark: the tick on the chosen entry of a group. */
.menu button .menu__check {
  margin-left: auto;
  color: var(--accent);
  flex-shrink: 0;
}

.menu hr {
  border: 0;
  border-top: var(--border-width) solid var(--hairline);
  margin: var(--space-1) 0;
}

/* ── Zone presets, swatches, option routes ─────────────────────────────────────────── */

/*
 * The zone presets: eight quick calls, as chips.
 *
 * They were eight outlined 42px rectangles in a two-column grid — about 190 pixels of boxes,
 * permanently open, for one value. A preset IS a legitimate quick call, which is what a chip is
 * for, and tapping one draws the zone immediately; what it never needed was a rectangle each.
 */
.zone-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.zone-grid .btn {
  flex: 0 1 auto;
  min-height: 36px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  background: transparent;
  border-color: var(--hairline-strong);
  font-size: var(--fs-secondary);
  font-weight: var(--fw-medium);
}

/* Two classes beat one, so the selected preset has to say so here or the outline and the fill
   above win — and the outline is the half that was still coming through, leaving the chosen
   preset in a grey hairline while every other selected pill in the app has an accent one. The
   weight is `.btn--active`'s, so a selected preset and a selected chip read the same. */
.zone-grid .btn--active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* Swatches wrap rather than scroll: a colour you cannot see is a colour you will not pick,
   and ten of them fit two rows even on a 320px phone. */
.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.swatch {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-pill);
  border: 2px solid var(--hairline-strong);
  flex: 0 0 auto;
  position: relative;
}

.swatch[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-line);
}

.swatch--theme {
  background: var(--surface-3);
}

/* The "no colour of its own" swatch is struck through, the way a null colour usually is. */
.swatch__slash {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(to bottom right, transparent calc(50% - 1.5px), var(--text-2) calc(50% - 1.5px), var(--text-2) calc(50% + 1.5px), transparent calc(50% + 1.5px));
}

.option-presets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* The same shape as every other quick preset: sized to its label, not to an equal share of the
   row, and a pill at the one height the app's choice pills use. `flex: 1 1 auto` is the rule the
   pass removed from every other wrapping row of names — with one preset it drew a full-width
   slab and with two it drew halves. */
.option-presets .btn {
  flex: 0 1 auto;
  min-height: 36px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--fs-secondary);
}

/* One branch of a read. Three of them used to be three tinted, bordered cards inside a
   disclosure that is already a container — a list item is not an object. */
.option-row {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-2) 0;
}

.option-row + .option-row {
  border-top: var(--border-width) solid var(--hairline);
}

.option-row__name {
  font-weight: var(--fw-semibold);
}

/* ── Share code ────────────────────────────────────────────────────────────────────
   Read out loud off a phone held at arm's length: large, tabular, letter-spaced — `8` and
   `B` have to be tellable apart across a gym. Selectable, and it wraps rather than
   overflowing on a 320px screen. */

.share-code {
  display: block;
  width: 100%;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: clamp(20px, 7vw, 30px);
  font-weight: var(--fw-bold);
  letter-spacing: 0.1em;
  text-align: center;
  padding: var(--space-4) var(--space-3);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius-md);
  background: var(--surface-3);
  color: var(--text);
  cursor: pointer;
  overflow-wrap: anywhere;
  user-select: all;
  -webkit-user-select: all;
}

/* An exclusive choice laid out as wrapping chips, for option sets a segmented control cannot
   hold. Every chip keeps its whole word and its own target, at the one chip height. */
.chips--choice .chip {
  flex: 0 1 auto;
}

/* A control sitting in a settings row: it takes the right-hand side rather than the row's
   whole width, and stacks under the label when the label needs the room. */
.setting-control {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 60%;
}

.group-row--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
  padding-block: var(--space-3);
}

.group-row--stacked .setting-control {
  max-width: 100%;
}

/* ---- Position groups -----------------------------------------------------------------
 *
 * The defensive call, one group at a time. Three pieces, and only three: a selector across the
 * top, a heading that STATES the call, and the disclosure sections underneath — which are the
 * app's ordinary <details> sections and need nothing of their own.
 */

/* A dot on the chip of a group that has been given a call. It is the only mark on the selector,
 * and it exists because "which of these seven have I already done something to" is otherwise a
 * question a coach can only answer by tapping all seven. */
.chip__dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-inline-start: var(--space-2);
  border-radius: 50%;
  background: var(--accent);
  vertical-align: middle;
}

/* The heading: who this group is and what it is playing. The call is at reading size rather
 * than caption size because it is the answer to the question the panel was opened with — "Over
 * · Slant Weak · TEX Strong" is the sentence, and the group's name above it is the label. */
.pgroup-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding-block: var(--space-2);
}

.pgroup-head__name {
  color: var(--text-3);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
}

.pgroup-head__call {
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
  color: var(--text);
}

/* A heading inside a menu — "Stunts & twists", "Pressures". Not a button: there is nothing to
 * choose here, and a disabled-looking row that can be tapped is worse than a label. */
.menu__heading {
  padding: var(--space-2) var(--space-3) var(--space-1);
  color: var(--text-3);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.04em;
}

/* Sign in with a provider. A neutral button rather than a branded slab: it sits in a sheet
   beside the app's own controls, and a coloured block would read as the primary action of the
   whole screen rather than one of two ways in. The mark carries the brand. */
.btn--provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* "or use your email" — a rule with the words sitting in it, so the two routes read as
   alternatives rather than as a form with a button stuck on top. */
.auth-or {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-2);
  font-size: var(--fs-sm);
}

.auth-or::before,
.auth-or::after {
  content: '';
  flex: 1 1 auto;
  height: var(--border-width);
  background: var(--hairline);
}
