/* Sobati Fitness — base styles.
   Direction-agnostic by construction: every inline offset uses CSS logical
   properties (margin-inline, padding-inline, text-align: start), so the FA/RTL
   layout is the same stylesheet flipped by <html dir>, not a second theme. */

/* Design tokens taken from the Claude Design project "Sobati Fitness App
   Design". Change them here, never inline in a template — the whole UI is
   meant to restyle from this block. */
:root {
  /*
   * The app is dark on every screen, and this is the one line that tells the
   * browser so (audit F4). Without it the UA paints its LIGHT chrome on the
   * controls this stylesheet does not restyle: the scrollbars, and every
   * native picker — `<input type="date">` on the daily report and the photo
   * sitting form, `<input type="file">` on payment, the form check and the
   * photo uploader. A white date picker on a dark card is not a theme; it is
   * what a page looks like when it never said which theme it is.
   */
  color-scheme: dark;

  /* The training bar's height, named because two other rules subtract it and a
     mismatch shows up as a strip of dead space or a covered control. */
  /* The bar's RESERVED height. It has to be the tallest the bar can be, not
     the shortest: the locked chat tab carries a VIP tag under its label, so a
     bar drawn for a non-VIP client is taller than one drawn for a VIP, and the
     spacer reserved the short one. `min-height` on .tnav pins both cases to
     this, so the reservation is right for everybody. */
  --tnav-h: 68px;

  /* The app background, taken from the design frame's own `background`.
     #08090b was the preview shell OUTSIDE that frame — using it made every
     page read a shade too dark and too flat. */
  --bg: #0f1115;          /* page */
  --shell: #08090b;       /* outside the app shell, on very wide screens */
  --panel: #12151a;       /* sidebars, sticky bars */
  --surface: #171a21;     /* cards, rows */
  --surface-2: #1f242d;   /* raised controls */
  --text: #e8eaed;
  --secondary: #9aa3af;   /* body text on a card */
  --muted: #5f6875;       /* labels, timestamps, placeholders */
  --accent: #d4a13a;      /* brand gold */
  --accent-hover: #e6bd63;
  --accent-ink: #1a1204;  /* text on an accent fill */
  --success: #7ec98f;
  --success-bg: #13251a;
  --success-border: #2c4a36;
  /* The gold family at low intensity — chips, highlighted cards, secondary
     buttons, the print eyebrow. Taken from the design, which uses these
     three consistently; they were missing here and had been approximated. */
  --accent-bg: #1f1a10;
  --accent-border: #4a3a15;
  --accent-dim: #8a6b1f;      /* uppercase eyebrow labels */
  /* The top stop of the VIP card's gradient, from the design's packages
     screen. The bottom stop is --surface, so the card fades into an ordinary
     one — which is the whole effect: warmer at the top, same ground below. */
  --accent-wash: #201a11;
  --nav-link: #c9ced6;        /* nav text: brighter than --secondary */

  /* DERIVED, not from the design — the design defines no error state at all.
     Built with the same construction it uses for success and accent (a
     saturated text colour, a very dark tinted fill, a mid-dark tinted
     border), at the same saturation and lightness, on a red hue chosen to sit
     beside the gold rather than fight it. Replaces three values that had been
     picked by eye. */
  --danger: #c9887e;
  --danger-bg: #251513;
  --danger-border: #49302c;
  --danger-strong: #e5534b;   /* kept for destructive button text only */

  /* The third state, between danger and fine (backlog §20.1).

     The progress report needs to draw a missed day and an off-plan day as
     different things (owner, 2026-08-24: missed red, off-plan yellow), and
     there was nothing between --danger and --accent to say it with. --accent
     was not reusable: it is the brand gold and it already means "starred lift"
     on the records screen, so borrowing it would make one colour say two
     things on two screens.

     Built the same way as the danger trio directly above — desaturated text
     colour, very dark tinted fill, mid-dark tinted border — on an amber hue
     sitting between the gold and the red rather than beside either. This
     palette has no shouting colour in it and this is not going to be the
     first. */
  --warn: #c2a166;
  --warn-bg: #221c11;
  --warn-border: #453820;

  /* INFO — the blue of the Needs-you list's SCHEDULED touchpoints (the
     owner's colour scheme, 2026-09-28: red money and messages, amber
     inactivity, blue scheduled, green good news). The same trio shape as the
     three above; a muted steel blue, so a list of check-ins due does not
     read louder than a payment waiting. */
  --info: #8fb3de;
  --info-bg: #111a26;
  --info-border: #263a52;

  /* THE THREE METALS (backlog §14, the owner 2026-09-01). A trio each, built
     exactly the way --success, --danger and --warn above are built: a
     saturated mark colour, a very dark tinted fill, a mid-dark tinted border.
     A cup badge therefore always sits on a ground designed for it rather than
     borrowing whatever card it lands on, which is what keeps it legible on
     --surface, on --panel and inside the block strip alike.

     The app is dark on every screen — `color-scheme: dark` at the top of this
     block is the whole theme story — and there is exactly one other: the print
     layer scoped to `.page-block-report`, whose cup line prints the badge on
     its own dark ground (it is a self-contained chip, so it stays legible).
     If a light context ever needs a light cup, this is the block that gains
     its stops; nothing else names a metal anywhere.

     GOLD IS THE BRAND GOLD, deliberately, at the accent family's brighter
     stop. This is a gold-branded business and a fourth colour for its top
     award would be a colour that means nothing. It is its own token rather
     than a reuse of --accent because --accent already means "the plan" and "a
     starred lift", and a cup should be tunable without moving every button in
     the app. */
  --cup-bronze: #cd8b5c;
  --cup-bronze-bg: #241a13;
  --cup-bronze-border: #4a3423;
  --cup-silver: #c8cfda;
  --cup-silver-bg: #1b1f26;
  --cup-silver-border: #39414d;
  --cup-gold: #e8bd55;
  --cup-gold-bg: #241d10;
  --cup-gold-border: #4f3f18;
  --border: #21262f;      /* hairlines between sections */
  --border-strong: #2a303b; /* card outlines, inputs */

  /* Chart furniture, taken from the design's isRecords screen. All three were
     approximated from existing tokens the first time and all three were wrong:
     a gridline drawn at --border is as bright as the axis it sits under, and a
     bar drawn at --border-strong disappears into the card outline behind it. */
  --grid-line: #1a1f27;   /* dashed chart guides — dimmer than --border */

  /* Gym mode, from the round-4 designs. Its own darker ground: the session
     screen is used at arm's length in a bright room, and the extra contrast is
     what keeps the set buttons readable there. */
  --gym-bg: #0a0c0f;
  --gym-card-top: #1c202a;    /* the resume card's gradient, top */
  --gym-live-top: #241d10;    /* the SAME card when a session is open */
  --gym-live-bot: #1a1710;
  --gym-live-line: #a8874a;   /* text on the live card */
  --gym-live-text: #cbb489;
  --gym-live-track: #3a2f16;
  --gym-done-bg: #101a14;     /* a finished week */
  --gym-done-text: #8fd6a1;
  --gym-done-sub: #6e7d72;
  --gym-done-pip: #57a06d;
  --gym-pip: #242a33;         /* a session not yet done */
  --gym-input-line: #333a45;

  /* Team dashboard, from the design's isTeam screen. Four values the palette
     had no equivalent for, so they are tokens rather than approximations. */
  --team-deep: #0c0e12;   /* the working area, a step below the page */
  --team-hair: #1c212a;   /* divider between queue items — dimmer than --border */
  --team-vip-bg: #15130d; /* the "VIP first" band */
  --tier-text: #7d848f;   /* the STANDARD pill's text */
  --bar-muted: #3d4757;   /* a bar that is not the highlighted one */
  --bar-prev: #2f3742;    /* the earlier of two compared bars */
  /* Cancelled by full-bleed bands (auth frame, program hero). */
  --page-pad: 1.5rem;
  --radius: 10px;
  --radius-lg: 14px;

  /* Archivo for Latin, Peyda for Farsi — both self-hosted (fonts.css).
     Each stack keeps the other family as a fallback so a mixed-script run
     still renders in a real webfont rather than dropping to system UI. */
  --font-en: "Archivo", "Peyda", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-fa: "Peyda", "Archivo", Tahoma, system-ui, sans-serif;
}

* { box-sizing: border-box; }

/* ------------------------------------------------------- bidirectional text
   Farsi and English mix constantly here: exercise names, meal items, supplement
   names and client names all routinely appear in the other script. Three rules
   keep that readable.

   1. <bdi> isolates an embedded run so neutral characters (commas, colons,
      parens, digits) resolve against the *paragraph* direction, not the run's.
      Without it "Welcome, {Farsi name}" renders the comma at the far end.
   2. Free-text blocks get unicode-bidi: plaintext, which resolves direction
      per paragraph from its first strong character — so a Farsi note inside an
      English program (or the reverse) aligns to its own side.
   3. Latin-only data (email, numbers, dates, URLs) is pinned ltr so it never
      gets dragged around by a surrounding RTL paragraph. */

bdi, .bidi-isolate {
  unicode-bidi: isolate;
}

.freetext {
  unicode-bidi: plaintext;
  text-align: start;
}

.ltr-data {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}

/* Inputs carrying dir="auto" must align to the direction they resolved to,
   not the page's — otherwise Farsi typed into an LTR page sits left. */
input[dir="auto"], textarea[dir="auto"] { text-align: start; }

/* A box whose HINT is Farsi prose but whose VALUE is a number.
   Gym mode's set fields are one: they keep dir="ltr" so a typed `10-5-5` cannot
   flip, while a cardio row's placeholder is the coach's sentence — «۲۰ دقیقه با
   شیب ۵». public/bidi.js isolates the hint so its WORDS come out in the right
   order; this puts them at the edge a Farsi reader starts from. Only while the
   hint is showing: the moment the client types, the box is a number again and
   aligns like every other one. Follows the HINT and not the page, because an
   English-browsing client can be reading a Farsi program. */
input.gym-hint-rtl:placeholder-shown { text-align: right; }
textarea { unicode-bidi: plaintext; }

/*
 * `hidden` MEANS HIDDEN.
 *
 * The attribute only works because of a UA rule — `[hidden] { display: none }`
 * — which any author `display` declaration outranks. So a component styled
 * `display: flex` and marked `hidden` in the template is simply visible, and
 * nothing anywhere says so.
 *
 * It had bitten three components: the "opening Telegram…" spinner announced
 * itself on the login AND signup screens to people who had not touched the
 * Telegram button, and the "Ask for feedback" button appeared for clients with
 * no JavaScript to make it do anything.
 *
 * Three OTHER components had already hit it and been patched one at a time —
 * `.install-hint[hidden]`, `.ask-bar[hidden]`, `.gym-fc-pos[hidden]`. Three
 * local fixes for one general problem is the sign that the fix belongs here
 * instead. Those rules are now redundant and harmless; they are left as the
 * record of how long this took to spot.
 *
 * !important is right here and almost nowhere else: this is not competing with
 * a design decision, it is restoring the meaning of an HTML attribute. Anything
 * that wants to be visible should not be marked hidden.
 */
[hidden] { display: none !important; }

/*
 * No double-tap zoom wait (audit F5). Every tap on iOS carried the ~300ms
 * pause while the browser waited to see whether a second one was coming —
 * worst in gym mode, where logging one set is several taps in a row with the
 * phone in one hand.
 *
 * On the CONTROLS rather than on `body`: `manipulation` keeps panning and
 * pinch-zoom, so it is safe here either way, but naming the interactive
 * elements means no scrolling surface can ever inherit something narrower by
 * accident. `summary` is in the list because this app's disclosures — the
 * mobile nav, the block strip on the gym lobby — are real `<details>`.
 */
button,
a,
label,
summary,
[role="button"] { touch-action: manipulation; }

html { font-family: var(--font-en); }
html[dir="rtl"] { font-family: var(--font-fa); }

/* A Farsi page still renders Latin runs, and vice versa — so both stacks need
   to be reachable regardless of page direction. */
html[dir="rtl"] .ltr-data { font-family: var(--font-en); }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
}

/* ------------------------------------------------------------------ header */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.5rem;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--accent);
  text-decoration: none;
}

.brand img {
  /* Fixed box so a slow font swap or a wide Farsi brand name can't reflow it. */
  flex: none;
  width: 30px;
  height: 30px;
  object-fit: contain;
}

.site-header nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.site-header nav a { color: var(--text); text-decoration: none; font-size: .95rem; }
.site-header nav a:hover { color: var(--accent); }

/*
 * Sign out is a BUTTON, and it has to look like the links beside it.
 *
 * `.site-header nav a` sets the nav's type size; the sign-out control is a
 * <button class="linklike"> inside a <form>, so it matched neither selector
 * and fell back to `font: inherit` — rendering a step larger than every link
 * next to it. Most visible in gym mode, where the header sits on the dark
 * ground with few items to compare against.
 *
 * The wrapping form becomes a flex item in its own right too: `display: inline`
 * left the button riding at a different baseline from its siblings.
 */
.site-header nav form.inline { display: flex; align-items: center; }
.site-header nav .linklike { font-size: .95rem; line-height: inherit; }

.lang-toggle {
  display: inline-flex;
  gap: .25rem;
  padding: .15rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}

.lang {
  padding: .15rem .6rem;
  border-radius: 999px;
  font-size: .8rem;
  letter-spacing: .04em;
  text-decoration: none;
  color: var(--muted);
}

.lang.current { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* -------------------------------------------------------------------- main */

main {
  flex: 1;
  width: 100%;
  max-width: 60rem;
  margin-inline: auto;
  padding: 2rem 1.5rem;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.75rem;
}

.card.narrow { max-width: 26rem; margin-inline: auto; }

h1 { margin-block-start: 0; font-size: 1.5rem; }
.lede { font-size: 1.1rem; color: var(--muted); }
.muted { color: var(--muted); font-size: .9rem; }

/*
 * The client's All Programs list — the team dashboard's program card, worn
 * client-side. The facts row and the day list follow the team card's rhythm;
 * what a client should not carry (status chips, revision counters, test tags)
 * simply is not here.
 */
.prog-facts {
  margin-block-start: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  font-size: 12.5px;
  color: var(--secondary);
}

.prog-days {
  margin-block-start: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  font-size: 13px;
  color: var(--secondary);
}
.prog-day { display: inline-flex; align-items: center; gap: 6px; }

/*
 * The day-number pill: the app's small gold-transparent chip, at its smallest.
 * A NUMBER IN A PILL and the muscles in plain text, not the other way round —
 * the pill is the index, the words are the content, and a row of gold words
 * would shout down the price of everything else on the card.
 */
.prog-day-pill {
  padding: 1px 7px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
}

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

label {
  display: block;
  margin-block: 1rem .35rem;
  font-size: .875rem;
  color: var(--muted);
  text-align: start;
}

input, select, textarea {
  width: 100%;
  padding: .6rem .75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  text-align: start;
}

/*
 * A RADIO IS NOT A TEXT BOX. The rule above is written for something you type
 * into: full width, a padded box, a filled background. A checkbox and a radio
 * are glyphs the browser draws at about 13px, and every one of those
 * declarations is wrong for them — the fifth time in this file's history that
 * a broad element rule has reached a component nobody wrote it for
 * (docs/decisions.md keeps the tally).
 *
 * What it looked like, measured at 390px on the regeneration tab's run-kind
 * options before this rule existed: the radio 324px wide inside a 346px
 * option box, which left the label nowhere to go — it was laid out at x=367
 * with a width of 0, i.e. outside the box entirely — plus 16px of top margin
 * inherited from the bare `label` rule. The owner's report, 2026-09-01.
 *
 * Component rules that size their own boxes are unaffected: the tab radios
 * (`.tabs > input[type="radio"]`) and the assign-row checkboxes are (0,2,1) and
 * come later in the file. Every checkbox and radio in the app was measured on
 * both sides of this change and only the run-kind options moved.
 */
input[type="radio"], input[type="checkbox"] {
  width: auto;
  padding: 0;
  background: none;
}

/*
 * A HIDDEN control keeps the 1px box `.visually-hidden` gives it. That class is
 * (0,1,0) and the rule above is (0,1,1), so it loses — the review pane's fold
 * flag measured 13x1 rather than 1x1. Harmless (absolutely positioned, clipped,
 * 1px tall) but wrong, and found by the security pass on this branch.
 *
 * Restored on the exact pair rather than with `:not(.visually-hidden)` above,
 * which was tried first and is the mistake this file keeps making: `:not()`
 * raises the rule to (0,2,1), and it then beat the single-class rules that size
 * the permissions grant cell (20x20 -> 13x20), the program ask tick (22 -> 13),
 * the deload week (18 -> 13) and both hidden pickers. Measured, reverted.
 */
input[type="radio"].visually-hidden, input[type="checkbox"].visually-hidden {
  width: 1px;
}

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/*
 * NO RING AROUND A WHOLE SCREEN.
 *
 * swap.js moves focus into the region it just replaced — that is what makes a
 * screen reader announce the new content and the keyboard land in it rather
 * than back at the top of the document. But it does that by giving the region
 * `tabindex="-1"`, and a programmatically-focused element still matches
 * `:focus`, so the browser drew its own default ring around the entire gym
 * screen or the entire package list. A blue rectangle around everything, for a
 * moment, every time a unit or a week was tapped.
 *
 * Safe to remove precisely because the value is -1: this element is not in the
 * tab order and can never be reached by a keyboard user, so no one ever needed
 * the ring to know where they are. The focus MOVE is the accessible behaviour;
 * the outline was only ever the browser guessing.
 */
[data-swap-region]:focus { outline: none; }

.btn {
  display: inline-block;
  margin-block-start: 1.25rem;
  padding: .6rem 1.2rem;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.btn:hover { filter: brightness(1.08); }

/*
 * A REFUSED BUTTON HAS TO LOOK REFUSED.
 *
 * There was no `.btn:disabled` rule anywhere in this file, so a disabled
 * Deliver computed to the SAME four values as a working one — the gold ground,
 * `opacity: 1`, `cursor: pointer`, the same border. decisions.md, 2026-09-01:
 * "a disabled button is a hint; the route is where the program either becomes
 * the client's or does not." The route half is real and tested. The hint was
 * never drawn, so a coach whose delivery is refused for an unresolved name
 * pressed a gold button and nothing happened, with no reason on screen.
 * (The design pass on slice ten, 2026-09-04, ranked this first of eight.)
 *
 * DIMMED INK, NOT A DIMMED BOX — the rule `.bld-act:disabled` states three
 * thousand lines down and for the same reason: `opacity` on the whole control
 * measured 2.17:1 for the glyph there and read as empty space rather than as
 * something that cannot fire. So the shape stays and the GOLD goes, because
 * gold is what says "this is the action on this screen".
 *
 * `[aria-disabled="true"]` rides along for the anchors that cannot carry a
 * real `disabled` attribute.
 */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--surface-2);
  color: var(--muted);
  border: 1px solid var(--border-strong);
  cursor: not-allowed;
  filter: none;
}
/* Hover must not resurrect it: `.btn:hover` above brightens whatever ground it
   finds, which on a disabled control is a control that answers the pointer. */
.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  background: var(--surface-2);
  color: var(--muted);
  filter: none;
}

.btn-secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
}

.linklike {
  border: 0;
  background: none;
  padding: 0;
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.linklike:hover { color: var(--accent); }

form.inline { display: inline; }

.error {
  padding: .6rem .8rem;
  border-radius: 8px;
  background: color-mix(in srgb, var(--danger) 15%, transparent);
  border: 1px solid var(--danger);
  color: var(--text);
  font-size: .9rem;
}

.divider {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-block: 1.5rem;
  color: var(--muted);
  font-size: .85rem;
}
.divider::before, .divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ------------------------------------------------------------------ footer */

.site-footer {
  padding: 1.5rem;
  text-align: center;
  color: var(--muted);
  border-block-start: 1px solid var(--border);
}

@media (max-width: 34rem) {
  .site-header { flex-direction: column; align-items: stretch; gap: .75rem; }
  .site-header nav { justify-content: space-between; }
}

/* ------------------------------------------------ add-to-home-screen hint
   Hidden until pwa.js decides the platform can install. Two variants share the
   markup; data-mode picks which one is visible, so the script toggles rather
   than builds DOM. */

.install-hint {
  position: fixed;
  inset-inline: 12px;
  bottom: 12px;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 520px;
  margin-inline: auto;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: 0 8px 28px rgb(0 0 0 / 45%);
}

.install-hint[hidden] { display: none; }

.install-hint img { flex: none; }

.install-hint-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 0.86rem;
  line-height: 1.45;
}

.install-hint-body strong { font-size: 0.92rem; }
.install-hint-body span { color: var(--secondary); }

/* Only the variant matching data-mode is shown. */
.install-hint .ios-only,
.install-hint .prompt-only { display: none; }
.install-hint[data-mode="ios"] .ios-only { display: inline; }
.install-hint[data-mode="prompt"] .prompt-only { display: inline-flex; }

/* The Share glyph is a block-level icon, not inline text, so it needs its own
   display rule — the generic .ios-only rule above is for the inline copy. */
.install-hint[data-mode="ios"] svg.ios-only { display: block; }

.share-glyph {
  flex: none;
  color: var(--accent);
  /* The icon is the instruction, so it should not be optically lighter than
     the text next to it. */
  opacity: .95;
}

.icon-btn {
  flex: none;
  /* 44px is the minimum comfortable touch target. The first version was a
     ~20px glyph at the bottom of a phone screen, directly under the thumb —
     easy to hit by accident, which is how the hint was getting dismissed by
     clients who never meant to. */
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}

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

/* An installed app has no browser chrome, so the safe-area insets are ours to
   respect — otherwise content sits under the notch and the home indicator. */
@media (display-mode: standalone) {
  body {
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
    padding-inline: env(safe-area-inset-left) env(safe-area-inset-right);
  }
}

/* ------------------------------------------------------------ package choice */

.field-group { border: 0; padding: 0; margin: 0 0 1.5rem; }
.field-group legend { padding: 0; font-size: .875rem; color: var(--muted); }
.field-group .muted { margin: .25rem 0 .6rem; }

/*
 * The delivery-language chooser on the packages screen.
 *
 * A fieldset carries no margin of its own and a legend reads as body text, so
 * with the market control removed this one collided with the lede above it:
 * three lines of unrelated text in a stack, the middle one silently a heading.
 * It is the only control before the price list now and should look like one.
 *
 * Placed AFTER .field-group deliberately — both are one class, so the later
 * rule wins, and writing it above meant the shorthand `margin` on .field-group
 * quietly reset it back to zero.
 */
.package-delivery { margin-block: 1.75rem 2rem; }

.segmented {
  display: inline-flex;
  gap: .25rem;
  padding: .2rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}

.segment {
  padding: .35rem .9rem;
  border-radius: 999px;
  font-size: .875rem;
  color: var(--secondary);
  text-decoration: none;
}
.segment.is-selected { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

/*
 * The packages screen's own rules live in the design-exact section below, as
 * one block. An earlier approximation of them lived here and the two disagreed
 * about padding, display and the card's whole structure — the later block won
 * some properties and lost others, which is how a card ends up 22px on three
 * sides and 1.1rem on the fourth.
 */

.notice { margin-block-start: 1rem; }

/* ------------------------------------------------------------ intake form
   Sidebar + form, matching the design's stepped intake. Collapses to a
   horizontal step strip on narrow screens. */

/*
 * The stepped intake, to the design file's own numbers.
 *
 * The sidebar is a COLUMN, not a floating card: one rule down its inline end,
 * no radius, no border box, --panel behind it. It read as a detached widget
 * before; in the design it is the edge of the page.
 */

.intake {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  /* No gap — the sidebar's own border is the divider, and a gap would leave it
     floating in the middle of a gutter. */
  gap: 0;
}

.intake-side {
  flex: 1 1 210px;
  max-width: 250px;
  padding: 28px 20px;
  border-inline-end: 1px solid var(--border);
  background: var(--panel);
}

.intake-eyebrow {
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}

.intake-main {
  flex: 1 1 420px;
  min-width: 280px;
  padding: 30px 26px 40px;
}

/* 3px, per the design — a hairline of progress, not a component. */
.intake-progress {
  height: 3px;
  border-radius: 2px;
  background: var(--border-strong);
  overflow: hidden;
}
.intake-progress-bar {
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
  /* Zero unless a step matches below, so an unrecognised step shows no
     progress rather than a full bar claiming the form is finished. */
  width: 0;
}
/*
 * There was a `transition: width .3s ease` here (audit F51, "NEVER animate a
 * layout property"). It is simply GONE rather than converted to a transform:
 * the width comes from `data-step`, which is rendered by the server and which
 * nothing on the client ever changes, so every step of this form is a fresh
 * document with no previous width to move from. The transition had never once
 * fired. The other two bars in this sheet are live and did get the transform.
 */

/*
 * Widths per step, because the CSP (style-src 'self') blocks inline style
 * attributes — style="width:20%" was dropped by the browser and the bar sat
 * at 100% on every step of the form.
 *
 * Written out for a five-step form. If a sixth is ever added, the bar shows 0
 * until a rule is added here, which is visible and wrong in the safe
 * direction; the intake tests assert the current count.
 */
.intake-progress-bar[data-of="5"][data-step="1"] { width: 20%; }
.intake-progress-bar[data-of="5"][data-step="2"] { width: 40%; }
.intake-progress-bar[data-of="5"][data-step="3"] { width: 60%; }
.intake-progress-bar[data-of="5"][data-step="4"] { width: 80%; }
.intake-progress-bar[data-of="5"][data-step="5"] { width: 100%; }

/* Six, since progress photos became a step of their own. */
.intake-progress-bar[data-of="6"][data-step="1"] { width: 16.7%; }
.intake-progress-bar[data-of="6"][data-step="2"] { width: 33.3%; }
.intake-progress-bar[data-of="6"][data-step="3"] { width: 50%; }
.intake-progress-bar[data-of="6"][data-step="4"] { width: 66.7%; }
.intake-progress-bar[data-of="6"][data-step="5"] { width: 83.3%; }
.intake-progress-bar[data-of="6"][data-step="6"] { width: 100%; }

.intake-main h1 {
  margin: 22px 0 0;
  font-weight: 700;
  font-size: 26px;
  letter-spacing: -.02em;
}
.intake-sub {
  margin: 8px 0 24px;
  font-weight: 400;
  font-size: 14.5px;
  color: var(--secondary);
}

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

/* Each step is a link. Every answer is already saved, so jumping back to fix
   one costs nothing — and a five-step form you cannot navigate is one people
   abandon at step four. */
.intake-step {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 10px;
  border-radius: 8px;
  text-align: start;
  text-decoration: none;
  color: var(--secondary);
  font-weight: 500;
  font-size: 13.5px;
}
.intake-step:hover { background: var(--surface-2); color: var(--text); }
.intake-step.is-current {
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
}

/* The number badge. Gold once reached — current or past — grey ahead of you,
   so the column reads as progress at a glance rather than as a menu. */
.intake-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--border-strong);
  color: var(--muted);
  font-weight: 600;
  font-size: 11px;
  /* Latin digits regardless of page language: these are positions in a list,
     and they must line up with the "step 2 of 5" the form talks about. */
  font-variant-numeric: tabular-nums;
}
.intake-step.is-current .intake-step-n,
.intake-step.is-done .intake-step-n {
  background: var(--accent);
  color: #1a1204;
}

.intake-hint {
  margin-block-start: 24px;
  padding-block-start: 16px;
  border-block-start: 1px solid var(--border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
}

/*
 * The About-you grid. The design lays the short fields out as
 * auto-fit/minmax(180px), so two or three sit per row on a desktop and they
 * stack on a phone without a breakpoint.
 *
 * Only fields that are genuinely short. A textarea, a radio group or a
 * checkbox row spans the full width — dropping a five-option chip group into a
 * 180px column wraps it into a stack and loses the row the design draws.
 */
.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-block-start: 4px;
}
/*
 * Cells align to each other, not to their own label.
 *
 * A grid row mixes a one-line label ("Current stress level") with a label that
 * wraps ("Typical sleep — hours and quality"), and each field laid out
 * independently, so the inputs beside each other sat at different heights. As
 * a flex column with the control pushed to the bottom, every input in a row
 * lands on the same line however many lines its label takes.
 */
.field-grid > .field {
  margin-block: 0;
  display: flex;
  flex-direction: column;
}
.field-grid > .field > input,
.field-grid > .field > select,
.field-grid > .field > textarea {
  margin-block-start: auto;
}

/*
 * A select in the grid fills its cell.
 *
 * The global rule sizes a select to its longest OPTION (width:auto), which is
 * right for a two-option language picker standing on its own and wrong here:
 * the country list's longest entry is "South Georgia & South Sandwich Islands",
 * and a control sized to that would push its way out of a grid column and take
 * the row's layout with it.
 */
.field-grid > .field > select {
  width: 100%;
  max-width: 100%;
}

/* The design separates each group — grid, chip row, textarea — by 22px. Set on
   the intake column only, so the tighter rhythm of the account form and the
   coach panels is left alone. */
.intake-main .field,
.intake-main .field-grid,
.intake-main .screen-row { margin-block-start: 22px; }
.intake-main .field-grid > .field { margin-block-start: 0; }
/* Inside the grid the column already sets the width. */
.field-grid input { max-width: none; }

/*
 * Screening rows: label on one side, its control on the other, each in its own
 * bordered row — the design's triage list.
 */
.screen-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 13px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
}
.screen-row > label,
.screen-row > .field-label {
  margin: 0;
  font-weight: 500;
  font-size: 14px;
  color: var(--text);
}
.screen-row input[type="text"] { max-width: 18rem; margin: 0; }

/* ----------------------------------------------------------------- fields */

.field { margin-block-end: 1.35rem; }
.field-help { margin: .2rem 0 .45rem; font-size: .82rem; line-height: 1.5; }
.req { color: var(--accent); }

.field-error {
  margin: .35rem 0 0;
  font-size: .82rem;
  color: var(--danger);
}
.field.is-invalid input,
.field.is-invalid textarea { border-color: var(--danger); }

.choices {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-block-start: .3rem;
}

/*
 * The label is a bare wrapper. ALL of the chrome lives on the inner <span>,
 * further down in the design pass.
 *
 * This used to be a pill in its own right — border, background, 999px radius —
 * while the design pass separately styled the span inside it as an 8px
 * rectangle. Both rules applied, so every radio and checkbox in the app
 * rendered as a rectangle sitting inside a pill. Two authors, two decades
 * apart in the file, neither wrong on its own.
 *
 * The rectangle is the one that matches the design (9px 16px, radius 8,
 * #1f242d, gold when selected), so the pill goes.
 */
.choice {
  display: inline-flex;
  align-items: center;
  margin: 0;
  cursor: pointer;
}

.intake-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin-block-start: 1.5rem;
  padding-block-start: 1.25rem;
  border-block-start: 1px solid var(--border);
}
.intake-actions .btn { margin-block-start: 0; }

/* ------------------------------------------------------------------ payment */

.narrow-wide { max-width: 34rem; margin-inline: auto; }

/*
 * WHAT IS OWED, AND WHERE TO SEND IT.
 *
 * Two cards, because they are two answers to one question and the app used to
 * give only the first. Grid rather than flex so both cards are the same height
 * without either being told a height — the amount card is three lines with a
 * discount and two without, and the destination card is a number plus a bank
 * name, so a fixed height would be wrong for one of them on every screen.
 *
 * `auto-fit` + a 15rem floor is the whole responsive rule: one column until
 * the card can give each side 15rem, two after. On a 375px phone the shell
 * leaves about 21rem of content, so it stacks — which is the point. Two
 * columns there would be roughly 150px each, and a sixteen-digit card number
 * broken across lines mid-group is a number that gets typed wrong.
 */
.pay-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: .75rem;
  margin: 1.25rem 0;
}
.pay-card {
  /* The design's gold wash: accent at the top fading into an ordinary card, so
     the row reads as the important part of the screen without two boxes
     shouting at each other. */
  background: linear-gradient(var(--accent-wash), var(--surface));
  border: 1px solid var(--accent-border);
  border-radius: 12px;
  padding: .9rem 1rem 1rem;
  /* A grid item defaults to min-width:auto and refuses to shrink below its
     content — the intake step-strip bug (decisions, 2026-08-16). A long
     unbroken account number would push the card past the phone's width. */
  min-width: 0;
}
.pay-card-title {
  margin: 0;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-dim);
}
.pay-to { display: flex; flex-direction: column; align-items: start; gap: .35rem; }
.pay-number {
  margin: .3rem 0 0;
  /* Monospace, per the design: a card number is checked digit against digit
     against a banking app, and proportional figures make that harder. */
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--text);
  /* Digits in groups of four are read and typed as groups; break between them
     rather than inside one, and never overflow the card. */
  overflow-wrap: anywhere;
}
.pay-holder { margin: 0; font-size: .85rem; }
.pay-to .btn { margin-block-start: .35rem; }

.amount { margin: 1.25rem 0; }
.amount > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: .5rem;
}
.amount dt, .amount dd { margin: 0; }
.amount dt { color: var(--secondary); }
.amount code {
  padding: .1rem .4rem;
  border-radius: 5px;
  background: var(--surface-2);
  font-size: .82em;
}
.amount-discount dd { color: var(--success); }
.amount-total {
  border-block-start: 1px solid var(--border);
  margin-block-start: .35rem;
  font-weight: 700;
  font-size: 1.1rem;
}
.amount-total dt { color: var(--text); }
.amount-total dd { color: var(--accent); }

.code-form label { margin-block-start: 0; }
.code-row { display: flex; gap: .5rem; align-items: stretch; }
.code-row input { text-transform: uppercase; }
.code-row .btn { margin-block-start: 0; white-space: nowrap; }
.btn-small { margin-block-start: 0; padding: .4rem .8rem; font-size: .85rem; }

.rule { margin-block: 1.5rem; border: 0; border-block-start: 1px solid var(--border); }
.h2 { font-size: 1.1rem; margin-block-end: .35rem; }

.notice-success {
  padding: .7rem .9rem;
  border-radius: 8px;
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  color: var(--success);
  font-size: .9rem;
}

input[type="file"] {
  padding: .5rem;
  background: var(--surface-2);
  cursor: pointer;
}

/* ----------------------------------------------------------------- timeline */

.timeline { margin: 1.5rem 0; padding: 0; list-style: none; }

.timeline li {
  position: relative;
  display: flex;
  gap: .8rem;
  padding-block: .55rem;
  padding-inline-start: .2rem;
}

/* Connector between dots, drawn on every step but the last. Inset-inline-start
   keeps it on the correct side when the page flips to RTL. */
.timeline li:not(:last-child)::before {
  content: "";
  position: absolute;
  inset-inline-start: 6px;
  top: 1.55rem;
  bottom: -.35rem;
  width: 1px;
  background: var(--border);
}

.timeline-dot {
  flex: none;
  width: 13px;
  height: 13px;
  margin-block-start: .35rem;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  background: var(--bg);
}

.timeline-body { display: flex; flex-direction: column; gap: .1rem; font-size: .92rem; }
.timeline-body .muted { font-size: .85rem; line-height: 1.5; }

.timeline .is-done .timeline-dot { background: var(--success); border-color: var(--success); }
.timeline .is-done strong { color: var(--secondary); }

.timeline .is-current .timeline-dot { background: var(--accent); border-color: var(--accent); }
.timeline .is-current strong { color: var(--text); }

.timeline .is-failed .timeline-dot { background: var(--danger); border-color: var(--danger); }
.timeline .is-failed strong { color: var(--danger); }

.timeline li:not(.is-done):not(.is-current):not(.is-failed) strong { color: var(--muted); }

/* ------------------------------------------------------------ coach shell
   Always LTR. Client content inside is bidi-isolated by <bdi>, so Farsi names
   and answers render correctly on an English page. */

.coach-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .75rem 1.25rem;
  border-block-end: 1px solid var(--border);
  background: var(--panel);
}
.coach-header nav { display: flex; align-items: center; gap: 1.1rem; font-size: .9rem; }
.coach-header nav a { color: var(--secondary); text-decoration: none; }
.coach-header nav a:hover { color: var(--accent); }

.coach-main { max-width: 76rem; margin-inline: auto; padding: 1.75rem 1.25rem 4rem; }
.coach .brand span { color: var(--text); font-size: 1rem; }

/* -------------------------------------------------------------- queue */

.stat-row { display: flex; flex-wrap: wrap; gap: .75rem; margin-block: 1.25rem; }
.stat {
  flex: 1 1 9rem;
  padding: .9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
/* Only a non-zero count is highlighted — a wall of accent colour tells you
   nothing about where to look. */
.stat.is-live { border-color: var(--accent); }
.stat.is-live .stat-n { color: var(--accent); }
.stat-n { display: block; font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.stat-label { font-size: .82rem; color: var(--muted); }

.queue { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.queue-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem;
  padding: .8rem 1rem;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  font-size: .9rem;
}
.queue-row:last-child { border-block-end: 0; }
.queue-row:hover { background: var(--surface-2); }
/* A left border rather than a background tint, so it reads at a glance without
   competing with the status badge. */
.queue-row.needs-action { border-inline-start: 3px solid var(--accent); }
.queue-id { color: var(--muted); font-variant-numeric: tabular-nums; }
.queue-client { flex: 1 1 9rem; font-weight: 600; }
.queue-package { flex: 1 1 8rem; color: var(--secondary); }
.queue-amount { color: var(--secondary); }

.badge {
  padding: .15rem .55rem;
  border-radius: 999px;
  font-size: .74rem;
  letter-spacing: .01em;
  white-space: nowrap;
  background: var(--surface-2);
  color: var(--secondary);
}
.badge-awaiting_payment_review, .badge-pending { background: var(--accent-bg); color: var(--accent); }
.badge-awaiting_generation { background: #13251a; color: var(--success); }
.badge-generated, .badge-pending_review { background: #1a2333; color: #8ab4f8; }
.badge-delivered, .badge-sent, .badge-confirmed { background: var(--success-bg); color: var(--success); }
/* Delivered but not started yet — gold, because it is something to look
   forward to rather than something finished. See lib/schedule.js. */
.badge-scheduled { background: var(--accent-bg); color: var(--accent); }
.badge-payment_declined, .badge-declined { background: var(--danger-bg); color: var(--danger); }
.badge-warn { background: var(--accent-bg); color: var(--accent); }
.badge-flag { background: var(--accent-bg); color: #e0a458; }

/* ------------------------------------------------------- request detail */

.coach-cols { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-start; }
.coach-col-main { flex: 3 1 26rem; min-width: 0; }
.coach-col-side {
  flex: 1 1 17rem;
  max-width: 22rem;
  padding: 1.1rem 1.2rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}

/*
 * A long intake used to run hundreds of pixels past the bottom of the payment
 * and generation panels beside it, leaving the page lopsided and pushing the
 * decision buttons out of view while the coach read.
 *
 * The intake now scrolls inside its own box, capped to the screen, and sticks
 * as the coach scrolls — so the answers stay beside the Approve and Generate
 * buttons rather than being something you scroll away from to act on.
 *
 * Deliberately NOT literally "the same height as the column to its left": CSS
 * can only equalise to a sibling by absolutely positioning the column, which
 * inverts the problem the moment the left column is the shorter one (a request
 * with no payment yet gives a two-inch-tall intake box). Capping to the
 * viewport is the same fix for the case that actually occurs and degrades
 * sensibly in the case that does not.
 *
 * Desktop only: below 768px the columns stack, and a scroll box nested in a
 * scrolling page is a trap on a touch screen.
 */
@media (min-width: 768px) {
  .coach-col-side {
    position: sticky;
    inset-block-start: 1.25rem;
    /* The gap below matches .coach-main's own bottom padding so the box never
       sits flush against the edge of the window. */
    max-height: calc(100vh - 2.5rem);
    overflow-y: auto;
    /* Keeps the scrollbar off the rounded corner. */
    overscroll-behavior: contain;
  }
}

.panel {
  margin-block: 1.25rem;
  padding: 1.2rem 1.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.receipt-text {
  padding: .6rem .8rem;
  border-radius: 8px;
  background: var(--surface-2);
  font-size: .9rem;
}

.decision { margin-block-start: 1rem; padding-block-start: 1rem; border-block-start: 1px solid var(--border); }
.decision-actions { display: flex; flex-wrap: wrap; gap: .6rem; }
.decision-actions .btn { margin-block-start: 1rem; }
.btn-danger { background: transparent; border: 1px solid var(--danger); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); filter: none; }

.mode-reason { color: var(--muted); font-size: .85rem; }

.program-list { margin-block-start: 1rem; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .5rem; }
.program-list li { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; font-size: .9rem; }

.intake-group { margin-block: 1rem .35rem; font-size: .82rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.intake-answers { margin: 0; font-size: .88rem; }
.intake-answers dt { color: var(--muted); font-size: .8rem; margin-block-start: .5rem; }
.intake-answers dd { margin: 0; line-height: 1.5; }

/* ---------------------------------------------------------- program review */

.panel-flag { border-color: #4a3a15; background: #1f1a10; }
.panel-warn { border-color: var(--accent); }
.note-body { font-size: .9rem; line-height: 1.6; }
.problem-list { margin: .5rem 0 0; padding-inline-start: 1.1rem; font-size: .88rem; }
.problem-list code { background: var(--surface-2); padding: .1rem .35rem; border-radius: 4px; }

.resolve-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-block-start: .6rem;
  padding-block-start: .6rem;
  border-block-start: 1px solid var(--border);
}
.resolve-name { flex: 1 1 11rem; font-weight: 600; font-size: .9rem; }
.resolve-row select { flex: 2 1 15rem; width: auto; }
.resolve-row .btn { margin-block-start: 0; }

.day-heading { margin-block: 1.25rem .4rem; font-size: .95rem; }
.day-table-head, .day-row {
  display: grid;
  grid-template-columns: 3fr .5fr .8fr 1.2fr;
  gap: .6rem;
  padding: .4rem .5rem;
  font-size: .875rem;
}
.day-table-head { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; }
.day-row { border-block-start: 1px solid var(--border); }
/* An unmatched row is where progress tracking will silently not work, so it is
   marked here and not only in the summary above. */
.day-row.is-unmatched { border-inline-start: 3px solid var(--accent); background: #1f1a10; }

.markdown-dump {
  max-height: 34rem;
  overflow: auto;
  padding: .75rem;
  border-radius: 8px;
  background: var(--bg);
  font-size: .74rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  direction: ltr;
  text-align: start;
}

.job-status { display: flex; align-items: center; gap: .6rem; font-size: .9rem; }
.spinner {
  width: 13px; height: 13px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

.reference-editor {
  min-height: 30rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8rem;
  line-height: 1.6;
  /* Reference files are mostly English structure with Farsi content inside, so
     the editor resolves per paragraph rather than forcing one direction. */
  unicode-bidi: plaintext;
  tab-size: 2;
}

/* ------------------------------------------------------------------ errors */

.badge-alert { background: var(--danger-bg); color: var(--danger); }

.error-list { list-style: none; padding: 0; }

.error-list > li {
  padding-block: 1rem;
  border-block-start: 1px solid var(--border);
}

/* A handled error stays on the page — it is history, not noise — but recedes
   so the open ones read first. */
.error-list > li.resolved { opacity: .55; }

.error-head {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1rem;
  align-items: baseline;
  justify-content: space-between;
}

.error-message {
  margin-block: .5rem;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: .9rem;
  /* Long messages must wrap rather than widen the page — the layout uses
     logical properties throughout and one overflowing pre would break RTL. */
  overflow-wrap: anywhere;
}

.error-list pre {
  max-height: 16rem;
  overflow: auto;
  font-size: .78rem;
  padding: .75rem;
  background: var(--surface-2);
  border-radius: 6px;
}

/* ---------------------------------------------------------------- security */

.callout {
  padding: 1rem 1.25rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  margin-block-end: 1.5rem;
}

.callout-warn { border-color: var(--accent-border); background: var(--accent-bg); }

.callout h2 { margin-block-start: 0; }

.recovery-codes {
  list-style: none;
  padding: 0;
  display: grid;
  /* Two columns where there is room, one on a phone — these get copied by hand
     and a wrapped code is a mistyped code. */
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: .5rem;
  margin-block: 1rem;
}

.recovery-codes li {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 1.05rem;
  letter-spacing: .05em;
  padding: .5rem .75rem;
  background: var(--surface-2);
  border-radius: 6px;
  text-align: center;
}

code.secret {
  font-size: 1.05rem;
  letter-spacing: .08em;
  overflow-wrap: anywhere;
}

.stack { margin-block-start: 1.5rem; }

/* ----------------------------------------------------------- client program */

.program { margin-block-end: 2rem; }

.program-head { margin-block-end: 2rem; }
.program-head h1 { margin-block-end: .35rem; }

.program-meta { color: var(--muted); margin-block: 0 .75rem; }
.program-meta .dot { margin-inline: .4rem; opacity: .5; }

.modalities { display: flex; flex-wrap: wrap; gap: .4rem; margin-block: 0; }

.chip {
  font-size: .8rem;
  padding: .25rem .6rem;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
}

.day { margin-block-end: 2rem; }

.day h2 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .6rem;
  font-size: 1.05rem;
  margin-block-end: .6rem;
  padding-block-end: .5rem;
  border-block-end: 1px solid var(--border);
}

.day-number { color: var(--accent); font-weight: 600; }
.day-focus { color: var(--text); font-weight: 500; }

/* Wide content scrolls inside its own box. A horizontally scrolling BODY would
   break the RTL layout on every other page, and four columns of exercise names
   do not fit a phone. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.exercise-table, .meal-table, .data-table {
  width: 100%;
  min-width: 30rem;
  border-collapse: collapse;
  font-size: .93rem;
}

/* Fixed layout so EVERY day table shares the same column widths. With auto
   layout each table sizes to its own longest exercise name, so Day 1 and
   Day 2 line up differently and the page reads as a stack of unrelated
   tables rather than one program. */
.exercise-table, .meal-table { table-layout: fixed; }

/*
 * The coach's data tables are the opposite case, and they used to borrow
 * .exercise-table wholesale — which meant they inherited both the fixed layout
 * AND the nth-child widths sized for an exercise table's four columns. On the
 * five-column client list that handed Programs 72px and Language 363px, and a
 * 213px email painted straight over its neighbours: the Contact, Programs and
 * Language columns visibly overprinted each other.
 *
 * Auto layout sizes each column to what is actually in it. Where the total
 * still will not fit, .table-scroll takes the overflow — which is the whole
 * point of the no-wrap rule on these cells: a value either fits or the table
 * scrolls, but it never breaks in half.
 */
.data-table { table-layout: auto; }

/* Widths for the fixed layout. Sets and reps hold "4" and "12+15"; the rest
   goes to the name, which is the column that actually varies. */
.exercise-table th:nth-child(1) { width: auto; }
.exercise-table th:nth-child(2),
.exercise-table th:nth-child(3) { width: 4.5rem; }
.exercise-table th:nth-child(4) { width: 32%; }

.meal-table th:nth-child(1) { width: 8rem; }

/* Long names and food lists wrap instead of forcing the table wider than its
   scroll container. */
.exercise-table td, .meal-table td, .data-table td { overflow-wrap: anywhere; }

.exercise-table th, .meal-table th, .data-table th {
  text-align: start;
  font-weight: 600;
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  padding: .5rem .6rem;
  border-block-end: 1px solid var(--border);
}

.exercise-table td, .meal-table td, .data-table td {
  padding: .65rem .6rem;
  border-block-end: 1px solid var(--border);
  vertical-align: top;
}

/* text-align: end, not right — flips correctly on an RTL program. */
.exercise-table th.num, .exercise-table td.num,
.data-table th.num, .data-table td.num { text-align: end; white-space: nowrap; }

.exercise-name { font-weight: 500; }

/* A superset is one row containing "A + B". Marked so it reads as one unit
   rather than two exercises someone forgot to separate. */
.exercise-name.is-combo { border-inline-start: 2px solid var(--accent); padding-inline-start: .5rem; }

.ex-notes { color: var(--muted); font-size: .88rem; }

.macros {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: .75rem;
  margin-block: 1rem 1.5rem;
}

.macro {
  padding: .75rem;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  text-align: center;
}

.macro-label { display: block; font-size: .75rem; text-transform: uppercase;
               letter-spacing: .04em; color: var(--muted); margin-block-end: .25rem; }
.macro-value { font-size: 1.05rem; font-weight: 600; }

/* A "/" in a protein cell means interchangeable options (Output Format §5). */
.swappable { color: var(--text); }

/* THE COACH'S NOTE — the one thing in this document a person wrote, and the
 * ONLY place that describes it. It used to be described twice: this rule, and
 * the card rule down in "notes and nutrition, as cards", which is later and
 * more specific and therefore won every property it named. What survived from
 * here was `background` alone, so the note ended up the one block on the page
 * wearing --surface-2 — the RAISED-CONTROL token, which nothing chose for it —
 * with the accent stripe this rule intends flattened to a 1px box by the card
 * rule's `border` shorthand. Measured 2026-09-01; see docs/bugs.md F62.
 *
 * IT IS NOT A CARD, and that is the redesign rather than smaller numbers.
 * Everything else in .program-body — every .day, .notes, .nutrition — is a
 * 1px/14px/22px card, because those are the GENERATED document. Dressing the
 * coach's sentence in a fourth identical card is what made it read as "too
 * large": it was the biggest filled slab on the page, saying nothing about
 * being different, and 44px of its width went to padding and border it did
 * not need. So: no box, no fill, no radius. An accent rule down the inline
 * start and open air, which is a person speaking in the margin of a printed
 * plan — distinct from the cards below without being louder than them.
 *
 * The stripe is border-INLINE-start, so it lands on the right in Farsi. */
.coach-note {
  margin-block: 20px 28px;
  padding-inline-start: 16px;
  border-inline-start: 3px solid var(--accent);
}

/* Attribution, not a section title. "A note from your coach" says WHO is
   speaking; it is not competing with "Nutrition" for the reader's eye, so it
   drops from the cards' 17px/700 to a quiet gold line above the prose.
   No uppercase and no letter-spacing: both are meaningless in Farsi, and
   letter-spacing visibly breaks the joins of a Persian word. Size, weight and
   colour say the same thing in both scripts. */
.coach-note h2 {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
}

/* The one paragraph on this page meant to be READ as prose rather than
   scanned as a table, so it gets the reading size and a looser line.
   `overflow-wrap: anywhere` is the fix for the reported "out of margin":
   a coach types URLs and @handles into this box, and a 91-character link has
   no break opportunity, so it ran 104px past the paragraph's content box and
   gave the whole program page a horizontal scrollbar at 390px. Same idiom as
   `.freetext .ltr-data` further down, for the same reason. */
.coach-note p {
  margin: 0;
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* The foot of the program: contact, chat, ask about a move. Everything that
   is a QUESTION about the block, at the end of the block, where a client who
   has just read it is. Separated by a rule rather than a card, because it is
   not part of the document — it is what to do having read it. */
/* No padding above the first row: the 1.5rem here was clearing a heading that
   was cut on 2026-08-21, and with the heading gone it was 24px of the 72px of
   empty air that stood between the rule and the first control. The rows carry
   their own padding. */
.program-foot {
  margin-block-start: 2.5rem;
  border-block-start: 1px solid var(--border);
}

/*
 * THE FOOT IS A ROW OF BUTTONS AGAIN (owner, 2026-09-28), drawn so that the
 * two failures of the first row cannot come back.
 *
 * It was a divided list from 2026-08, because shrink-to-fit buttons had made
 * one screen into three layouts: at 390px English fell into four ragged rows
 * (162/172/245/138px) and Farsi into two rows of two, every control 38.2px
 * tall. These are not shrink-to-fit. A GRID decides the widths: one column of
 * full-width buttons on a phone, three equal columns from 48rem — so the
 * label never sizes the button in either language, a long one wraps inside
 * its own box, and a row of three is one height. 48px tall, over the 44px a
 * thumb needs. The locked shape (_vip-locked.eta) is the same .btn with its
 * tag on the top border, so it keeps the size of a live one; the row gap
 * leaves that tag room.
 */
.foot-buttons {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px 12px;
  margin: 0;
  padding: 1.5rem 0 0;
  list-style: none;
}
@media (min-width: 48rem) {
  .foot-buttons { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.foot-buttons > li { display: flex; min-width: 0; }
.foot-buttons .foot-btn {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-sizing: border-box;
  min-width: 0;
  min-height: 48px;
  margin: 0;
  padding: 10px 16px;
  font-size: 15px;
  line-height: 1.3;
  text-align: center;
  touch-action: manipulation;
}
.foot-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-secondary.foot-btn:hover { border-color: var(--accent-border); color: var(--accent); filter: none; }
/* The VIP mark on a GOLD button: the mark's own gold would vanish into it, so
   it inverts — dark chip, gold letters. */
.vip-mark.is-on-fill { background: var(--accent-ink); color: var(--accent); }

/* Below the row: the upgrade link is the only control at the foot that is
   not a consolation, so it is primary and full width. */
.program-foot-upgrade {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 100%;
  min-height: 48px;
  margin-block-start: 16px;
}

.back-link { margin-block-start: 1.5rem; }

/* Program markdown editor. Monospace and generous height: this is a structural
   document with aligned table pipes, and a proportional font makes a
   misaligned row impossible to spot. */
.program-source {
  width: 100%;
  min-height: 60vh;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  /* 1rem, not .85rem: this is a textarea, and the 16px floor applies to
     anything typed into. A monospace row gets wider; the field already scrolls
     horizontally on purpose (white-space: pre below). */
  font-size: 1rem;
  line-height: 1.55;
  /* No wrapping: a wrapped table row looks like a broken one. */
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  tab-size: 2;
}

/* --------------------------------------------------------- notifications */

.notify-list { list-style: none; padding: 0; margin-block-start: 1rem; }

.notify-list li { border-block-start: 1px solid var(--border); }
.notify-list li:first-child { border-block-start: none; }

/* The whole row is the tap target. Read on a phone, where a small link inside
   a large row is the classic way to make something feel broken. */
.notify-list a {
  display: block;
  padding: 1rem .25rem;
  text-decoration: none;
  color: inherit;
}

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

.notify-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}

.notify-body { margin-block: .35rem; }
/* An announcement is written in paragraphs and read as they were typed
   (§3). Only the kinds that declare `prose` get it — everything else is one
   sentence from the locale file. `anywhere` because a broadcast can carry a
   long URL and a phone is 360px wide. */
.notify-prose { white-space: pre-wrap; overflow-wrap: anywhere; }

/* An answer from the coach, read in place.
 *
 * Not a link, so it does not get the row-as-tap-target treatment above — but
 * it keeps the same padding, or a reply would sit tighter against its
 * neighbours than every other row on the page. */
.notify-exchange { padding: 1rem .25rem; }

.notify-who {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Their own question, quieter than the answer and set back from it: this is
   context for the reply, not the point of the row. */
.notify-asked {
  margin-block-start: .6rem;
  padding-inline-start: .7rem;
  border-inline-start: 2px solid var(--border);
}
.notify-asked p { margin: .2rem 0 0; font-size: 13px; color: var(--secondary); }

.notify-answer {
  margin-block-start: .7rem;
  padding-inline-start: .7rem;
  border-inline-start: 2px solid var(--accent);
}
.notify-answer p { margin: .2rem 0 0; line-height: 1.6; white-space: pre-wrap; }

/*
 * WHERE AN ANSWER GOES, on a message the coach sent unprompted (2026-08-30).
 *
 * Inside .notify-exchange, which is a div and not a link, so this is the only
 * tappable thing in the row — deliberately, because the row is a paragraph of
 * prose and wrapping it in an <a> would make reading it a tap target. Given a
 * block of its own with 44px of height so the target clears the mobile
 * minimum, rather than a bare inline link sitting on a 13px line.
 */
.notify-reply { margin-block-start: .7rem; }
.notify-reply a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 13px;
  font-weight: 600;
}

/* Read ones recede rather than disappear — the list is history, not an inbox
   that empties. The exchange is a div rather than an a, so it has to be named
   here too or an answered message would stay at full strength forever. */
.notify-list li:not(.is-unread) a,
.notify-list li:not(.is-unread) .notify-exchange { opacity: .6; }
.notify-list li.is-unread { border-inline-start: 2px solid var(--accent); }

/* ================================================================ account
 *
 * The component shapes below are lifted from the Claude Design file rather
 * than invented: the card (20px / 14px radius / --border-strong), the field
 * pill (10px 12px / 8px radius / --panel), the gold chip (999px pill on
 * --accent-bg), and the label/value pair. Anything not in the design is
 * derived from those — see the danger family in :root.
 */

/*
 * Body links are gold. Controls are not.
 *
 * :where() IS THE WHOLE POINT OF THIS RULE. It contributes NO specificity, so
 * this weighs (0,0,1) — beaten by any rule with a single class. Every
 * link-shaped control therefore wins on its own styling automatically, without
 * this rule having to know the control exists.
 *
 * It used to be a chain of :not(), one per control class, which weighed
 * (0,11,2) and outranked everything. A control missing from the chain rendered
 * its own label in gold ON a gold fill and simply vanished. That happened to
 * the records week button, then to gym mode's Start button, and each time a
 * name was added to the chain and the mechanism left alone — until a client
 * hit it a third time on the package screen, where "Iran" and "English" were
 * unreadable the moment they were selected.
 *
 * A list that must be updated by hand every time the app grows is a list that
 * will be out of date. This cannot fall behind, because there is nothing to
 * keep up to date.
 *
 * Inheritance is unaffected: a (0,0,1) rule still beats an inherited colour, so
 * a link inside muted or helper text is still gold.
 */
main :where(a) {
  color: var(--accent);
  text-decoration: none;
}
main :where(a):hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

.notice-ok {
  padding: .75rem 1rem;
  border-radius: 8px;
  border: 1px solid var(--success-border);
  background: var(--success-bg);
  color: var(--success);
  margin-block-end: 1.25rem;
}

/*
 * Not an error — the save succeeded. It reports that the change did NOT reach
 * the request the coach is already working from, which the client has to act
 * on themselves. Gold rather than red: nothing has gone wrong, but they cannot
 * walk away assuming it got through.
 */
.notice-warn {
  padding: .75rem 1rem;
  border-radius: 8px;
  border: 1px solid var(--accent-border);
  background: var(--accent-bg);
  color: var(--accent);
  margin-block-end: 1.25rem;
  line-height: 1.5;
}

/* ---- progress rail: REMOVED (2026-08-20) ----
 *
 * The account page's five-step rail is gone with §17 item 3. It and the
 * request card underneath it narrated the same request in two shapes, and only
 * the card carried the continue / resubmit / cancel actions — so the card
 * moved up to where the rail was and the rail went.
 *
 * The rules are deleted rather than left "in case": this file already carries
 * enough dead weight, and git remembers. What is worth keeping is why they
 * existed — the rail must never WRAP, because the steps are joined by a line
 * drawn across each one and a second row opens with a line running out of
 * nothing. If anything rebuilds a rail, that is the trap.
 */

/* ---- field pills ---- */

.field-pills {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin-block: 1rem;
}

.field-pill {
  flex: 1 1 12rem;
  padding: .625rem .75rem;
  border-radius: 8px;
  background: var(--panel);
  min-width: 0;
}

.field-label { display: block; font-size: .8rem; color: var(--muted); }
.field-value { display: block; margin-block-start: 3px; color: var(--text); overflow-wrap: anywhere; }

/* ---- gold chip ---- */

.chip {
  display: inline-block;
  padding: .3rem .75rem;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: .75rem;
  font-weight: 600;
  white-space: nowrap;
}

.program-row {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1rem;
  align-items: center;
  justify-content: space-between;
  padding-block: .6rem;
  border-block-end: 1px solid var(--border);
}
.program-row:last-child { border-block-end: none; }

.h2 { font-size: 1.05rem; margin-block-start: 0; }

/* ==========================================================================
 * DESIGN-EXACT PASS
 *
 * Values below are taken verbatim from the Claude Design file (Sobati
 * Fitness.dc.html), not approximated. Where the design and an earlier rule
 * here disagree, the design wins — hence this section sitting last.
 *
 * The design expresses type as the CSS `font` shorthand (`700 23px Archivo`).
 * That is kept as weight + size pairs rather than the shorthand itself,
 * because the shorthand would also reset font-family and drop the Farsi
 * fallback to Peyda.
 * ====================================================================== */

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

/* The design's auth and program screens sit on gradients, not flat fill.
   This is most of why the built pages read as "close but not it". */
body {
  background: var(--bg);
}

.auth-frame {
  min-height: 640px;
  display: flex;
  flex-wrap: wrap;
  gap: 26px;
  justify-content: center;
  padding: 44px 24px;
  /* radial-gradient(900px 500px at 50% -10%, #1a1e26, #0f1115) */
  background: radial-gradient(900px 500px at 50% -10%, #1a1e26, #0f1115);
}

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

h1 { font-weight: 700; font-size: 23px; letter-spacing: -0.02em; margin: 0 0 4px; }
.card > h1 + p,
.auth-card > h1 + p { margin: 0 0 18px; font-size: 14px; color: var(--secondary); }

/* The program hero uses a much larger display size. */
.program-hero h1 { font-weight: 800; font-size: 32px; letter-spacing: -0.025em; margin: 10px 0 0; }

.eyebrow {
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}

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

label {
  display: block;
  margin-block-end: 6px;
  font-weight: 500;
  font-size: 12.5px;
  color: var(--secondary);
}
label + input, label + select, label + textarea { margin-block-end: 0; }
/*
 * `:where()` so this scores as ONE element selector and not as (0,1,2).
 *
 * It is the separator between two fields of a STACKED form, which is what most
 * forms here are — but written bare it out-specified every component that had
 * already said what it wanted. `.pose-pick { margin: 0 }` on the photo page
 * says so in as many words and was losing; so was `.team-inline-form select`'s
 * neighbour. A component asking for its own spacing should win against a rule
 * that knows nothing about it, and :where() is how a default says so.
 */
form > :where(label:not(:first-child)) { margin-block-start: 15px; }

/*
 * A GAPPED GROUP OWNS THE SPACE BETWEEN ITS OWN CHILDREN.
 *
 * The two bare `label {}` rules — this one and the earlier client-shell one at
 * the top of the file — leave every label in the app carrying a 16px block
 * start and a 6px block end. Inside a container that declares `gap`, BOTH
 * apply, and the spacing on screen is the sum of the two, which no rule
 * anywhere states. This is the same collision the dashboard's `<main>` had:
 * an element-level rule reaching a component that only ever declared
 * `display` and `gap`. Measured on the branch that found it, 2026-08-29:
 *
 *  - the deload week group, which declares `gap: 8px 14px`, drew its two rows
 *    74px apart (44px label + 16 + 6 + 8);
 *  - the pause card's row and the challenge card's row are `align-items:
 *    center`, and centring a MARGIN box whose margins are 15px and 6px puts
 *    the label's words (15−6)/2 = 4.5px below the middle — so the Save button
 *    sat 4.5px above the fields it saves, on both cards;
 *  - the permissions matrix and the photo-sitting grid, at `gap: 10px`, drew
 *    their rows 32px apart.
 *
 * Only groups whose children are DRAWN AS A SET are listed. A vertical form
 * that leans on the label margin as its field separator is not one of them —
 * `.team-stack` is deliberately absent, because it already tunes its own
 * children (`.team-stack input`, `.team-stack button`) and the label margin is
 * what separates one field from the next there.
 */
.team-deload-weeks > label,
.team-inline-form > label,
.team-inline-form > p,
.team-photo-add-grid > label,
.team-perm-row > label,
.team-article-pubrow > p {
  margin-block: 0;
}

/*
 * 16px IS A FLOOR, NOT A PREFERENCE.
 *
 * iOS Safari zooms the whole viewport when a control under 16px takes focus,
 * and it does not zoom back out — so every field in the app shifted the page
 * under the person filling it in and left it shifted. AGENTS.md: "MUST: Mobile
 * <input> font-size >=16px to prevent iOS zoom". Audit finding F11/F21, one of
 * the worst five.
 *
 * It applies to anything a person TYPES INTO — input, textarea, select — and
 * to nothing else. Static UI text is a separate question and is left alone;
 * `npm run check` fails on a typeable control below the floor so this cannot
 * come back one rule at a time, which is how it arrived.
 */
input[type="text"], input[type="email"], input[type="password"],
input[type="tel"], input[type="number"], select, textarea {
  width: 100%;
  padding: 11px 13px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px;
}

/*
 * A SELECT IS EXACTLY AS TALL AS AN INPUT — not at-least, exactly.
 *
 * Two ways it drifted, one per bound. It started 6px SHORTER: a select does
 * not take a line-height (browsers force `normal` and ignore any value), so
 * the rules above styled its text and left the box a line-gap short. And with
 * only a min-height it could then run TALLER: the country dropdown's options
 * begin with a flag emoji, and on Apple devices the emoji font's metrics
 * exceed the text line, so the moment a country was chosen the closed select
 * grew past its neighbours.
 *
 * So the height is STATED, both bounds, as the same sum the input arrives at
 * on its own: one line at the page's 1.6, the 11px paddings, both borders.
 * `em`, so it tracks the font-size in the same rule rather than freezing
 * today's pixel count. A closed select is single-line by nature, so a fixed
 * height clips nothing.
 */
select {
  height: calc(1.6em + 22px + 2px);
}

input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent-border);
}

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

.btn {
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  font-size: 13.5px;
  display: inline-block;
  text-decoration: none;
}
.btn:hover { background: var(--accent-hover); color: var(--accent-ink); }

/*
 * An <a class="btn"> and a <button class="btn"> are the same control and must
 * be the same size.
 *
 * They were not: side by side in the messages pane, "Mark handled" measured
 * 41px and "Open client" measured 57px on a single line of text. A button and
 * an anchor resolve their line box differently — the button's UA styles give it
 * one, the anchor inherits the page's — so the two only ever matched by
 * coincidence, and stopped matching the moment either font-size moved.
 *
 * inline-flex with an explicit line-height makes the height a function of the
 * padding and the font, which both already share. Now they cannot drift.
 *
 * The remaining 2px was a BORDER: .btn-secondary carries `border: 1px solid`
 * and the primary carried none, so a secondary button was 2px taller than the
 * primary beside it — every time, in every row where the two appear together.
 * Measured at 35.6 against 37.6. A transparent border on the base class gives
 * both variants the same box.
 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  border: 1px solid transparent;
}
/* Whatever else is in the row, the controls sit on one line rather than
   stretching to the tallest thing beside them. */
.team-pane-actions { align-items: center; }
.team-actions-row { align-items: center; }

/*
 * A button that posts is wrapped in a form, and a form carries the global
 * bottom margin meant for a page of fields. Centring then aligned the form's
 * MARGIN box against the plain anchor's border box, leaving the two controls
 * 8px out of line even once their heights matched.
 */
.team-pane-actions form,
.team-actions-row form { margin: 0; }

/*
 * .btn-small was a no-op everywhere this second .btn block applies — same
 * specificity, declared later, so it won every property .btn-small set. Every
 * "small" button in the dashboard was rendering full size, which is why Deliver
 * sat in the review pane looking like the primary action on the page.
 *
 * Two classes, so it beats both .btn blocks by specificity rather than by
 * ordering. The values are the design's own team-pane button: 10px 15px, 13px.
 */
.btn.btn-small { padding: 10px 15px; font-size: 13px; }

.btn-secondary {
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
}
.btn-secondary:hover { background: var(--surface); color: var(--text); }

/* Auth submit is full width at a slightly larger size. */
.btn-block { width: 100%; margin-block-start: 20px; padding: 12px; font-size: 14.5px; }

/* ---- "or" divider -------------------------------------------------- */

.divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 20px 0;
  color: var(--muted);
  font-size: 12px;
}
.divider::before, .divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border-strong);
}
.divider span { padding: 0; }

/* ---- auth card ----------------------------------------------------- */

.auth-card {
  flex: 1 1 320px;
  max-width: 390px;
  padding: 28px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}
.auth-card .auth-logo { width: 42px; height: 42px; object-fit: contain; margin-block-end: 14px; }

.auth-alt { display: flex; flex-direction: column; gap: 8px; }
.auth-alt .btn-secondary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  padding: 11px;
  font-size: 14px;
}

/* ---- program hero -------------------------------------------------- */

.program-hero {
  padding: 38px 28px 30px;
  border-block-end: 1px solid var(--border);
  background: linear-gradient(180deg, #191d25, #0f1115);
}
.program-hero-inner { max-width: 900px; margin: 0 auto; }
.program-hero .program-meta { margin-block-start: 10px; font-size: 14.5px; color: var(--secondary); }
.program-hero .modalities { margin-block-start: 16px; gap: 7px; }
.program-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 20px; }

.program-body { max-width: 900px; margin: 0 auto; padding: 32px 0 60px; }

/* ---- day cards ----------------------------------------------------- */

.day {
  margin-block-end: 18px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  overflow: hidden;
}

.day-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  padding: 16px 20px;
  border-block-end: 1px solid var(--border);
  background: #1a1e26;
  margin: 0;
}
.day-head .day-number {
  font-weight: 800;
  font-size: 13px;
  letter-spacing: .12em;
  color: var(--accent);
  text-transform: uppercase;
}
/* Tracking is a Latin-caps effect. On a Farsi program the label is a word,
   and letter-spacing visibly breaks the joins of a Persian word (decisions.md,
   the free-prose box) — so it comes off with the program's own direction. */
.program[dir="rtl"] .day-head .day-number { letter-spacing: 0; }
.day-head .day-focus { font-weight: 600; font-size: 16px; color: var(--text); }
.day-head .day-count { margin-inline-start: auto; font-size: 12.5px; color: var(--muted); }

/* Rows, not a table. The design lays each exercise out as a flex row so the
   sets/reps sit as their own blocks rather than table cells — which is also
   what lets it wrap sanely on a phone. */
.ex-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 13px 20px;
  border-block-end: 1px solid #1c212a;
}
.ex-row:last-child { border-block-end: none; }

.ex-row .ex-name { flex: 1 1 220px; font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
.ex-row .ex-figure {
  min-width: 3.25rem;
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--panel);
  text-align: center;
  font-size: 13px;
}
.ex-row .ex-figure b { display: block; font-size: 11px; font-weight: 500; color: var(--muted); }
.ex-row .ex-note { flex: 1 1 100%; font-size: 12.5px; color: var(--muted); }
.ex-row .ex-note:empty { display: none; }
/* The key to a rep scheme — "one set: 10 reps near failure, then 5 + 5". Its
   own line under the figures, because it is a sentence and everything else on
   the row is a label and a number. Full width so it never squeezes the note
   beside it. */
.ex-row .ex-key {
  flex: 1 1 100%;
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--secondary);
}

/* ---- full-bleed bands ----------------------------------------------
 *
 * The auth frame and the program hero span the viewport in the design, but
 * they live inside <main>, which is capped at 60rem with its own padding.
 *
 * margin-inline: calc(50% - 50vw) rather than a transform: a transform would
 * need the opposite sign on an RTL page, and margin-inline is already
 * direction-aware. main's vertical padding is cancelled for pages carrying a
 * band so the gradient starts flush under the header.
 */
/* The design's bands span the FRAME (1180px), not the viewport. The earlier
   calc(50% - 50vw) made them span the whole screen, which is what made the
   program title look detached from the page instead of part of it. */
.auth-frame,
.program-hero {
  /* No negative margin. It used to cancel the shell padding on the element
     above, but the four screens that carry a band (login, signup, telegram
     wait, program) now zero that padding themselves further down — so the
     negative margin had nothing left to cancel and simply hung 24px off each
     side of the viewport, giving every one of those pages a horizontal
     scrollbar. Full width comes from the block box itself. */
  margin-inline: 0;
  width: auto;
}

/*
 * Page classes rather than :has().
 *
 * These rules decide LAYOUT — whether a page is full-bleed and how wide its
 * content is — so a browser without :has() would not degrade gracefully, it
 * would render the page wrong. :has() is Safari 15.4+, Chrome 105+ and Firefox
 * 121+, which is a real risk on the older Android devices common in this
 * client base.
 *
 * Every page already carries a page-<template> class, so the same thing can be
 * said without it. The remaining :has() rules are all cosmetic (a selected
 * card's highlight), where a browser that ignores them loses nothing that
 * matters.
 */
.page-login,
.page-signup,
.page-telegram-wait,
.page-program {
  padding-block-start: 0;
  max-width: none;
}

/* Content inside a full-width main still needs the reading-width cap. */
.page-program > .program > .program-body,
.page-program > .back-link {
  max-width: 900px;
  margin-inline: auto;
  padding-inline: 28px;
}

.auth-sub { margin: 0 0 18px; font-size: 14px; color: var(--secondary); }

/* ---- per-screen widths and title sizes -----------------------------
 *
 * Read off the design, screen by screen. A single global h1 size and one
 * container width cannot match a design that sets both per screen — which is
 * why the pages read as "nearly right" no matter how close the colours were.
 */

.page-home            { max-width: 880px; }
.page-request-packages{ max-width: 1000px; }
.page-request-payment { max-width: 760px; }
.page-request-intake  { max-width: 900px; }
.page-account         { max-width: 900px; }
.page-programs        { max-width: 760px; }
.page-notifications   { max-width: 760px; }

.page-home h1             { font-size: 30px; font-weight: 700; }
.page-request-packages h1 { font-size: 30px; font-weight: 700; }
.page-request-payment h1  { font-size: 28px; font-weight: 700; }
.page-request-intake h1   { font-size: 26px; font-weight: 700; }
.page-account h1          { font-size: 28px; font-weight: 700; }
.page-request-status h1   { font-size: 28px; font-weight: 700; }

/* Auth keeps 23px — that size belongs to the auth card specifically. */
.auth-card h1 { font-size: 23px; }

/* ---- hero gradients, per screen ------------------------------------ */

/* Home: a neutral lift off the page colour. */
.page-home .card:first-of-type {
  background: linear-gradient(180deg, #1c2029, #171a21);
}

/* Packages and payment: gold-tinted, because both are about buying. */
.page-request-packages .card:first-of-type,
.page-request-payment .card:first-of-type {
  background: linear-gradient(180deg, #201a11, #171a21);
  border-color: var(--accent-border);
}

/* ---- receipt dropzone hatch ---------------------------------------- */

/* The design marks the receipt upload area with a diagonal hatch, so it reads
   as a drop target rather than an empty panel. */
.dropzone {
  background: repeating-linear-gradient(135deg, #1f242d 0 6px, #191d24 6px 12px);
  border: 1px dashed var(--border-strong);
  border-radius: 12px;
  padding: 22px;
  text-align: center;
}

/* ---- header, exactly ------------------------------------------------
 *
 * The wordmark is UPPERCASE, letterspaced and small — not the larger gold
 * lockup that was here. It appears on every screen, so it was the single
 * biggest thing making the whole app look unlike the design.
 */

.site-header {
  padding: 14px 24px;
  border-block-end: 1px solid var(--border-strong);
  background: var(--surface);
}

.site-header .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}
.site-header .brand img { width: 30px; height: 30px; object-fit: contain; }
.site-header .brand bdi,
.site-header .brand span {
  font-weight: 800;
  font-size: 13px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text);
}

.site-header nav {
  display: flex;
  align-items: center;
  gap: 16px;
  font-weight: 500;
  font-size: 13.5px;
}
.site-header nav a,
.site-header nav .linklike {
  color: var(--text);
  text-decoration: none;
  font-weight: 500;
  font-size: 13.5px;
}
.site-header nav a:hover { color: var(--accent); }

/* THE TOGGLE IS A SIBLING OF THE NAV, NOT A CHILD (see _header.eta), so the
   nav has to claim the space itself instead of being the row's last item.
   Brand at the start, nav and toggle together at the end, as before. */
.site-header nav { margin-inline-start: auto; }

/* The language toggle is a segmented pill, not two separate links. */
.lang-toggle {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
}
.lang-toggle .lang {
  padding: 2px 10px;
  border-radius: 999px;
  color: var(--secondary);
  font-weight: 500;
  font-size: 11px;
  text-decoration: none;
}
.lang-toggle .lang.current {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

/* ---- home ----------------------------------------------------------- */

.page-home { padding: 38px 24px 56px; }
.page-home h1 { letter-spacing: -.02em; margin: 0; }
.page-home .signed-in-as { margin: 8px 0 0; font-size: 14px; color: var(--secondary); }

/*
 * HOME'S HERO — the card at the top that changes with the moment.
 *
 * Same box as .status-card, deliberately: the four variants are four answers
 * to one question ("what is happening") and drawing them as four different
 * shapes would make the page look like it had been rebuilt every time the
 * client's state moved. What differs is the gradient, and only for the two
 * that are asking for something.
 */
.hero-card {
  margin-block-start: 26px;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: linear-gradient(180deg, #1c2029, #171a21);
}
/* Training gets the gym's live colours — the same card the lobby draws, so
   the client recognises it as the same thing on both screens. */
.hero-training {
  background: linear-gradient(180deg, var(--gym-live-top), var(--gym-live-bot));
  border-color: var(--gym-live-line);
}
/* The two that want money wear the accent wash, which is what this app uses
   everywhere else for "this is the thing to press". */
.hero-renewal, .hero-start {
  background: linear-gradient(180deg, var(--accent-wash), var(--surface));
  border-color: var(--accent-border);
}
.hero-eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-dim);
}
.hero-training .hero-eyebrow { color: var(--gym-live-text); }
.hero-title {
  margin: 8px 0 6px;
  font-weight: 700;
  font-size: 20px;
  /* A long day focus must wrap inside the card rather than widen the page. */
  overflow-wrap: anywhere;
}
.hero-meta { margin: 0; max-width: 56ch; font-size: 14.5px; line-height: 1.6; color: var(--secondary); }
.hero-go { margin-block-start: 16px; }

/*
 * The demoted lines. Deliberately quiet and deliberately NOT cards: status
 * news under a training hero, a renewal that has not arrived, and the week
 * after a delivery. Each is one sentence and a link — enough to be seen, not
 * enough to compete with the thing above it.
 */
.hero-aside {
  margin: 10px 0 0;
  font-size: 13.5px;
  color: var(--muted);
}
.hero-aside a { white-space: nowrap; }

.status-card {
  margin-block-start: 26px;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: linear-gradient(180deg, #1c2029, #171a21);
}
.status-card h2 { margin: 14px 0 6px; font-weight: 700; font-size: 20px; }
.status-card p { margin: 0; max-width: 56ch; font-size: 14.5px; line-height: 1.6; color: var(--secondary); }

.status-pill {
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent-bg);
  border: 1px solid var(--accent-border);
  color: var(--accent);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.status-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.status-meta { font-size: 13px; color: var(--secondary); }

/* Segmented progress: four bars, filled up to the current stage. Simpler than
   the account page's dotted rail and what the design uses here. */
.seg-bars { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 20px; }
.seg-bars span { flex: 1 1 80px; height: 4px; border-radius: 2px; background: var(--border-strong); }
.seg-bars span.is-done { background: var(--accent); }

.seg-labels {
  display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 8px;
  font-weight: 500; font-size: 11.5px; color: var(--muted);
}
.seg-labels span { flex: 1 1 80px; }

.tile-grid {
  margin-block-start: 14px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}
.tile {
  display: block;
  padding: 20px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}
.tile:hover { border-color: var(--accent-border); color: var(--text); }
.tile-title { font-weight: 600; font-size: 15.5px; }
.tile-sub { margin-block-start: 4px; font-size: 13px; color: var(--secondary); }

/* ---- packages, design-exact -----------------------------------------
 *
 * Transcribed from the Claude Design file's `isPackages` screen. Every number
 * below is the design's own; the colours all had tokens already, and the one
 * that did not (--accent-wash) was added for it.
 *
 * A grid of equal-height cards. The price is the loudest thing on each
 * (700/26px gold), the feature list is quiet, and each card's control pins to
 * the bottom so they line up across cards whatever the feature lists do.
 */

.page-request-packages { padding: 40px 24px 60px; max-width: 1000px; margin-inline: auto; }
.page-request-packages h1 { margin: 0; letter-spacing: -.02em; }
.page-request-packages > p:first-of-type,
.page-request-packages .lede { margin: 8px 0 0; font-size: 15px; color: var(--secondary); }

.package-grid {
  margin-block-start: 26px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
}

/*
 * THE CARD IS A WRAPPER; THE PAINT IS ON .package-inner.
 *
 * A single package is a <label> around a radio, so the whole 240px box is the
 * tap target — which is what it looks like, and what a thumb expects. The
 * border and background sit on an inner box rather than on the label itself so
 * that `input:checked ~ .package-inner` can reach them as a SIBLING.
 *
 * That indirection is the point. The obvious way is `:has(input:checked)` on
 * the label, and docs/inventory.md records why not: Firefox before 121 and
 * Safari before 15.4 do not support it, both of which are plausible on an
 * older Android in Iran, and there the client would simply be unable to see
 * which package they had picked. A selection ring is not something to degrade.
 */
.package-card {
  position: relative;
  display: flex;
  flex-direction: column;
}

/*
 * The name-and-price wrapper is INVISIBLE to layout at this width: its two
 * children go on being flex items of `.package-inner`, exactly as they were
 * before the wrapper existed. It becomes a real row only on a phone.
 */
.package-head { display: contents; }

/* Visually hidden but still focusable and announced — the card is the control. */
.package-card > input {
  position: absolute;
  width: 1px; height: 1px;
  margin: 0; padding: 0;
  opacity: 0;
}

.package-inner {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}

label.package-card { cursor: pointer; }
label.package-card:hover .package-inner { border-color: var(--accent-border); }

/* Chosen: a doubled gold line, so it stays distinct from the featured card,
   which carries a single one. */
.package-card > input:checked ~ .package-inner {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.package-card > input:focus-visible ~ .package-inner {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Not priced in this market — the selectable state is removed, not just dimmed. */
.package-card.is-unpriced { cursor: default; }
.package-card.is-unpriced .package-inner { opacity: .62; }
.package-card.is-unpriced:hover .package-inner { border-color: var(--border-strong); }

.package-name { margin: 0; font-weight: 600; font-size: 17px; }

.package-price {
  margin: 10px 0 14px;
  font-weight: 700;
  font-size: 26px;
  color: var(--accent);
}
.package-price.is-unset { font-size: 17px; color: var(--secondary); font-weight: 500; }

.package-features {
  display: flex;
  flex-direction: column;
  gap: 7px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--secondary);
}

/* ---- the badge ----
 *
 * Which package wears it is set from the dashboard, not fixed here — it is a
 * claim a client reads as fact, so where it sits is the owner's call. See
 * pricing.js featuredPackage().
 */
.package-badge {
  position: absolute;
  top: -10px;
  inset-inline-start: 22px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  font-size: 10.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.package-card.is-featured .package-inner { border-color: var(--accent); }

/* ---- the Select control ----
 *
 * A <span>, not a <button>: the label around it is the control, and a real
 * button inside a form would submit it. It carries the design's treatment and
 * reports state, so a chosen card says so rather than only being outlined.
 */
.package-select {
  margin-block-start: auto;
  padding: 11px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
  font-size: 13.5px;
  text-align: center;
}
/* The featured card's is filled, as the design draws it. */
.package-card.is-featured .package-select {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
}
.package-select-on { display: none; }
.package-card > input:checked ~ .package-inner .package-select-off { display: none; }
.package-card > input:checked ~ .package-inner .package-select-on { display: inline; }
.package-card > input:checked ~ .package-inner .package-select {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}

/* ---- the VIP cards ----
 *
 * Three again, one per length — the design's single card with length chips was
 * built and replaced the next day at the owner's call: "from $380" hides what
 * the longer lengths cost, and the thing worth saying about them (the longer
 * commitment is cheaper per block) could not be said on a chip. Each carries
 * the design's VIP treatment: warm gradient, gold name, gold-tinted border.
 */
.package-card.is-vip .package-inner {
  border-color: var(--accent-border);
  background: linear-gradient(180deg, var(--accent-wash), var(--surface));
}
.package-card.is-vip .package-name { color: var(--accent); }

/*
 * The savings line under a 12- or 24-week price. Quiet — it qualifies the
 * price, it does not compete with it — and gold, because it is good news.
 * Pulled up against the price's own 14px bottom margin so the pair read as
 * one statement.
 */
.package-save {
  margin: -8px 0 14px;
  font-weight: 600;
  font-size: 12.5px;
  color: var(--accent);
}

/* ---- the group separators ----
 *
 * Not in the design file, which drew one grid — added with the split at the
 * owner's call. The eyebrow treatment is the design's own (600/11px, .14em
 * tracking, uppercase, muted), with a hairline carrying on from the words: the
 * groups differ in what is being bought, and the boundary should be legible
 * at a glance without shouting.
 */
.package-group {
  margin: 30px 0 0;
  display: flex;
  align-items: center;
  gap: 14px;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}
.package-group::after { content: ""; flex: 1; border-block-start: 1px solid var(--border); }
/* The grid below a group heading needs less than its usual top margin — the
   heading already provides the pause. `.package-shared` sits between the two
   when a group has one, and an adjacent-sibling selector still counts a
   `display: none` element — so the second selector is what carries the margin
   for that group at EVERY width, not only the one that draws the line. */
.package-group + .package-grid,
.package-shared + .package-grid { margin-block-start: 16px; }

/*
 * What every package in a group says, said once. Drawn only on a phone — see
 * the block at the end of this section for why, and for the rest of the
 * one-column layout.
 */
.package-shared { display: none; }

/* ---- the packages screen on a phone -----------------------------------
 *
 * MEASURED, not guessed. At 390x844 the six cards were 259px each and the
 * page was 2579px — 3.06 screens, with the last card's bottom edge 2.5
 * screens down. A client choosing between six things could see two and a
 * half of them at a time.
 *
 * 560px, AND the column count is forced rather than inferred. Both halves of
 * that were learned by measuring.
 *
 * The first cut derived the breakpoint from the grid — two columns need
 * 2 x 240px + a 14px gap = 494px of content against 24px of side padding — and
 * put it at 540px. Walking the widths found the grid actually collapsing at
 * 541px, so there was exactly one viewport width showing a single column of
 * the 259px cards: the complaint itself, preserved in a one-pixel band.
 *
 * Worse, the derivation eats itself. This block narrows the side padding, which
 * WIDENS the content, which at 541px is enough for two columns again — so the
 * query would have drawn two 240px columns of a layout designed for one, with
 * a name and a price sharing a 240px line.
 *
 * So: a breakpoint the file already uses elsewhere, and `grid-template-columns:
 * 1fr` inside it. Below 560px it is one column and these rules; above it, the
 * design file's own layout, which the owner is happy with, is untouched.
 *
 * Three things were making the stack tall, and none of them was the content:
 *
 * 1. THE PRICE HAD A LINE TO ITSELF. In three columns a 26px price under the
 *    name is the design's loudest note and costs nothing. Stacked, it is six
 *    lines of pure height. It moves up beside the name, still gold and still
 *    the heaviest thing on the card.
 *
 * 2. THE SELECT BAR SAID WHAT THE CARD ALREADY SAID. The whole card is the
 *    tap target — a 46px bar inside it repeats the affordance rather than
 *    adding one, six times over. It is hidden UNTIL CHOSEN, and then it is
 *    the words "Selected". That keeps the rule it was written for: a chosen
 *    card says so in words and not only by turning gold.
 *
 * 3. THE VIP CARDS REPEATED EACH OTHER. Their first two feature lines are
 *    identical across all three; the group heading now carries them once.
 *
 * Result, measured at the same 390x844: cards 81-124px, the page 1665px, 1.97
 * screens, and the last card's bottom edge 1.44 screens down instead of 2.5.
 */
@media (max-width: 560px) {
  .page-request-packages { padding: 26px 20px 44px; }
  /*
   * 14px, not the 10px this first tried. The featured badge is positioned
   * `top: -10px` and protrudes into the gap above its card — at a 10px gap it
   * sat ON the card above. The gap has to clear the badge, not merely the
   * cards.
   */
  .package-grid {
    /* Stated, not inferred — see above. */
    grid-template-columns: 1fr;
    margin-block-start: 18px;
    gap: 14px;
  }
  .package-group { margin-block-start: 22px; }

  /*
   * A COLUMN, like the feature lists it was lifted out of, and for the same
   * reason they are one: these are separate facts, not a sentence.
   *
   * This first ran them inline with a `::before` middot between. Both lines
   * are long enough to wrap at 390px, and a separator drawn on the second
   * flex item lands at the START of the line it wrapped to — a bullet floating
   * under the first fact, attached to nothing.
   */
  .package-shared {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 8px 0 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--secondary);
  }

  .package-inner { padding: 13px 15px; border-radius: 12px; }
  /* Follows the padding it sits against. */
  .package-badge { inset-inline-start: 15px; }

  /* Name and price on one line. `display: contents` above this width. */
  .package-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
  }
  .package-name { font-size: 15px; }
  /* Still the loudest thing on the card, just no longer a line of its own. */
  .package-price { margin: 0; font-size: 18px; white-space: nowrap; }
  .package-price.is-unset { font-size: 12.5px; white-space: normal; }

  .package-save { margin: 5px 0 0; }

  .package-features {
    margin-block-start: 5px;
    gap: 3px;
    font-size: 12.5px;
  }
  /* Said once under the group heading instead. */
  .package-features > [data-shared] { display: none; }

  /*
   * Hidden until it has something to report, and then it is a line of words
   * rather than a bar.
   *
   * SPLIT BY SPECIFICITY, ON PURPOSE. The wide rules that paint this box —
   * `.package-card > input:checked ~ .package-inner .package-select` at (0,4,1)
   * and the featured card's fill at (0,3,0) — both outrank a bare
   * `.package-select`, so a reset written there would lose and a chosen card
   * would keep its bar. The box reset therefore goes on the checked selector,
   * which is the one that wins.
   *
   * `font-size` is the exception and stays off it: `scripts/check.mjs` reads
   * any selector containing `input` as a control somebody types into, and
   * flags a size under 16px as the iOS focus-zoom breach. This is a <span>;
   * the `input` in that selector is the radio the card hides. Splitting it
   * keeps the rule honest instead of arguing with the checker.
   */
  .package-select { display: none; font-size: 12px; }
  .package-card > input:checked ~ .package-inner .package-select {
    display: block;
    margin-block-start: 7px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font-weight: 700;
    text-align: start;
  }
}

/* ---- delivery language chips ---- */

.package-chips { display: flex; gap: 7px; }

.package-chip {
  display: block;
  padding: 9px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--secondary);
  font-weight: 600;
  font-size: 13px;
  text-decoration: none;
  cursor: pointer;
}
.package-chip.is-selected {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}

/* ---- delivery language ---- */

.package-delivery {
  margin-block-start: 22px;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  justify-content: space-between;
  padding: 18px 22px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--panel);
}
.package-delivery-k { font-weight: 600; font-size: 14.5px; }
.package-delivery-v { margin-block-start: 4px; font-size: 13px; color: var(--secondary); }

/* ---- the way on ---- */

/*
 * The packages screen uses .intake-actions now, so its buttons sit exactly
 * where every other step of the request flow puts them — that consistency is
 * what makes a back button findable. The design's own bottom-right placement
 * went with it: the design drew no back button, and once there are two, a
 * lone primary flung to the far right leaves the pair reading as unrelated.
 * The design's button SIZE is kept.
 */
.page-request-packages .intake-actions .btn { padding: 13px 28px; font-size: 14.5px; }

/* ================================================================== shell
 *
 * The design renders the app inside a frame capped at 1180px (390px on
 * mobile) with its own background. That frame is the app; the darker colour
 * around it is the preview shell. Reproducing the cap is what makes the
 * full-width bands read as part of the page rather than as the whole screen.
 */

:root { --shell-pad: 24px; }

/*
 * 1180px in the design is the mockup's simulated DESKTOP VIEWPORT, not a
 * content wrapper — which is why capping the header at it left gutters and
 * made the bar look broken. On a real site that width is simply the browser.
 *
 * So: bars span the viewport, the page background is the app background, and
 * each screen caps its own content at the width the design gives it.
 */
body {
  background: var(--bg);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/*
 * BARS span the viewport; their CONTENTS are capped.
 *
 * Capping the header element itself left dark gutters either side of it, which
 * is what made it look broken — in the design the frame IS the viewport, so a
 * band that reaches the frame edge reaches the screen edge on a real site.
 */
.site-header, .site-footer { width: 100%; max-width: none; }

.shell-row {
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
  /* Matches the widest screen the design defines, so header and footer
     contents line up with the content below them. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

main {
  width: 100%;
  max-width: none;
  background: var(--bg);
  padding-inline: 0;
  flex: 1;
}

/* Each screen carries its own width, centred, with the design's side padding. */
main > * { padding-inline: var(--shell-pad); }
.page-home, .page-account, .page-request-intake, .page-request-packages,
.page-request-payment, .page-programs, .page-notifications,
.page-request-status, .page-forgot-password, .page-reset-password {
  margin-inline: auto;
  padding-inline: var(--shell-pad);
}

/* Screens whose own container is narrower still sit inside the 1180 shell. */
.page-home, .page-account, .page-request-intake,
.page-request-packages, .page-request-payment,
.page-programs, .page-notifications, .page-request-status {
  margin-inline: auto;
}

/* ---- notes and nutrition, as cards ---------------------------------
 *
 * .coach-note was in this list and is deliberately NOT any more. These are
 * the generated document's blocks; the coach's note is a person's sentence
 * and is described once, beside .swappable. Adding it back here re-creates
 * the two-rule conflict of F62 — `border` alone would flatten its stripe. */

.program-body .notes,
.program-body .nutrition {
  margin-block-start: 18px;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
}
.program-body .notes { margin-block-start: 26px; background: var(--panel); }
.program-body .nutrition { background: var(--surface); }

.program-body .notes h2,
.program-body .nutrition h2 { margin: 0 0 12px; font-weight: 700; font-size: 17px; }
.program-body .nutrition h2 { margin-block-end: 16px; }
.program-body .nutrition h3 { margin: 24px 0 12px; font-weight: 600; font-size: 15px; }

.program-body .notes ul,
.program-body .nutrition ul {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--nav-link);
}

/* ---- macro tiles ---------------------------------------------------- */

.macros {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
  margin: 0;
}
.macro {
  padding: 14px;
  border-radius: 10px;
  background: var(--panel);
  border: 0;
  text-align: start;
}
.macro-label { font-weight: 400; font-size: 11.5px; color: var(--muted); text-transform: none; letter-spacing: 0; }
.macro-value { display: block; margin-block-start: 4px; font-weight: 700; font-size: 20px; }
/* Calories lead, so they carry the accent. */
.macro:first-child .macro-value { color: var(--accent); }

/* ---- meal plan ------------------------------------------------------ */

.meal-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.meal-head {
  display: flex;
  gap: 10px;
  padding: 10px 14px;
  background: var(--panel);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}
.meal-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 14px;
  border-block-start: 1px solid #1c212a;
  font-size: 14px;
}
.meal-head span:nth-child(1), .meal-row span:nth-child(1) { flex: 1 1 90px; font-weight: 600; }
.meal-head span:nth-child(2), .meal-row span:nth-child(2) { flex: 1 1 160px; }
.meal-head span:nth-child(3), .meal-row span:nth-child(3) { flex: 1 1 140px; }
.meal-head span:nth-child(4), .meal-row span:nth-child(4) { flex: 1 1 120px; }
.meal-row span:nth-child(n+2) { color: var(--secondary); }

/* ---- exercise figures, inline not boxed ----------------------------- */

.ex-row .ex-figures { display: flex; gap: 18px; font-size: 13px; color: var(--secondary); }
.ex-row .ex-figures b { color: var(--muted); font-weight: 400; }
.ex-row .ex-figures i { color: var(--text); font-weight: 600; font-style: normal; }

/* ---- footer (landing only) ------------------------------------------ */

.site-footer {
  margin-block-start: auto;
  padding: 24px 28px 20px;
  border-block-start: 1px solid var(--border);
  /* Darker than the page, as in the design — it closes the page rather than
     floating on it. */
  background: #0d1014;
  font-weight: 500;
  font-size: 13px;
}
.footer-links { display: flex; flex-wrap: wrap; gap: 16px; }
.site-footer a { color: var(--nav-link); text-decoration: none; }
.site-footer a:hover { color: var(--accent); }
.site-footer small { font-weight: 400; font-size: 12px; color: var(--muted); }

/* ---- card rhythm ----------------------------------------------------
 *
 * The account and notification screens stack several cards. They had no
 * consistent gap and inherited a tighter padding than the design's 22px, so
 * the boxes read as cramped and unevenly spaced.
 */

main > .card,
main > section.card {
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}

main > .card + .card,
main > section.card + section.card { margin-block-start: 18px; }

/* First card clears the header; last one clears the footer. */
main > .card:first-child { margin-block-start: 26px; }
main > .card:last-of-type { margin-block-end: 40px; }

/* The page title sits above the cards, not inside the first one. */
main > h1:first-child { margin-block-start: 34px; }

/* Give every non-band page breathing room top and bottom, matching the
   design's 38-44px leading padding. */
.page-account, .page-notifications, .page-programs,
.page-request-status, .page-forgot-password, .page-reset-password {
  padding-block: 38px 56px;
}

/* On the program screen the content column is 900px, so the footer row lines
   up with it rather than with the wider shell. */
.page-program { max-width: none; padding-inline: 0; }
.page-program > * { padding-inline: 0; }
.page-program .program-body { max-width: 900px; margin-inline: auto; padding-inline: 28px; }
.page-program .program-hero-inner { max-width: 900px; margin-inline: auto; }

/* ---- shared form furniture ------------------------------------------- */

/* Two fields on one row where there is room, stacked on a phone. Used by the
   signed-out contact form, signup, and the account and admin detail pages. */
.field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.field-row label { margin-block-start: 0; }

.choice-row { display: flex; flex-wrap: wrap; gap: 7px; }

/*
 * The chip. The real input is visually hidden but still focusable and still
 * the thing the browser checks — hiding it with display:none would take it out
 * of the tab order and out of the form.
 *
 * Values are the design's, exactly: 9px 16px, 8px radius, --surface-2 on
 * --border-strong, and gold-on-gold-tint when selected.
 */
.choice input { position: absolute; opacity: 0; pointer-events: none; }
.choice span {
  /*
   * A REAL 44px BOX, and the design's padding kept.
   *
   * 9px of padding on 13.5px text at the body's 1.6 leading plus two 1px
   * borders measures 41.6px — a pixel and a half under the 44px AGENTS.md
   * asks of a touch target, which is the QA run's finding of 2026-09-01
   * ("the notifications celebrations chip at 41.59px"). The span IS the
   * control: the real input is visually hidden, so there is nothing else
   * under the thumb.
   *
   * `min-height` rather than more padding, because the padding is the
   * design's own number ("9px 16px, exactly", below) and the chip should not
   * be redrawn to reach a floor. `inline-flex` for the same reason
   * `.team-photoset-del` uses it: a min-height on an inline-block grows the
   * box downwards and leaves the word sitting at the top of it.
   */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  touch-action: manipulation;
  padding: 9px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--secondary);
  font-weight: 600;
  font-size: 13.5px;
  cursor: pointer;
}
.choice:hover span { border-color: var(--accent-border); }
/* The selected chip is gold-outlined, as in the design. */
.choice input:checked + span {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}
/* On the input, not the span: the input is what actually receives focus, and
   a keyboard user needs to see where they are with the box invisible. */
.choice input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- contact --------------------------------------------------------- */
/*
 * Transcribed from the design's isContact screen rather than mapped onto
 * `.card`. The form panel is 22px / 14px / --border-strong / --surface and the
 * aside panels are 18px / 14px / --border-strong / --panel — two different
 * boxes that one class cannot express, which is the mistake the records screen
 * made. See docs/decisions.md.
 */

.page-contact { max-width: 760px; padding-block: 44px 56px; }
.contact-head h1 { margin: 0; font-size: 30px; letter-spacing: -.02em; }
.contact-head p {
  margin: 8px 0 0;
  max-width: 52ch;
  font-size: 15px;
  line-height: 1.6;
  color: var(--secondary);
}

.contact-cols { margin-block-start: 26px; display: flex; flex-wrap: wrap; gap: 14px; }

.contact-panel {
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}
.contact-form { flex: 1 1 320px; min-width: 280px; max-width: 620px; }
.contact-sent { margin-block-start: 26px; }

/* Who we already know they are. The rule under it is what separates this from
   the form, exactly as drawn. */
.contact-who {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  align-items: center;
  padding-block-end: 14px;
  border-block-end: 1px solid var(--border-strong);
}
/* .contact-badge — the "SIGNED IN" pill — is gone with its markup; it was
   the design canvas's variant caption, never a label for a client. */
.contact-who-name { font-weight: 600; font-size: 13.5px; }
.contact-who-meta { font-size: 12px; color: var(--muted); }

/* The first label after the strip carries the design's 15px gap; the global
   form rule would otherwise close it up. */
.contact-form label { display: block; margin: 15px 0 6px; font-weight: 500; font-size: 12.5px; color: var(--secondary); }
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.contact-grid label { margin: 0 0 6px; }

/* The design gives every control the full width of the card. The global form
   rules size a <select> to its content, which left Topic visibly narrower than
   the Message box beneath it. */
.contact-form input,
.contact-form select,
.contact-form textarea {
  width: 100%;
  /* The global rules cap a select at 22rem and a text input at 32rem, which
     left Topic a pixel short of the card and would clip a wider one. Inside
     this card the card decides the width. */
  max-width: none;
  padding: 11px 13px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  /* The 16px floor, same reason as the global rule above. */
  font-size: 16px;
}
.contact-form textarea {
  padding: 12px 13px;
  line-height: 1.6;
  /* Vertical only: a horizontally resized textarea escapes the card. */
  resize: vertical;
}

.contact-reply { margin: 14px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--secondary); }
.contact-reply a { color: var(--accent); }

/* The send button, at the design's own measurements rather than the shared
   .btn-block ones (20px/12px). */
.contact-form .btn-block {
  margin-block-start: 16px;
  padding: 13px;
  border: 0;
  border-radius: 8px;
  font-size: 14.5px;
  font-weight: 700;
}

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

.page-legal { max-width: 760px; padding-block: 44px 56px; }

.legal { max-width: 68ch; }
.legal h1 { font-size: 30px; letter-spacing: -.02em; margin: 0 0 8px; }
.legal h2 { font-size: 19px; margin: 32px 0 10px; }
.legal h3 { font-size: 15px; margin: 24px 0 8px; }
.legal p, .legal li { font-size: 15px; line-height: 1.7; color: var(--nav-link); }
.legal ul { padding-inline-start: 1.25rem; }
.legal li { margin-block-end: .4rem; }
.legal hr { border: 0; border-block-start: 1px solid var(--border); margin-block: 34px; }
.legal strong { color: var(--text); }

/* ---- markdown constructs (lib/markdown.js, 2026-08-28) ---------------- */

/* Everything the house renderer can now emit, styled ONCE and inherited by
   both surfaces that render markdown: the legal pages and article bodies.
   Kept out of `.legal` and `.article-body` so neither has to know about a
   construct the other gained — the renderer is one renderer.

   The renderer emits NO inline style attribute, deliberately: the CSP is
   `style-src 'self'`, so anything computed (a table column's alignment)
   arrives as a class and is answered here. */

.md-quote {
  margin: 0 0 22px;
  padding: 14px 18px;
  border: 0;
  border-inline-start: 3px solid var(--accent-border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
  background: var(--accent-bg);
}
.md-quote > :last-child { margin-block-end: 0; }
.md-quote strong { color: var(--accent); }

/* A figure, and its caption. The caption is quieter than the prose around it
   on purpose — it describes the picture, it is not part of the argument. */
.md-figure { margin: 0 0 22px; }
.md-figure img,
.article-body img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.md-figure figcaption {
  margin-block-start: 8px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}

/* A table wider than a phone scrolls INSIDE this box. The page itself must
   never scroll sideways (AGENTS.md), and a scroll region needs to be
   reachable by keyboard, which is what the renderer's tabindex is for — so
   it also needs a focus ring, or the focus lands somewhere invisible. */
.md-tablewrap {
  margin: 0 0 22px;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.md-tablewrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.md-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.md-table th,
.md-table td {
  padding: 10px 14px;
  text-align: start;
  border-block-end: 1px solid var(--border);
  /* Numbers in a table are there to be compared down the column. */
  font-variant-numeric: tabular-nums;
}
.md-table th { color: var(--text); font-weight: 600; background: var(--panel); }
.md-table td { color: var(--nav-link); }
.md-table tbody tr:last-child td { border-block-end: 0; }
.md-al-center { text-align: center; }
.md-al-end { text-align: end; }

/* A fence. `dir="ltr"` is not set in the markup — code is Latin data and its
   own direction, so the rule states it here rather than asking the renderer
   to decorate every block. */
.md-code {
  margin: 0 0 22px;
  padding: 14px 16px;
  overflow-x: auto;
  direction: ltr;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.md-code code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.6;
  color: var(--nav-link);
  white-space: pre;
}

/* Footnotes: the reference, and the list at the foot with a link back. */
.md-fnref { font-size: .75em; line-height: 0; }
.md-fnref a { text-decoration: none; }
.md-footnotes { margin-block-start: 34px; }
.md-footnotes hr {
  border: 0;
  border-block-start: 1px solid var(--border);
  margin-block: 0 18px;
}
.md-footnotes ol { padding-inline-start: 1.25rem; margin: 0; }
.md-footnotes li { font-size: 14px; line-height: 1.65; color: var(--secondary); }
.md-footnotes li + li { margin-block-start: 8px; }
/* The back-link is a target as well as a mark: 24px minimum, per AGENTS.md,
   expanded around a glyph that is much smaller than that. */
.md-fnback {
  display: inline-block;
  min-width: 24px;
  min-height: 24px;
  padding-inline: 4px;
  text-decoration: none;
}

/* ---- telegram waiting screen ---------------------------------------- */

/* Matches the design's alternative-sign-in buttons: same card, same input
   surface, so the waiting state reads as part of the auth flow rather than a
   separate page. */
.tg-status {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-block: 18px;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  font-size: 13.5px;
  color: var(--secondary);
}
.tg-status.is-done { border-color: var(--accent-border); color: var(--accent); }
.tg-status.is-done .spinner { display: none; }

/* ================================================== mobile (design: 390px)
 *
 * The design ships a desktop AND a mobile view, and they differ structurally
 * rather than just in width. The consistent pattern: a vertical sidebar
 * becomes a full-width band whose items scroll HORIZONTALLY, with a bottom
 * border instead of an inline-end one.
 *
 * 768px rather than the design's literal 390px: 390 is the phone frame's
 * width, not the point at which the desktop layout stops working. A sidebar
 * plus content needs more room than a tablet has.
 */
@media (max-width: 767px) {
  :root { --shell-pad: 16px; }

  .site-header { padding: 12px 16px; }
  .site-header nav { gap: 12px; font-size: 13px; }

  /* Five steps sharing a phone's width. Smaller and tighter so the longest
     label fits its share on one line instead of breaking mid-word. */

  /* Sidebar -> full-width band. */
  .intake-side {
    flex: 1 1 100%;
    /*
     * min-width: 0, and it is the whole reason this worked on paper and not on
     * a phone.
     *
     * A flex item defaults to `min-width: auto`, which means it REFUSES to
     * shrink below its content. The step strip below sets `overflow-x: auto`
     * expecting to scroll inside itself — but it could never shrink to a size
     * that needed scrolling, so instead it pushed this band out to 874px on a
     * 375px screen and the whole PAGE scrolled sideways. That is the "tabs clip
     * into the side" report, and the sidebar's own border ended up drawn off
     * the right edge of the phone.
     */
    min-width: 0;
    max-width: none;
    padding: 14px;
    border-inline-end: 0;
    border-block-end: 1px solid var(--border);
  }

  /* Steps -> a horizontal scroller. Each step stops wrapping, so they read as
     a row of chips rather than a stack of broken lines. */
  .intake-steps {
    margin-block-start: 10px;
    display: flex;
    flex-direction: row;
    gap: 6px;
    /* Same trap one level down: the <ol> is itself a block in a flex column,
       and without this it reports its full content width upwards. */
    min-width: 0;
    overflow-x: auto;
    padding-block-end: 4px;
    -webkit-overflow-scrolling: touch;
    /* Scrolls to a step boundary rather than stopping mid-chip. */
    scroll-snap-type: x proximity;
  }
  .intake-steps li { scroll-snap-align: start; }
  .intake-steps li { flex: none; }
  .intake-step { white-space: nowrap; width: auto; }

  /* The design hides the hint on mobile: it explains that answers save as you
     go, which is reassurance worth a sidebar and not worth the fold. */
  .intake-hint { display: none; }

  /* The form column loses the desktop's generous padding — and the same
     min-width: 0, or a long unbroken answer in an input does to this column
     what the step strip did to the band above it. */
  .intake-main { min-width: 0; padding: 20px 16px 32px; }
  .intake-main h1 { font-size: 22px; }
  .intake-sub { font-size: 13.5px; margin-block-end: 18px; }

  /* Titles step down — the design's desktop sizes overflow a 390px screen. */
  .page-home h1, .page-request-packages h1 { font-size: 24px; }
  .page-account h1, .page-request-payment h1, .page-request-status h1 { font-size: 23px; }
  .program-hero h1 { font-size: 24px; }
  .program-hero { padding: 26px 20px 24px; }
  .program-body { padding-inline: 0; }

  .auth-frame { padding: 24px 16px; min-height: 0; }
  .auth-card { padding: 22px; }

  /* Exercise rows: name on its own line, figures beneath. Four columns of
     anything do not fit a phone. */
  .ex-row { padding: 12px 14px; }
  .ex-row .ex-name { flex: 1 1 100%; }
  .ex-row .ex-figures { flex: 1 1 100%; gap: 14px; }

  .day-head { padding: 14px 14px; }
  .contact-cols { flex-direction: column; }
}

/* ---- admin ----------------------------------------------------------- */

.search-row { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 1rem 1.25rem; }
.search-row input { flex: 1 1 16rem; }
.search-row .btn { flex: none; }

/* ---- form control widths -------------------------------------------
 *
 * The design-exact pass set width:100% on every control, which is right for a
 * 390px auth card and wrong everywhere else: on the account page a two-option
 * language select stretched the full container, which reads as a broken layout
 * rather than a considered one.
 *
 * The design constrains these by putting them in a grid; anything NOT in one
 * gets a sensible cap here.
 *
 * Text inputs were exempted at first, on the theory that a name or an address
 * genuinely uses the width. They do not: a name field running the whole width
 * of a 1180px card reads as a broken layout, not a generous one, and no field
 * in the design file is drawn that way. They are capped too — just wider than
 * a select, because the content is longer.
 */
/*
 * A select is as wide as its longest option, not as wide as the row.
 *
 * 22rem was still far too much for a two-option language picker — it looked
 * like a text field that had failed to fill rather than a chooser. width:auto
 * lets the control size to its content; min-width stops a one-word option
 * collapsing to a stub, and max-width catches a long option list.
 */
select {
  width: auto;
  min-width: 9rem;
  /*
   * 22rem, or the row — whichever is smaller.
   *
   * The cap alone was not enough. A select sized to its longest option means
   * the country list is as wide as "South Georgia & South Sandwich Islands",
   * which is 349px: fine in a 1180px card, 19px past the edge of a 390px phone
   * on the account page, where it made the whole page scroll sideways. The
   * grid version of this rule already said `max-width: 100%` for exactly this
   * reason; a select outside a grid needs it just as much.
   */
  max-width: min(22rem, 100%);
}

/* A number is a handful of digits. */
input[type="number"] {
  max-width: 10rem;
}

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="password"],
input[type="search"] {
  max-width: 32rem;
}

/* Inside a grid or the auth card, the container already decides the width. */
.field-row select,
.field-row input,
.auth-card input,
.search-row input {
  max-width: none;
}
/* …but a select is `width: auto` and sizes to its longest option, so what
   the container decides is only the CEILING. `none` here lifted the base
   rule's `min(22rem, 100%)` altogether, and the signup card's country list
   ("South Georgia & South Sandwich Islands", 380px at 16px) held /signup
   404px wide on a 390px phone: the only page in the sweep of 2026-09-23 that
   scrolled sideways. The 22rem half of the cap stays lifted — the card is
   the measure — and the row's edge is put back. */
.auth-card select { max-width: 100%; }

/* A textarea is for prose and should use the width it is given, but not grow
   past a comfortable reading measure on a wide screen. */
textarea { max-width: 46rem; }
.program-source, .freetext { max-width: none; }

/* The radio-chip rows (reply channel, unit toggles) are inline controls, not
   full-width fields — the design draws them as a row of pills. */
.choice-row { max-width: 100%; }

/* ---- shell, settled -------------------------------------------------
 *
 * The account page was measuring 32px wide with the body scrolling sideways.
 * The shell had accumulated three overlapping attempts at the same thing —
 * a flex body, per-screen max-widths on <main> itself, and padding pushed onto
 * `main > *` — and between them <main> could end up sized by its content
 * rather than its container.
 *
 * One rule each, stated last so they win: the surface fills the shell, and the
 * page class caps the CONTENT rather than the surface.
 */
body { display: block; }

main {
  display: block;
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
  padding-inline: 0;
}

main > * { padding-inline: var(--shell-pad); }

/* Per-screen measures. Applied to the page class, which sits on <main>, so the
   cap is on how wide the content reads — not on the surface behind it. */
.page-home            { max-width: 880px;  margin-inline: auto; }
.page-request-packages{ max-width: 1000px; margin-inline: auto; }
.page-request-payment { max-width: 760px;  margin-inline: auto; }
.page-request-intake  { max-width: 900px;  margin-inline: auto; }
.page-account         { max-width: 900px;  margin-inline: auto; }
.page-programs        { max-width: 760px;  margin-inline: auto; }
.page-notifications   { max-width: 760px;  margin-inline: auto; }
.page-contact         { max-width: 760px;  margin-inline: auto; }
.page-legal           { max-width: 760px;  margin-inline: auto; }

/* Full-bleed screens keep the whole shell. */
.page-login, .page-signup, .page-telegram-wait, .page-program {
  max-width: none;
  padding-block-start: 0;
}
.page-login > *, .page-signup > *, .page-telegram-wait > *, .page-program > * {
  padding-inline: 0;
}

/* ---- navigation: inline on desktop, a menu on mobile -----------------
 *
 * A <details> disclosure rather than a scripted menu: the CSP forbids inline
 * script, and the main navigation is the last thing that should depend on JS
 * having loaded.
 *
 * The <nav> is a SIBLING of the <details>, never a child. A closed <details>
 * hides its contents through the ::details-content pseudo-element with
 * content-visibility: hidden, and no author `display` rule on the child wins
 * against that — nesting the nav collapsed the entire desktop header to a
 * zero-width box, links present in the DOM and nothing visible on screen.
 *
 * Kept outside, the nav is governed only by our own rules and the browser's
 * job is reduced to toggling [open], which the ~ combinator reads. On desktop
 * the <details> is removed outright and the nav lays out inline.
 */

.nav-menu { display: none; }

@media (max-width: 767px) {
  /* The panel is positioned against the header row, not the burger, so it
     spans to the row's edge rather than the button's. */
  .shell-row, .coach-header { position: relative; }

  .nav-menu {
    display: block;
    /* An open menu must not stretch the header row. */
    flex: none;
  }

  .nav-menu > summary {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: var(--surface-2);
    cursor: pointer;
    list-style: none;
  }
  /* Safari draws its own triangle without this. */
  .nav-menu > summary::-webkit-details-marker { display: none; }

  .nav-burger,
  .nav-burger::before,
  .nav-burger::after {
    display: block;
    width: 16px;
    height: 2px;
    background: var(--text);
    border-radius: 2px;
  }
  .nav-burger { position: relative; }
  .nav-burger::before, .nav-burger::after { content: ''; position: absolute; }
  .nav-burger::before { top: -5px; }
  .nav-burger::after  { top: 5px; }

  /* Closed: the nav is not in the layout at all. Open: it becomes the panel.
     inset-inline-end rather than right, so it opens from the correct edge on
     an RTL page. */
  .nav-menu ~ nav { display: none; }

  .nav-menu[open] ~ nav {
    position: absolute;
    inset-block-start: calc(100% + 8px);
    inset-inline-end: 0;
    z-index: 40;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    min-width: 13rem;
    padding: 6px;
    border: 1px solid var(--border-strong);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 18px 40px rgba(0, 0, 0, .5);
  }

  /* The client panel hangs off .shell-row, which already sits inside the
     header's padding. The coach panel hangs off .coach-header itself, so it
     needs that padding adding back or it opens flush to the screen edge. */
  .coach-header .nav-menu[open] ~ nav { inset-inline-end: 1.25rem; }

  .nav-menu[open] ~ nav a,
  .nav-menu[open] ~ nav .linklike {
    display: block;
    padding: 10px 12px;
    border-radius: 8px;
    text-align: start;
    width: 100%;
  }
  .nav-menu[open] ~ nav a:hover { background: var(--surface-2); }

  /* THE TOGGLE IS NOT IN THE PANEL ANY MORE. It used to be the nav's last
     item, so on a phone it was inside the collapsed menu; it is a sibling of
     the nav now and stays on the header row. The rules that made it behave
     inside the open panel went with it.

     Ordered rather than moved in the markup, because `.nav-menu ~ nav` needs
     the <details> to come first in source: brand, then the toggle, then the
     burger, with the hidden nav after them. */
  .site-header .brand { order: 1; }
  .site-header .lang-toggle { order: 2; margin-inline-start: auto; }
  .site-header .nav-menu { order: 3; }
  .site-header nav { order: 4; }

  /* A BIGGER TARGET, now that it is a control on the header row rather than a
     full-width row inside a menu. Measured at 390px it was 35x22, which is a
     thumb's worth of miss; the padding takes the pill to the burger's height
     beside it without changing the type or the desktop pill.

     touch-action so a quick second tap switches back instead of being eaten as
     a double-tap zoom — this is a control people press twice while deciding. */
  .site-header .lang-toggle {
    padding: 4px;
    touch-action: manipulation;
  }
  .site-header .lang-toggle .lang {
    display: inline-flex;
    align-items: center;
    min-height: 30px;
    padding: 4px 12px;
  }
  /* It is a link, and on this screen it is the one that fixes everything, so
     it gets a ring of its own rather than relying on the UA's. */
  .site-header .lang-toggle .lang:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}

/* ---- coach tables: stop data wrapping mid-value ---------------------- */

/* An email or a timestamp breaking across two lines is what made these pages
   look misaligned. These are single tokens: they either fit or the table
   scrolls, but they never wrap.

   Scoped to .data-table, NOT .exercise-table. The exercise table is fixed
   layout by design, and a nowrap cell in a fixed-layout table cannot widen
   its column — it just paints over the next one. */
.data-table td.ltr-data,
.data-table td .ltr-data,
.data-table td.num {
  white-space: nowrap;
}

/* Every cell aligned to the top, so a wrapped name in one column does not
   leave its neighbours floating in the middle of the row. */
.exercise-table td, .meal-table td, .data-table td { vertical-align: top; }

/* Table headers were inheriting the uppercase treatment inconsistently. */
.exercise-table th, .data-table th { white-space: nowrap; vertical-align: bottom; }

/* Price inputs sit next to their formatted value; keep them on one line. */
.data-table td input[type="text"] { max-width: 10rem; display: inline-block; }
.data-table td .muted.small { white-space: nowrap; margin-inline-start: .5rem; }

/* ---- short data tokens must not wrap mid-value -----------------------
 *
 * Generalised from the coach tables, where an email breaking across two lines
 * was what made the page look misaligned. The same shape appears in every
 * metadata row: a date, an id, an email, a count. These are single tokens —
 * they either fit or their container scrolls, but they never break.
 *
 * Scoped to the CONTAINERS that hold short values, deliberately not to
 * .ltr-data generally: that class is also on program markdown, stack traces
 * and Telegram deep links, where nowrap would force a horizontal scrollbar
 * across the whole page.
 */
.program-row .ltr-data,
.error-head .ltr-data,
.status-meta .ltr-data,
.meal-row .ltr-data,
.seg-labels .ltr-data,
.program-meta .ltr-data,
.badge .ltr-data {
  white-space: nowrap;
}

/* A field pill is the ONE place that keeps wrapping.
 *
 * It was in the nowrap list with an ellipsis, which looked tidy and quietly
 * truncated a client's own email address on their own account page — with no
 * way to read the rest of it. A pill is a box of its own, so a value on two
 * lines costs nothing; the row-misalignment this rule exists to prevent is not
 * possible here. Show the whole thing. */
.field-pill .field-value {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Metadata rows wrap between ITEMS, never inside one. */
.error-head, .status-meta, .program-meta { flex-wrap: wrap; }

/* The one place nowrap would be wrong: long-form content keeps wrapping. */
.freetext .ltr-data,
.program-source,
.error-list pre,
.legal .ltr-data,
.auth-card .ltr-data {
  white-space: normal;
  overflow-wrap: anywhere;
}
/* …except a preformatted stack trace, which scrolls in its own box. */
.error-list pre { white-space: pre; }

/* ============================================================== tabs
 *
 * CSS-only tabs: a group of radios, a bar of labels pointing at them, and one
 * panel per radio revealed by a sibling selector.
 *
 * No JavaScript, because the CSP forbids inline script and a form's own
 * navigation should not depend on a script having loaded. Radios also arrive
 * with keyboard support already done — arrow keys move between them, which is
 * exactly the interaction a tab list is supposed to have.
 *
 * The pairing rule below relies on the panels being the ONLY <div> children of
 * .tabs (the bar is a <nav>), so :nth-of-type counts panels and nothing else.
 * Put another div in there and tab 2 starts showing panel 1.
 */

.tabs { margin-block-start: 18px; }

/* Visually hidden, still focusable, still the thing the browser checks.
   display:none would drop them out of the tab order entirely. */
.tabs > input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/*
 * Bookmark tabs, the same shape as the coach section tabs: the row sits ON the
 * panel, the active tab gives up its bottom border and takes the panel's
 * surface, and the -1px pulls them together into one piece of paper.
 *
 * inline-flex so the row is as wide as its tabs — stretched across the card,
 * five tabs sat in a sea of empty background.
 */
.tab-bar {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 3px;
  margin-block-end: -1px;
  position: relative;
  z-index: 1;
}

.tab-label {
  /* Overrides the global form `label` block/margin, which would otherwise
     stack these vertically and undo the whole row. */
  display: inline-block;
  margin: 0;
  padding: 8px 15px;
  border: 1px solid var(--border-strong);
  border-block-end: none;
  border-radius: 9px 9px 0 0;
  background: var(--panel);
  color: var(--secondary);
  font-weight: 500;
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
}
.tab-label:hover { color: var(--text); background: var(--surface-2); }

/* The panel each tab opens onto. */
.tabs > .tab-panel {
  display: none;
  padding: 20px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  border-start-start-radius: 0;
  background: var(--surface-2);
}

.tab-panel-sub {
  margin: 0 0 18px;
  font-size: 13px;
  color: var(--muted);
}

/*
 * One pair of rules per tab index. Written out rather than generated because
 * CSS cannot count — six is well past the three groups that exist today and
 * costs nothing if unused.
 */
.tabs > input:nth-of-type(1):checked ~ .tab-bar .tab-label:nth-of-type(1),
.tabs > input:nth-of-type(2):checked ~ .tab-bar .tab-label:nth-of-type(2),
.tabs > input:nth-of-type(3):checked ~ .tab-bar .tab-label:nth-of-type(3),
.tabs > input:nth-of-type(4):checked ~ .tab-bar .tab-label:nth-of-type(4),
.tabs > input:nth-of-type(5):checked ~ .tab-bar .tab-label:nth-of-type(5),
.tabs > input:nth-of-type(6):checked ~ .tab-bar .tab-label:nth-of-type(6) {
  /* Matches the panel exactly, and one pixel taller so it overlaps the
     panel's border and erases the line beneath itself. */
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
  padding-block-end: 9px;
}

.tabs > input:nth-of-type(1):checked ~ .tab-panel:nth-of-type(1),
.tabs > input:nth-of-type(2):checked ~ .tab-panel:nth-of-type(2),
.tabs > input:nth-of-type(3):checked ~ .tab-panel:nth-of-type(3),
.tabs > input:nth-of-type(4):checked ~ .tab-panel:nth-of-type(4),
.tabs > input:nth-of-type(5):checked ~ .tab-panel:nth-of-type(5),
.tabs > input:nth-of-type(6):checked ~ .tab-panel:nth-of-type(6) {
  display: block;
}

/* Keyboard focus has to be visible on the BAR, since the radio itself is
   invisible — otherwise tabbing into the group looks like nothing happened. */
.tabs > input:focus-visible ~ .tab-bar {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 767px) {
  /*
   * On a phone the bookmarks stop being bookmarks.
   *
   * Six tabs cannot fit across 375px, so they used to scroll sideways — which
   * hid half of them behind a gesture nobody knows is available, on the one
   * screen where a client is most likely to be looking for a specific section.
   *
   * They become a two-column grid instead: every section visible at once, no
   * horizontal scroll, nothing clipped. The folder-tab join is dropped with
   * them, because a tab in the middle of a grid cannot join the panel below
   * and pretending otherwise just looks broken — so the row detaches, squares
   * off its corners, and reads as what it now is: a chooser.
   */
  .tab-bar {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
    /* Undo the bookmark overlap — nothing is being joined to any more. */
    margin-block-end: 12px;
    z-index: auto;
  }

  .tab-label {
    /* Squared off, and centred: as a grid cell it is a button, not a tab. */
    border-radius: 8px;
    border-block-end: 1px solid var(--border-strong);
    padding: 10px 8px;
    font-size: 12.5px;
    text-align: center;
    white-space: normal;
    /*
     * AND 44px TALL, which it was two pixels short of.
     *
     * 10px of padding on 12.5px text at the body's 1.6 leading plus the two
     * 1px borders this state adds back measures 42px — the QA run's finding
     * of 2026-09-01 ("the account page's tab control at 42px"). This is the
     * phone state of the one control that moves between the account page's
     * six sections, so the 44px mobile minimum is exactly the case it is
     * written for.
     *
     * The flex box centres the label in the taller cell; `text-align` still
     * centres a label that wraps to two lines inside it.
     */
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    touch-action: manipulation;
  }

  /* The active one no longer has to match the panel it opens, so it can go
     back to being clearly selected. */
  .tabs > input:nth-of-type(1):checked ~ .tab-bar .tab-label:nth-of-type(1),
  .tabs > input:nth-of-type(2):checked ~ .tab-bar .tab-label:nth-of-type(2),
  .tabs > input:nth-of-type(3):checked ~ .tab-bar .tab-label:nth-of-type(3),
  .tabs > input:nth-of-type(4):checked ~ .tab-bar .tab-label:nth-of-type(4),
  .tabs > input:nth-of-type(5):checked ~ .tab-bar .tab-label:nth-of-type(5),
  .tabs > input:nth-of-type(6):checked ~ .tab-bar .tab-label:nth-of-type(6) {
    background: var(--accent);
    border-color: var(--accent);
    color: #1a1204;
    padding-block-end: 10px;
  }

  .tabs > .tab-panel { border-start-start-radius: var(--radius); }
}

/* ==================================================== coach section nav
 *
 * Two rows instead of one long list of ten links.
 *
 * The top row is the sections a coach is allowed into; the second is the pages
 * inside the current one. Both are server-rendered from src/lib/coach-nav.js,
 * which has already dropped anything this coach lacks the capability for — so
 * an assistant coach sees a shorter bar, not a greyed-out one.
 *
 * Server-rendered rather than the CSS-only tab trick used on the account page,
 * because these are real pages with real URLs. A coach must be able to
 * bookmark the queue, and the browser back button has to work.
 */

.coach-sections {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
}

.coach-section {
  padding: 7px 14px;
  border-radius: 8px;
  color: var(--secondary);
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
}
.coach-section:hover { color: var(--text); background: var(--surface-2); }
.coach-section.is-current { background: var(--accent); color: #1a1204; }

/*
 * Bookmark tabs: the section's pages, sitting ON the card rather than in a
 * band of their own.
 *
 * The first cut was a full-width strip under the header. It floated free of
 * the content it belonged to and read as a second navigation bar — two bars
 * stacked, neither obviously attached to the page.
 *
 * These live inside <main>, so they take the page's width and padding, and the
 * active tab gives up its bottom border and takes the card's surface colour.
 * The -1px margin pulls the row down onto the card's top border so the two
 * become one continuous shape, the way a folder tab meets its folder.
 */

.coach-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 3px;
  /* Sits the row on the card's top border rather than above it. */
  margin-block-end: -1px;
  position: relative;
  z-index: 1;
}

.coach-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 15px;
  border: 1px solid var(--border);
  /* No bottom border on any tab: the card's own edge is the line they sit on. */
  border-block-end: none;
  border-radius: 9px 9px 0 0;
  background: var(--panel);
  color: var(--secondary);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
}
.coach-tab:hover { color: var(--text); background: var(--surface-2); }

/*
 * The current tab IS the card. Same surface, and one pixel taller so it
 * overlaps the card's border and erases the line beneath itself — which is
 * what makes the tab and the panel read as one piece of paper.
 */
.coach-tab.is-current {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
  font-weight: 600;
  padding-block-end: 9px;
}

/*
 * The panel the tabs sit on.
 *
 * Most coach views open with <section class="card">, which already is one. The
 * queue and the reference pages open with a plain <section> and had nothing
 * for the tabs to attach to — they floated above the content, which is the
 * look this was meant to replace. Those get the card's surface here rather
 * than by editing each view, so a new coach page is attached by default
 * instead of by remembering.
 *
 * Either way the top-inline-start corner is squared off, so the card meets the
 * first tab rather than curving away from underneath it.
 */
.coach-tabs + .card,
.coach-tabs + section:not(.card):not(.coach-cols) {
  border-start-start-radius: 0;
}

.coach-tabs + section:not(.card):not(.coach-cols) {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  border-start-start-radius: 0;
  padding: 1.75rem;
}

/* The two-column screens keep their own layout; the tabs simply sit on the
   line above them rather than wrapping them in a box they were not designed
   to live in. */
.coach-tabs + .coach-cols {
  padding-block-start: 22px;
  border-block-start: 1px solid var(--border);
}

@media (max-width: 767px) {
  /* Scrolls sideways rather than wrapping: a wrapped second row of tabs would
     sit above the card with a visible seam between the rows, which is exactly
     the joined look this is for. */
  .coach-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .coach-tabs::-webkit-scrollbar { display: none; }
}
}

/* ======================================================= progress photos
 *
 * Built from the design's isPhotos screen: 3/4 pose cards in an auto-fit grid,
 * dashed while a pose is missing and solid once it is filled, with the state
 * badge pinned top-start.
 */

.photos-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  justify-content: space-between;
}

/*
 * The swap region, and the one place on this screen it costs anything.
 *
 * Uploads happen in place here (owner's §16.1), and swap.js replaces the
 * contents of ONE element — so everything a photo can change has to sit inside
 * one box: the counter, both pose grids, the save-set control, and the history
 * a sealed sitting moves into. That box is a plain <div> between <main> and the
 * screen's own cards, and the shell rules up at "shell, settled" address those
 * cards as `main > .card`. One level down, they stopped matching: no side
 * padding, no border, no surface — the cards would render as bare text on the
 * page background.
 *
 * So the same declarations are repeated through the wrapper, transcribed from
 * `main > section.card` rather than reinvented, and SCOPED TO THIS PAGE. The
 * generic version was written first and thrown away: records.eta's region wraps
 * boxes with their own surfaces, and moving the shell padding from the wrapper
 * onto each child would have run their backgrounds to the edge of the screen.
 *
 * The wrapper's own padding is cancelled first, or `main > *` gives it 24px and
 * the children get 24px more inside that.
 */
.page-photos > [data-swap-region] { padding-inline: 0; }
.page-photos > [data-swap-region] > * { padding-inline: var(--shell-pad); }

.page-photos > [data-swap-region] > section.card {
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}
.page-photos > [data-swap-region] > section.card + section.card {
  margin-block-start: 18px;
}
/* Clears the footer, exactly as `main > .card:last-of-type` did. */
.page-photos > [data-swap-region] > section.card:last-of-type {
  margin-block-end: 40px;
}
.photos-lede {
  margin: 7px 0 0;
  max-width: 52ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--secondary);
}

.photoset-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
  padding: 13px 16px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--panel);
  font-size: 13.5px;
}
.photoset-count { font-weight: 500; font-size: 13px; color: var(--accent); }

/* Saving a whole sitting, on the client's own screen. 44px like every other
   tap target — `btn-small` alone measures 39 — and `margin-inline-start: auto`
   so the link sits at the end of the bar in both directions, which is what
   keeps it off the date in Farsi as well as in English. */
.photoset-dl {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-inline-start: auto;
  touch-action: manipulation;
}

.pose-grid {
  margin-block-start: 14px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
}

.pose-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px;
  border-radius: 11px;
  /* Dashed until the pose is shot: the card reads as an empty slot rather than
     as a filled one that failed to load. */
  border: 1px dashed var(--border-strong);
  background: var(--panel);
  min-height: 220px;
  overflow: hidden;
}
.pose-card.is-filled {
  border-style: solid;
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.pose-state {
  align-self: flex-start;
  margin-block-end: auto;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--accent-border);
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  /* Above the thumbnail, which fills the card behind it. */
  position: relative;
  z-index: 1;
}
.pose-state.is-done {
  border-color: var(--success-border);
  background: var(--success-bg);
  color: var(--success);
}

/*
 * The thumbnail fills the card, with the label and buttons over it. object-fit
 * cover so a portrait phone photo is cropped rather than letterboxed into a
 * shape that makes every client's set look different.
 */
.pose-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.pose-card.is-filled .pose-meta,
.pose-card.is-filled .pose-actions {
  position: relative;
  z-index: 1;
}
/* A scrim, so white text stays readable over a bright photo. */
.pose-card.is-filled::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(8, 9, 11, .1) 40%, rgba(8, 9, 11, .88) 100%);
}
.pose-card.is-filled .pose-meta,
.pose-card.is-filled .pose-actions { z-index: 2; }
.pose-card.is-filled::after { z-index: 0; }

.pose-label { font-weight: 600; font-size: 13.5px; color: var(--text); }
.pose-hint { margin-block-start: 2px; font-size: 11.5px; color: var(--secondary); }

/*
 * EVERY CONTROL ON A POSE CARD IS 44px.
 *
 * The delete was `.linklike.danger` — `padding: 0`, about 23px tall, under a
 * thumbnail in a grid on a phone, and one tap from a delete that unlinks the
 * file. It is a real button now (F26), and the two beside it match: a 44px
 * delete next to a 38px Upload would read as a mistake, and .btn.btn-small was
 * under the mobile floor here too. Scoped to this row, so the small buttons
 * everywhere else are the touch-target pass's business and not this one's.
 */
.pose-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.pose-actions .btn { min-height: 44px; }
/*
 * The danger colour lives on the border and the text, not on a fill: it sits
 * over a photograph, and a solid red box over a client's own body shot reads
 * as an alarm rather than as an action they can choose not to take.
 */
.pose-delete {
  border: 1px solid var(--danger);
  background: transparent;
  color: var(--danger);
}
.pose-delete:hover {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  filter: none;
}
.pose-form { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0; }

/*
 * The file input is visually hidden inside its own label, so the label IS the
 * button. A bare file input cannot be styled, and the CSP forbids the inline
 * script that would otherwise proxy a click onto it.
 */
.pose-pick { position: relative; overflow: hidden; cursor: pointer; margin: 0; }
.pose-pick input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  cursor: pointer;
}

.pose-extra { margin-block-start: 18px; }
.pose-extra > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 13.5px;
  color: var(--secondary);
  padding: 8px 0;
}
.pose-extra > summary:hover { color: var(--text); }

.photoset-seal {
  margin-block-start: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.photos-howto .howto-grid {
  margin-block-start: 11px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--secondary);
}

/* A finished set, as a scrolling strip — the coach's view and the client's
   history both use it. */
.photoset { margin-block-start: 18px; }
.photo-strip {
  margin-block-start: 10px;
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-block-end: 4px;
}
.photo-strip-item { margin: 0; flex: none; width: 150px; }
.photo-strip-item img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  display: block;
}
.photo-strip-item figcaption {
  margin-block-start: 5px;
  font-size: 11.5px;
  color: var(--muted);
  text-transform: capitalize;
}

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

/* ------------------------------------------------------- photo compare */

.photos-tabs { margin-block-start: 18px; }

.compare-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 10px;
  margin-block-end: 18px;
}
.compare-picker label { margin: 0 0 4px; font-size: 12.5px; color: var(--secondary); }
.compare-picker .btn { margin: 0; }

/*
 * Two columns that stay the same size whether or not a pose exists in that
 * set — if one column collapses the dates stop lining up, which is the one
 * thing a comparison has to get right.
 */
.compare-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
}
.compare-col { min-width: 0; }
.compare-date { font-weight: 600; font-size: 12.5px; color: var(--secondary); margin-block-end: 9px; }
.compare-col img,
.compare-empty {
  width: 100%;
  aspect-ratio: 3 / 4;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  object-fit: cover;
  display: block;
}
.compare-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  border-style: dashed;
  background: var(--panel);
  color: var(--muted);
  font-size: 12.5px;
  text-align: center;
  padding: 12px;
}

/*
 * The stance instruction. One line above the grid rather than a hint inside
 * each card: the four cards were carrying four rewordings of the same thing,
 * and because they were different lengths they wrapped differently and left
 * the pose labels sitting at different heights across the row.
 */
.pose-note {
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--secondary);
  max-width: 62ch;
}

/* The second press, removed only once the script that replaces it is running.
   pwa.js adds .is-one-press; without it this stays visible and the form is the
   two-step version it has always been. */
.pose-form.is-one-press .pose-submit { display: none; }

/* A form whose upload is in flight.
 *
 * swap.js adds this while a POST is on the wire and takes it off again when the
 * answer arrives — which on success is the moment the form is replaced anyway,
 * so what this really covers is the seconds in between. A 2MB photo on an
 * Iranian mobile connection can sit there for several of them, and a button
 * that looks exactly as it did before the press is a button people press again.
 *
 * Generic on purpose: it lands on a pose form, the coach's add-a-sitting form,
 * a payment receipt and the coach's chat reply, which share no other class. The
 * greying is the signal; `pointer-events: none` on the controls is what
 * actually stops the second press, since a real <form disabled> does not exist
 * and disabling each field would drop them out of the FormData that is already
 * being sent.
 *
 * `textarea` is on the list for the chat reply: the answer to that post is a
 * fragment carrying a fresh, empty composer, so words typed while the previous
 * reply is on the wire would be replaced mid-sentence. */
.is-uploading {
  opacity: .55;
  cursor: progress;
}
.is-uploading .btn,
.is-uploading button,
.is-uploading label,
.is-uploading textarea,
.is-uploading input { pointer-events: none; }

/* ------------------------------------------------------------- footer
 *
 * The design splits the footer in two: where to find the business, drawn as
 * bordered pills, and the pages of the site, drawn as plain links. They were
 * one flat row here, which made Terms read like a social channel.
 */

.site-footer .shell-row {
  align-items: center;
  gap: 20px;
}

.footer-social { display: flex; flex-wrap: wrap; gap: 8px; }
.footer-social a {
  padding: 7px 13px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
  font-size: 12px;
}
.footer-social a:hover { border-color: var(--accent-border); color: var(--accent); }

.footer-links { display: flex; flex-wrap: wrap; gap: 16px; }
.footer-links a {
  color: var(--secondary);
  text-decoration: none;
  font-weight: 500;
  font-size: 13px;
}
.footer-links a:hover { color: var(--text); }

/* The design's exact band. */
.site-footer {
  padding: 26px 24px 20px;
  text-align: start;
  background: #0d1014;
  border-block-start: 1px solid var(--border);
}

@media (max-width: 767px) {
  /* Stacked, and the pills stay tappable rather than shrinking to fit. */
  .site-footer .shell-row { flex-direction: column; align-items: flex-start; gap: 14px; }
  .site-footer { padding: 20px 16px 18px; }
}

/* The dialling code sits against the number, not above it — one field split in
   two, which is how the design draws it and how a phone number reads. */
.phone-row { display: flex; gap: 8px; align-items: stretch; }
.phone-row select { flex: 0 0 7rem; min-width: 0; }
.phone-row input { flex: 1 1 auto; min-width: 0; max-width: none; }

.field-help { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }

/* ------------------------------------------------------- under construction */
.uc { text-align: center; }
.uc-mark { font-size: 40px; line-height: 1; margin-block-end: 12px; }
.uc h1 { margin: 0 0 6px; }

/* The Google button, as the design draws it: a white circular G, then the label. */
.auth-google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  margin-block-start: 8px;
  text-decoration: none;
}
/* "· soon" on the Google button, dimmed the way the footer dims Shop and
   Guides — the promise stays visible without the control pretending to work. */
.auth-soon { color: var(--muted); font-size: .85em; font-weight: 400; }
.google-g {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  color: #1a1204;
  font-weight: 700;
  font-size: 11px;
}

/* Designed but unbuilt: dimmer than a live link, so "soon" reads as a state
   rather than as decoration on something that works. */
.footer-links a.footer-soon { color: var(--muted); }
.footer-links a.footer-soon:hover { color: var(--secondary); }

/*
 * Form rhythm inside the auth card.
 *
 * A standalone label carries margin-block-start: 15px, but a .field-row — the
 * two-column pairs, first/last name and password/confirm — carried none. So
 * every single field had a gap above it and every paired row had none, which
 * is what made the create-account screen read as unevenly spaced rather than
 * as a form.
 *
 * The row now takes the same 15px, and the first one takes none because it
 * opens the form.
 */
.auth-card form > .field-row,
.auth-card form > .phone-row { margin-block-start: 15px; }
.auth-card form > .field-row:first-of-type { margin-block-start: 0; }

/* A label already provides the 6px above its own control. */
.auth-card form > label + .phone-row { margin-block-start: 6px; }

/* Both provider buttons share one shape, so Google and Telegram cannot drift
   apart the way they did when each carried its own rules. */
.auth-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  margin-block-start: 8px;
  text-decoration: none;
}
.tg-mark { display: inline-flex; align-items: center; color: #2aabee; }

/* ============================================== account: request history
 *
 * The design's timeline: one card per request or program, newest first, with
 * three tiles inside a request card. A rail can only ever describe the newest
 * one; this is the history behind it.
 */

.req-cards { display: flex; flex-direction: column; gap: 12px; margin-block-start: 14px; }

.req-card {
  padding: 20px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}

/* Superseded: dimmed, not hidden. A client should be able to find an old block
   without it competing with the current one. */
.req-card.is-superseded { background: var(--panel); opacity: .75; }

.req-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
}
.req-title { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.req-name { font-weight: 600; font-size: 15px; }
.req-date { font-size: 13px; color: var(--muted); }

.req-tiles { margin-block-start: 16px; display: flex; flex-wrap: wrap; gap: 8px; }
.req-tile {
  flex: 1 1 120px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--panel);
}
.req-tile-k { font-size: 11.5px; color: var(--muted); }
.req-tile-v { margin-block-start: 2px; font-weight: 600; font-size: 13px; }
.req-tile-v.is-ok { color: var(--success); }
.req-tile-v.is-warn { color: var(--accent); }
.req-tile-v.is-bad { color: var(--danger); }

.req-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 14px; }
.req-actions .btn { margin: 0; }

/* The design's third button style: bordered, no fill. */
.btn-ghost {
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--secondary);
}
/*
 * The background has to be reset explicitly.
 *
 * .btn-ghost also carries .btn, and .btn:hover paints a solid gold background.
 * Setting only colour and border here left the gold underneath, so the two
 * download buttons flashed a colour that appears nowhere else in the scheme.
 */
.btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--accent-border);
}

/* ------------------------------------------------ account: profile summary */

.summary-grid {
  margin-block-start: 14px;
  padding: 20px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
}
.summary-k { font-size: 12px; color: var(--muted); }
.summary-v { margin-block-start: 3px; font-size: 13.5px; color: var(--text); }
.summary-edit { margin-block-start: 16px; }

.crumb { margin: 0 0 14px; font-size: 13px; }
.crumb a { color: var(--secondary); text-decoration: none; }
.crumb a:hover { color: var(--text); }

/*
 * A .field inside a .field-row must not carry its own block margin.
 *
 * The row spaces its children with `gap` and is itself spaced by the rule
 * above; the inner .field was ALSO contributing a 1.35rem bottom margin, so
 * every paired row — first/last name, password/confirm — sat about 22px lower
 * than a plain label would. That is why the create-account screen read as
 * airier than the login screen beside it, which has no paired rows at all.
 */
.field-row > .field { margin-block: 0; }

/* A disabled field: the same shape as its neighbours, visibly not typable.
   Dimmed rather than restyled, so it still reads as part of the form. */
input:disabled, select:disabled, textarea:disabled {
  opacity: .6;
  cursor: not-allowed;
  background: var(--panel);
  color: var(--secondary);
}

/* ==================================================== gym mode
 *
 * The design's isGym screen. A phone, in a gym, one thumb — so the tap targets
 * are large, the header sticks, and the darker ground (#0a0c0f) is deliberate:
 * this is the one screen read at arm's length under gym lighting.
 */

body.gym { background: #0a0c0f; min-height: 100vh; }

/* The <main> landmark of gym-layout (audit F3). It replaces nothing — the body
   was the parent before — so it only has to not become a new box: no padding,
   no width of its own, and `display: block` as the body's own children were. */
.gym-main {
  display: block;
  /* Same opt-out as .team-main, and for the same reason — see the comment
     there. "No width of its own" is not what `display: block` alone says when
     a bare `main` rule is already carrying a width and a centring margin.
     Nor is "no padding": this block said it in prose and never declared it, so
     the day screen carried the same 32px band the dashboard did, between the
     sticky head and the first card. Measured, both, 2026-09-11. */
  max-width: none;
  margin-inline: 0;
  padding-block: 0;
}
.gym-main > * { padding-inline: 0; }

.gym-head {
  position: sticky;
  top: 0;
  z-index: 5;
  padding: 16px 20px;
  border-block-end: 1px solid var(--border);
  background: var(--panel);
}
.gym-head-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.gym-day {
  font-weight: 800;
  font-size: 12px;
  letter-spacing: .14em;
  color: var(--accent);
  text-transform: uppercase;
}
.gym-focus { margin-block-start: 3px; font-weight: 700; font-size: 20px; }
.gym-count { text-align: end; }
.gym-count-k { font-size: 11.5px; color: var(--muted); }
.gym-count-v { font-weight: 700; font-size: 20px; }

.gym-progress {
  margin-block-start: 12px;
  height: 4px;
  border-radius: 2px;
  /* --border (#21262f), not --border-strong: the design uses the softer line
     here so the gold fill reads against it rather than competing. */
  background: var(--border);
  overflow: hidden;
}
/*
 * Filled by SCALE, not by width (audit F51 — "NEVER animate layout props").
 * The bar is full width and squashed down to the fraction that is done; gym.js
 * moves it between sets, so unlike the intake bar this transition really does
 * run, on the screen held in one hand mid-workout.
 *
 * `border-radius` is deliberately not here any more: a scaled box scales its
 * corners with it, so a 2px radius at 20% becomes a 0.4px one. The rounding
 * comes from `.gym-progress`, which already has the radius AND the
 * `overflow: hidden` that clips to it.
 *
 * `transform-origin` is physical because transform-origin has no logical
 * keywords — hence the RTL rule beside it. Without it a Farsi client's bar
 * fills from the wrong end.
 */
.gym-progress-bar {
  height: 100%;
  width: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
}
html[dir="rtl"] .gym-progress-bar { transform-origin: right; }
/* Fractions in 5% steps: the CSP blocks inline style attributes, and twenty
   rules is plenty of resolution for a progress bar. */
.gym-progress-bar[data-progress="0"]   { transform: scaleX(0); }
.gym-progress-bar[data-progress="5"]   { transform: scaleX(0.05); }
.gym-progress-bar[data-progress="10"]  { transform: scaleX(0.1); }
.gym-progress-bar[data-progress="15"]  { transform: scaleX(0.15); }
.gym-progress-bar[data-progress="20"]  { transform: scaleX(0.2); }
.gym-progress-bar[data-progress="25"]  { transform: scaleX(0.25); }
.gym-progress-bar[data-progress="30"]  { transform: scaleX(0.3); }
.gym-progress-bar[data-progress="35"]  { transform: scaleX(0.35); }
.gym-progress-bar[data-progress="40"]  { transform: scaleX(0.4); }
.gym-progress-bar[data-progress="45"]  { transform: scaleX(0.45); }
.gym-progress-bar[data-progress="50"]  { transform: scaleX(0.5); }
.gym-progress-bar[data-progress="55"]  { transform: scaleX(0.55); }
.gym-progress-bar[data-progress="60"]  { transform: scaleX(0.6); }
.gym-progress-bar[data-progress="65"]  { transform: scaleX(0.65); }
.gym-progress-bar[data-progress="70"]  { transform: scaleX(0.7); }
.gym-progress-bar[data-progress="75"]  { transform: scaleX(0.75); }
.gym-progress-bar[data-progress="80"]  { transform: scaleX(0.8); }
.gym-progress-bar[data-progress="85"]  { transform: scaleX(0.85); }
.gym-progress-bar[data-progress="90"]  { transform: scaleX(0.9); }
.gym-progress-bar[data-progress="95"]  { transform: scaleX(0.95); }
.gym-progress-bar[data-progress="100"] { transform: scaleX(1); }

.gym-units { margin-block-start: 12px; display: flex; align-items: center; gap: 8px; }
.gym-units-k { font-size: 11.5px; color: var(--muted); }
.gym-unit-tabs { display: flex; gap: 4px; padding: 3px; border-radius: 9px; background: #1a1e26; }
.gym-unit {
  padding: 5px 12px;
  border-radius: 7px;
  font-weight: 600;
  font-size: 12px;
  color: var(--secondary);
  text-decoration: none;
}
.gym-unit.is-current { background: var(--accent); color: #1a1204; }

.gym-body {
  padding: 16px 16px 90px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 640px;
  margin-inline: auto;
}

.gym-offline {
  margin: 0;
  padding: 9px 12px;
  border: 1px solid var(--accent-border);
  border-radius: 9px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12.5px;
}

.gym-card {
  padding: 16px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}
.gym-card-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.gym-name { font-weight: 600; font-size: 16.5px; }
.gym-tag {
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--accent-bg);
  border: 1px solid var(--accent-border);
  color: var(--accent);
  font-weight: 600;
  font-size: 11px;
}
.gym-meta {
  margin-block-start: 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 13px;
  color: var(--secondary);
}
.gym-sep, .gym-meta .gym-last { color: var(--muted); }
.gym-last { color: #c9ced6; }

.gym-sets { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 14px; }
.gym-set { display: flex; flex-direction: column; align-items: center; gap: 6px; }

/* The tick is a real checkbox behind a label — it must work with no script. */
.gym-tick input { position: absolute; opacity: 0; pointer-events: none; }
.gym-tick span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 46px: a thumb, on a phone, with chalk on it. */
  width: 46px;
  height: 46px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface-2);
  color: var(--secondary);
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
}
.gym-tick input:checked + span {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success);
}
.gym-tick input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

.gym-weight {
  width: 46px;
  padding: 6px 4px;
  text-align: center;
  font-size: 13px;
  max-width: none;
}
.gym-unit-label {
  align-self: flex-start;
  display: flex;
  align-items: center;
  height: 46px;
  padding-inline-start: 4px;
  font-size: 11px;
  color: var(--muted);
}

.gym-best {
  margin-block-start: 10px;
  padding: 7px 11px;
  border-radius: 8px;
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  color: var(--success);
  font-weight: 600;
  font-size: 11.5px;
}

.gym-foot {
  position: sticky;
  bottom: 0;
  display: flex;
  /* WRAPPING, because one of the three children asks for a row of its own.
     `.gym-foot-done` is `flex: 1 1 100%` — the finished-day sentence, written
     to sit on its own line above the two controls. Without this it could not
     take that line and was squeezed into the leftover space between them: a
     74px column 147px tall in Farsi, 92px and 110px in English, turning a
     52px sticky foot into a third of a 390px screen. Measured at 390px in both
     languages; the two-control state has nothing that wraps and is unchanged. */
  flex-wrap: wrap;
  gap: 10px;
  padding: 14px 16px;
  border-block-start: 1px solid var(--border);
  background: rgba(18, 21, 26, .96);
  backdrop-filter: blur(8px);
  margin: 0;
}
.gym-foot-btn { flex: 1; margin: 0; text-align: center; padding: 14px; }

/* ------------------------------------------------------ day picker + stats */

.gym-day-list { display: flex; flex-direction: column; gap: 10px; margin-block-start: 14px; }
.gym-day-card {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  padding: 14px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface);
  text-decoration: none;
  color: var(--text);
}
.gym-day-card:hover { border-color: var(--accent-border); }
.gym-day-focus { flex: 1; font-weight: 600; font-size: 15px; }

.gym-stats {
  margin-block-start: 14px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 9px;
}
.gym-stat { padding: 12px; border-radius: 10px; background: var(--surface); }
.gym-stat-k { font-size: 11px; color: var(--muted); }
.gym-stat-v { margin-block-start: 3px; font-weight: 700; font-size: 18px; }

.gym-pr-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.gym-pr {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  padding: 11px 14px;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}
.gym-pr:last-child { border-block-end: 0; }
.gym-pr-name { flex: 1 1 140px; font-weight: 500; font-size: 13.5px; }
.gym-pr-best { font-weight: 700; font-size: 13.5px; color: var(--accent); }
.gym-pr-when { font-size: 11.5px; color: var(--muted); }

/* ------------------------------------------------------------ gym: weeks */

.gym-weeks-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
  justify-content: space-between;
  margin-block-start: 14px;
}

.gym-weeks {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-block-start: 10px;
}
.gym-week {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  height: 40px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--secondary);
  font-weight: 600;
  font-size: 13.5px;
  text-decoration: none;
}
.gym-week:hover { color: var(--text); border-color: var(--accent-border); }
/* A week with anything logged in it reads as done at a glance. */
.gym-week.is-logged {
  border-color: var(--success-border);
  background: var(--success-bg);
  color: var(--success);
}
.gym-week.is-current { background: var(--accent); border-color: var(--accent); color: #1a1204; }

.gym-last-week { margin-block-start: 12px; }

/*
 * Visually-hidden form controls must be given a SIZE.
 *
 * These are `position: absolute; opacity: 0` so the styled <span> beside them
 * is what the client sees. But the base rule gives every input width:100%, and
 * an absolutely positioned element with no positioned ancestor resolves that
 * against the initial containing block — the viewport. Each hidden checkbox was
 * therefore 1280px wide.
 *
 * In LTR they overflowed harmlessly to the right. In RTL they overflowed to the
 * LEFT, dragging the whole page sideways and pushing the sticky header and
 * footer out of view — which is what "the top and bottom are floating" was.
 *
 * 1px and clipped: still focusable, still submitted, no longer able to move
 * anything.
 */
.gym-tick input,
.choice input,
.tabs > input[type="radio"],
.pose-pick input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* The file picker is the exception: it must stay clickable, because the label
   IS the button and the click has to reach the input underneath. */
.pose-pick input[type="file"] {
  inset: 0;
  width: 100%;
  height: 100%;
  clip-path: none;
  opacity: 0;
  cursor: pointer;
}

/* Anchor the hidden control to its own component rather than the viewport. */
.gym-tick, .choice, .tabs, .pose-pick { position: relative; }

/*
 * Numeric BLOCKS keep the page's alignment; only their digits go LTR.
 *
 * The digits are wrapped in <bdi dir="ltr"> in the markup rather than the block
 * carrying dir="ltr" itself. A dir attribute on the block sets its direction,
 * so text-align:start then resolves to LEFT — which in RTL put the number under
 * the far end of its own label. Isolating the inline run instead keeps the
 * block aligned with the page and the digits reading left-to-right.
 */
.gym-stat-v, .gym-count-v, .gym-pr-best { text-align: inherit; }

/*
 * The notifications bell.
 *
 * An icon rather than a word: it holds the same width in both languages, and a
 * bell is understood without reading. The count sits on its shoulder rather
 * than beside it, so the item stays the width of an icon however many are
 * unread.
 */
.nav-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Bigger than the glyph so it is a comfortable tap target on a phone. */
  padding: 4px;
  color: var(--text);
}
.nav-bell:hover { color: var(--accent); }

.nav-bell-count {
  position: absolute;
  /* Logical, so it sits on the correct shoulder in RTL. */
  inset-block-start: -3px;
  inset-inline-end: -6px;
  min-width: 16px;
  padding: 0 4px;
  border-radius: 999px;
  font-size: 10.5px;
  line-height: 16px;
  text-align: center;
}

@media (max-width: 767px) {
  /* Inside the burger panel the bell sits in a row of text links, so it needs
     to read as one of them rather than float in an empty row. */
  .nav-menu[open] ~ nav .nav-bell { padding: 10px 12px; }
  .nav-menu[open] ~ nav .nav-bell-count { inset-block-start: 4px; inset-inline-end: 4px; }
}

/* ============================================================== records
 *
 * The design's isRecords screen, client tab.
 *
 * THIS SCREEN DOES NOT USE .card, DELIBERATELY. The design's own containers are
 * 17px padding / 13px radius / --border / --panel; `.card` is 28px/10px and
 * `main > .card` overrides that again to 22px/14px/--border-strong. Reaching for
 * the app's card class the first time round produced three different box
 * treatments on one screen and a header and a totals grid wrapped in cards that
 * the design does not have at all. `.rec-card` is the design's box, once.
 *
 * EVERY BAR IS SIZED BY AN ATTRIBUTE, not by an inline style. The CSP is
 * `style-src 'self'` and drops inline style attributes outright — the intake
 * progress bar was stuck at 100% for exactly this reason. One rule set below
 * maps data-pct onto a custom property, and both the horizontal bars and the
 * vertical columns read it, so the 5% steps are declared once per shape.
 */

[data-pct="0"] { --pct: 0%; }
[data-pct="5"] { --pct: 5%; }
[data-pct="10"] { --pct: 10%; }
[data-pct="15"] { --pct: 15%; }
[data-pct="20"] { --pct: 20%; }
[data-pct="25"] { --pct: 25%; }
[data-pct="30"] { --pct: 30%; }
[data-pct="35"] { --pct: 35%; }
[data-pct="40"] { --pct: 40%; }
[data-pct="45"] { --pct: 45%; }
[data-pct="50"] { --pct: 50%; }
[data-pct="55"] { --pct: 55%; }
[data-pct="60"] { --pct: 60%; }
[data-pct="65"] { --pct: 65%; }
[data-pct="70"] { --pct: 70%; }
[data-pct="75"] { --pct: 75%; }
[data-pct="80"] { --pct: 80%; }
[data-pct="85"] { --pct: 85%; }
[data-pct="90"] { --pct: 90%; }
[data-pct="95"] { --pct: 95%; }
[data-pct="100"] { --pct: 100%; }

/* A label that exists for a screen reader only. The design shows a bare
   select; a control with no accessible name is still a bug. */
/*
 * The skip link (audit F2).
 *
 * Hidden by being moved off the top of the viewport rather than by
 * `.visually-hidden`'s 1px clip: it has to become a REAL 44px box the moment
 * it takes focus, and a clipped element that un-clips on focus has to undo
 * seven declarations to do that. Overflow above the viewport never produces a
 * scrollbar, so nothing is added to the page for the people who never tab.
 *
 * z-index above everything the app stacks (the highest elsewhere is 45): a
 * skip link that lands under a sticky header is a skip link to nowhere. It is
 * `position: fixed` for the same reason — the team shell and gym mode both
 * scroll a region rather than the document.
 */
.skip-link {
  position: fixed;
  inset-block-start: 8px;
  inset-inline-start: 8px;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  color: var(--text);
  font-size: 15px;
  text-decoration: none;
  /* Off the top of the viewport, still in the tree and still focusable. */
  transform: translateY(-250%);
}

.skip-link:focus { transform: none; }

/* It is only ever on screen while focused, so hover is hover-on-the-focused-
   link: the same "more prominent than rest" step every other control here
   takes. */
.skip-link:hover { border-color: var(--accent-border); color: var(--accent); }

/*
 * The landmarks it aims at carry tabindex="-1" so focus really moves — without
 * it a fragment link scrolls and leaves the tab sequence where it was, which is
 * the bug that makes most skip links useless.
 *
 * Deliberately NO `outline: none` to hide the ring that lands on <main>. Modern
 * UA sheets draw the default ring from :focus-visible, and a programmatic or
 * fragment focus on a tabindex="-1" element does not match it — so there is
 * nothing to suppress, and suppressing it would break AGENTS.md's NEVER for a
 * ring that is not painted anyway.
 */

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

/* The design's 30px/56px leading and trailing padding. */
.page-records { padding-block: 30px 56px; }

/* The design's box: ONE treatment, used by all four panels. */
.rec-card {
  padding: 17px;
  border: 1px solid var(--border);
  border-radius: 13px;
  background: var(--panel);
}

/* --------------------------------------------- what needs you (the pane)

   The nudges. A STRIP ACROSS THE SHELL until 2026-09-14 — `.team-nudges` and
   `.team-nudge*`, deleted with the markup rather than left "in case", because
   git remembers and this file already carries enough dead weight. The owner
   asked for a tab instead; team-layout.eta keeps the argument the strip was
   built on and _team-nudges.eta keeps what replaced it.

   The shape is the design comp's "Today · start here" pane on the `team`
   screen: a title, a line of lede, then a column of bordered rows — one thing
   waiting per row, each with somewhere to go. The comp's row leads with a gold
   count square, which only means something when the row is a whole queue; ours
   is one nudge, so the square is replaced by the kind's uppercase gold label,
   which is the one piece of the strip worth keeping.

   `--panel` on `--team-deep`, the pane's own ground: the same one step up that
   `.team-card` takes, so a row reads as a card on this surface rather than
   borrowing the strip's old accent band. Nothing here is coloured for
   urgency — a nudge is work, not an error, and the tab's count is what says
   how much of it there is. */

.team-needs-pane { max-width: 46rem; }

.team-needs-title {
  margin: 8px 0 0;
  font-size: 23px;
  font-weight: 700;
  letter-spacing: -.02em;
  /* Farsi runs 15-25% longer than its English equivalent, and this heading is
     five words in one and six in the other — long enough to wrap at 390px and
     leave one word alone on the second line. */
  text-wrap: balance;
}

.team-needs-lede {
  margin: 7px 0 0;
  max-width: 52ch;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--secondary);
}

.team-needs {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The whole row is the target — this is read on a laptop and on a phone, and
   a small link inside a large row is the classic way to make something feel
   broken. Grid rather than flex: the label, the sentence and the foot are
   three stacked runs, and `min-width: 0` on the row is what lets a long Farsi
   sentence wrap instead of pushing the foot off the end. */
.team-need {
  display: block;
  min-width: 0;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  text-decoration: none;
  color: var(--text);
}
.team-need:hover { border-color: var(--accent); }
/* This stylesheet has no global `a:focus-visible` — each component states its
   own, the policy the CRM pane's own rules were added under. INSET, because
   the pane it sits in is an overflow container and a ring drawn outside the
   row is a ring that can be clipped at its edge. */
.team-need:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.team-need-k {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}

.team-need-b {
  display: block;
  margin-block-start: 5px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  /* The sentence carries a CLIENT'S NAME, which is user-generated and can be
     one long unbroken run — the row is bordered, so an overflowing name would
     leave the card rather than the card growing round it. */
  overflow-wrap: anywhere;
}

/* A block review's "No progress" / "Not logged" — a word, not a colour. */
.team-need-flag {
  display: inline-block;
  margin-block-start: 6px;
  padding: 1px 8px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}

@media (max-width: 640px) {
  .team-needs-title { font-size: 20px; }
}

/* ------------------------------------------------ what needs you, the list

   The owner, 2026-09-28: the tab becomes the one place for everything that
   needs attending — a summary of the queues, then every item with its
   urgency, who and why in one line, and three things to do.

   URGENCY IS A COLOUR AND A WORD. The row's inline-start edge carries the
   tone (the owner's scheme: red money and messages, amber inactivity, blue
   scheduled, green good news), and the first thing in the row is a pill
   saying it in words — so the cue survives without colour. `data-tone`, not
   a class: `class="team-need"` is counted by the tests and the badge, and a
   modifier class would change that string. */

.team-need[data-tone] { border-inline-start-width: 4px; }
.team-need[data-tone="red"] { border-inline-start-color: var(--danger); }
.team-need[data-tone="amber"] { border-inline-start-color: var(--warn); }
.team-need[data-tone="blue"] { border-inline-start-color: var(--info); }
.team-need[data-tone="green"] { border-inline-start-color: var(--success); }
/* An item is a card holding controls, not a link: no hover ring. */
article.team-need:hover { border-block-color: var(--border); border-inline-end-color: var(--border); }

.needs-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 0;
}

.needs-flag { margin-block-start: 0; margin-inline-start: 4px; text-decoration: none; }

.needs-tone {
  display: inline-flex;
  align-items: center;
  padding: 1px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.6;
}
.needs-tone[data-tone="red"] { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-border); }
.needs-tone[data-tone="amber"] { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }
.needs-tone[data-tone="blue"] { color: var(--info); background: var(--info-bg); border-color: var(--info-border); }
.needs-tone[data-tone="green"] { color: var(--success); background: var(--success-bg); border-color: var(--success-border); }

/* The one line of who and why IS the link to where the thing lives. */
.needs-who {
  /* The whole line is the target: 44px tall on a phone, not the 22px of its
     text (QA at 390, 2026-09-28). */
  box-sizing: border-box;
  min-height: 44px;
  /* The extra height goes BELOW the line, so its focus ring does not cut
     through the urgency line above it. */
  padding-block: 2px 11px;
  margin-block: 3px -11px;
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
}
.needs-who:hover { text-decoration-color: var(--accent); color: var(--text); }
.needs-who:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.needs-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.needs-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px;
  margin-block-start: 12px;
}

/* 44px on every control here, phone or not: this list is worked with a
   thumb between sessions as often as at a desk. */
.needs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin: 0;
  touch-action: manipulation;
}

.needs-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; }
.needs-snooze-k { font-size: 12.5px; color: var(--secondary); padding-inline-end: 2px; }

/* "Message…" is the first control in the row — the primary act — and the
   composer, once open, takes the whole width above Done and Snooze. */
.needs-compose { flex: 0 0 auto; }
.needs-compose[open] { flex: 1 1 100%; }
.needs-compose[open] > summary { margin-block-end: 10px; }
.needs-compose > summary { list-style: none; cursor: pointer; width: max-content; }
.needs-compose > summary::-webkit-details-marker { display: none; }
.needs-compose > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.needs-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
}
/* `margin: 0` — the page rule gives every label 15px above and 6 below,
   which inside this 12px-padded box left 27px of blank over the label and
   14 between it and the help line (2026-10-01). The form's gap is enough. */
.needs-label { margin: 0; font-size: 13.5px; font-weight: 600; color: var(--text); }
.needs-help { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--secondary); }
.needs-text {
  width: 100%;
  min-height: 7.5rem;
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 16px;
  line-height: 1.55;
  resize: vertical;
}
.needs-text:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.needs-text[aria-invalid="true"] { border-color: var(--danger-border); }
.needs-send { align-self: flex-start; }

.needs-error {
  margin: 0;
  padding: 6px 10px;
  border: 1px solid var(--danger-border);
  border-radius: 8px;
  background: var(--danger-bg);
  color: var(--danger);
  font-size: 13px;
}

/* A pressed button keeps its label and gains a spinner (AGENTS.md). */
.needs-btn.is-busy::after {
  content: "";
  width: 12px;
  height: 12px;
  margin-inline-start: 8px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .needs-btn.is-busy::after { animation: none; } }

/* THE SUMMARY ROW — the rail's own counts, as links to their tabs. */
.needs-sum { margin-block-start: 14px; }
.needs-sum-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.needs-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--panel);
  color: var(--text);
  font-size: 13px;
  text-decoration: none;
  touch-action: manipulation;
}
.needs-chip:hover { border-color: var(--accent); }
.needs-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.needs-chip-n { font-weight: 700; font-size: 15px; font-variant-numeric: tabular-nums; }
.needs-chip[data-tone="red"] .needs-chip-n { color: var(--danger); }
.needs-chip-k { color: var(--secondary); }
.needs-sum-clear { margin: 0; font-size: 13px; color: var(--secondary); }

.needs-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-block-start: 14px;
}
.needs-whose { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 10px; overflow: hidden; }
.needs-seg {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  color: var(--secondary);
  font-size: 13px;
  text-decoration: none;
  touch-action: manipulation;
}
.needs-seg + .needs-seg { border-inline-start: 1px solid var(--border-strong); }
.needs-seg[aria-current="page"] { background: var(--surface-2); color: var(--text); font-weight: 600; }
.needs-seg:hover { color: var(--text); background: var(--surface); }
.needs-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.needs-status:empty { display: none; }
.needs-status {
  margin-block-start: 14px;
  padding: 8px 12px;
  border: 1px solid var(--success-border);
  border-radius: 10px;
  background: var(--success-bg);
  color: var(--success);
  font-size: 13.5px;
  scroll-margin-top: 80px;
}
/* Focused on arrival after a send, Done or Snooze (`#needs-status`): the
   ring says where the keyboard now is. */
.needs-status:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ------------------------------------------- what needs you, in lanes

   The owner, 2026-09-30: "for the needs you tab of the dashboard, the right
   side of the screen is unused, so have me see more things in one look other
   than having to scroll down." A column of 46rem cards, each ~170px tall,
   showed three items at 1440×900 and let the page run to 5,800px.

   THREE LANES + COMPACT CARDS. Act now (red and amber), Touchpoints (blue),
   Good news (green), each headed with its count. On a wide screen they stand
   side by side and grow with their cards; the page scrolls past them and each
   lane's heading sticks to the top of the window (2026-10-05 — until then the
   shell was held to one screen and each lane scrolled on its own, which the
   owner found "annoying"). Below the breakpoint the lanes stack in the same
   order, one column, the phone's list as before. Grid columns follow
   the document's direction, so in Farsi Act now is the rightmost lane.

   THE CARD is three short rows: the tone in words and the kind (with how long
   it has waited at the far end), the who-and-why on ONE line with an
   ellipsis (the whole sentence unfolds on hover or focus, and is what a
   screen reader hears),
   then the owner's small print and the controls. Every control is 44px square
   or wider; Message opens the composer in place and the card grows round it.

   `class="team-need"` and `class="team-needs"` are unchanged on the row and
   each lane's list — the rail's badge and the tests count those strings — so
   everything here hangs off `.needs-lane` and data attributes. */

.needs-lanes {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin-block-start: 14px;
}
.needs-lane { min-width: 0; }
.needs-lane-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  scroll-margin-top: 80px;
}
.needs-lane-n {
  min-width: 24px;
  padding: 0 7px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 12px;
  line-height: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.needs-lane[data-lane="act"] .needs-lane-n { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-border); }
.needs-lane[data-lane="touch"] .needs-lane-n { color: var(--info); background: var(--info-bg); border-color: var(--info-border); }
.needs-lane[data-lane="good"] .needs-lane-n { color: var(--success); background: var(--success-bg); border-color: var(--success-border); }
/* An empty lane says so in a line, rather than standing as a blank column
   that reads as "failed to load". */
.needs-lane-empty {
  margin: 0;
  padding: 12px 14px;
  border: 1px dashed var(--border-strong);
  border-radius: 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--secondary);
}
.needs-lane > .team-needs {
  position: relative;
  margin: 0;
  gap: 8px;
}

/* THE COMPACT CARD. Row one: the tone in words, the age, the controls. Row
   two: the kind and the who-and-why, one line. Row three: the small print.
   The controls come after the sentence in the source — read and tabbed to
   after it — and the grid lifts them into row one, where they cost no height
   of their own. */
.needs-lane .team-need {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  grid-template-areas:
    "head . age  acts"
    "why  why why  why"
    "meta meta meta meta";
  align-items: center;
  column-gap: 8px;
  padding-block: 6px 8px;
  padding-inline: 12px 8px;
  border-radius: 10px;
  scroll-margin-block: 8px;
}
.needs-lane .needs-head { grid-area: head; flex-wrap: nowrap; min-width: 0; }
.needs-lane .needs-tone { flex: none; padding: 0 7px; font-size: 11px; line-height: 18px; }
.needs-age {
  grid-area: age;
  margin: 0;
  font-size: 11.5px;
  color: var(--secondary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The kind and the who-and-why, on ONE line: the kind keeps its words, the
   sentence gives way with an ellipsis. `display: block` so the ellipsis has a
   box to cut against; the flag ("Not logged") stays inline in it. The phone's
   list gets two lines instead (below), where one would hold a name and little
   else. */
.needs-why {
  grid-area: why;
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  margin: 2px 0 0;
}
.needs-why .team-need-k { flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Tracking pulls Persian letters apart — the kind is Farsi prose on /fa. */
[dir="rtl"] .needs-lane .team-need-k { letter-spacing: normal; font-size: 11px; }
.needs-lane .needs-who,
.needs-notice-b {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  margin: 0;
  padding-block: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13.5px;
  line-height: 1.5;
}
.needs-lane .needs-flag { padding-block: 0; font-size: 11px; }
/* The whole sentence, on demand: under the pointer or keyboard focus the
   line unfolds in place. Only the sentence itself answers, so moving down the
   lane does not open every card it passes. */
.needs-lane .needs-who:hover,
.needs-lane .needs-who:focus-visible,
.needs-notice-link:hover .needs-notice-b,
.needs-notice-link:focus-visible .needs-notice-b {
  white-space: normal;
  overflow-wrap: anywhere;
  -webkit-line-clamp: unset;
  line-clamp: none;
}

.needs-lane .needs-actions {
  grid-area: acts;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  margin-block-start: 0;
}
/* The compact controls: 44px square at least, an icon or two characters. */
.btn.needs-ico {
  min-width: 44px;
  min-height: 44px;
  padding: 0 8px;
}
.needs-icon { display: block; flex: none; }
/* No global button or link ring in this stylesheet: stated here, inset from
   nothing, since the card's own padding keeps it inside the lane's clip. */
.needs-lane .btn.needs-ico:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.needs-lane .needs-snooze { flex-wrap: nowrap; gap: 4px; }
.needs-lane .needs-snooze-k {
  display: inline-flex;
  align-items: center;
  padding-inline: 2px 0;
  color: var(--secondary);
}
.needs-lane .needs-compose > summary { width: auto; }
.needs-lane .needs-compose > summary .needs-icon { color: var(--accent); }
/* Open, the Message control reads as pressed: it is also the way to close it. */
.needs-lane .needs-compose[open] > summary { border-color: var(--accent-border); background: var(--accent-bg); }

.needs-lane .needs-meta {
  grid-area: meta;
  flex-direction: column;
  flex-wrap: nowrap;
  gap: 0;
  min-width: 0;
  margin: 4px 0 0;
}
.needs-last { margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--secondary); }

/* COMPOSING: the controls leave row one for a row of their own under the
   card, the composer across its width, Done and Snooze below it — the list's
   layout before the lanes. `data-open` is the server's (a deep link, a refused
   send) and needs.js's; `:has()` covers a click with script off. */
.needs-lane .team-need[data-open],
.needs-lane .team-need:has(.needs-compose[open]) {
  grid-template-areas:
    "head . age  age"
    "why  why why  why"
    "meta meta meta meta"
    "acts acts acts acts";
}
.needs-lane .team-need[data-open] .needs-actions,
.needs-lane .team-need:has(.needs-compose[open]) .needs-actions {
  flex-wrap: wrap;
  margin-block-start: 8px;
}

/* THE CONTACT LINE — the owner's small print. Every value its own link and
   its own Latin isolate; wrapped by value, never inside one. 24px tall per
   link: small print by the owner's ask, still a target a mouse or a thumb can
   hit without a neighbour. */
.needs-contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 6px;
  margin: 0;
  font-size: 12px;
  line-height: 1.35;
  color: var(--secondary);
}
.needs-contact-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  min-height: 24px;
  color: var(--secondary);
  text-decoration: none;
  overflow-wrap: anywhere;
  touch-action: manipulation;
}
.needs-contact-link:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.needs-contact-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 3px; }
.needs-contact-k { font-size: 11px; font-weight: 600; letter-spacing: .02em; }
.needs-contact-sep { color: var(--muted); }

/* A WORK NUDGE (a form check, a block review with no item twin): its tone,
   kind and who-and-why are one link to where it is answered; the age sits at
   the end of its first row, the contact line under it, outside it. */
.needs-lane .team-need[data-notice] {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "link age"
    "meta meta";
  align-items: start;
  padding-block: 8px;
}
.needs-lane .team-need[data-notice] .needs-age { margin-block-start: 2px; }
.needs-notice-link {
  grid-area: link;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  min-height: 44px;
  color: var(--text);
  text-decoration: none;
  touch-action: manipulation;
}
.needs-notice-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.needs-notice-b {
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
}
.needs-notice-link:hover .needs-notice-b { text-decoration-color: var(--accent); }

@media (max-width: 1099px) {
  /* One column, read on a phone as often as not: the who-and-why gets two
     lines, and the 44px its line had before the lanes (QA at 390,
     2026-09-28). */
  .needs-lane .needs-who,
  .needs-notice-b {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  /*
   * THE 44px IS THE KIND AND THE SENTENCE TOGETHER, NOT THE SENTENCE ALONE
   * (2026-10-01). It was `min-height: 44px` on the link, whose text sits at
   * the top of its box: a one-line sentence left 20px of empty link under
   * it, and with the contact line's margin a 29px blank band through the
   * middle of most cards on a phone — QA's "block of blank space". The list
   * before the lanes hid that with a negative margin the lanes dropped.
   *
   * Now the link's hit area is stretched over the whole who-and-why row
   * (the kind beside or above it included, the shape a work nudge's card
   * already has — `.needs-notice-link`), and the ROW is at least 44px with
   * its lines centred. A one-line card loses the band; a tap anywhere on
   * the two lines still opens the client. The link is not positioned, so
   * the pseudo-element measures the row; its focus ring stays on the words.
   */
  /* p: the item's sentence. A form-check notice's span.needs-why sits inside
     a link that is already 44px tall (QA of #223). */
  .needs-lane p.needs-why {
    position: relative;
    min-height: 44px;
    align-content: center;
  }
  .needs-lane .needs-who::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  /* The kind above the sentence rather than beside it: beside, it takes a
     third of a phone's width from the words. */
  .needs-why { flex-wrap: wrap; row-gap: 0; }
  .needs-why .team-need-k { max-width: 100%; }
  .needs-lane .needs-who,
  .needs-notice-b { flex-basis: 14rem; }
}

@media (min-width: 1100px) {
  /* The tab's 46rem cap is for the single column and the settings sub-view;
     the lanes take the width the owner said was going unused. */
  .team-needs-pane.is-lanes { max-width: none; }
  .team-needs-pane.is-lanes .team-needs-lede { max-width: none; }
  .team-needs-pane.is-lanes .needs-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 16px;
    margin-block-start: 14px;
  }
  .team-needs-pane.is-lanes .needs-bar > .needs-sum,
  .team-needs-pane.is-lanes .needs-bar > .needs-tools { margin-block-start: 0; }
  .team-needs-pane.is-lanes .needs-bar > .needs-tools { margin-inline-start: auto; }
  .needs-lanes {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
    /* Each lane as tall as its own cards (QA of #234): stretched to the
       tallest, a two-card lane was a 5,000px empty box once the page scrolls. */
    align-items: start;
  }
  .needs-lane {
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding: 10px 8px 8px;
    border: 1px solid var(--team-hair);
    border-radius: 12px;
    background: var(--bg);
  }

  /*
   * THE PAGE SCROLLS, THE HEADINGS STAY (owner, 2026-10-05: "for the needs
   * you dashboard, i need to be able to scroll down a little bit, right now
   * it's just one screen and it's annoying").
   *
   * From #215 to here the shell was held to one screen on this tab (the #209
   * clients-frame recipe) and each lane scrolled inside it: the page itself
   * never moved, the wheel only moved whichever lane was under the pointer,
   * and at 1280×720 a lane showed three cards. Now the lanes grow with their
   * cards and the document scrolls past them, like every other tab. What the
   * frame gave — the column names always in view — is kept by making each
   * lane's heading sticky: it rides at the top of the window for as long as
   * its lane is under it, the way a table's header row does.
   *
   * `.team-detail` drops its `overflow-y: auto` for this tab: an overflow box
   * is the scroller a sticky child sticks to, and this one never scrolls, so
   * with it the headings would scroll away with the page. Still a containing
   * block (`position: relative`), so the pane's `.visually-hidden` labels do
   * not stretch the document.
   */
  .team-shell.is-lanes .team-detail { position: relative; overflow: visible; }
  .needs-lane-title {
    position: sticky;
    top: 0;
    z-index: 2;
    margin: -10px -8px 8px;
    padding: 10px 12px 8px;
    border-block-end: 1px solid var(--team-hair);
    border-radius: 11px 11px 0 0;
    background: var(--bg);
  }
  /* A card brought to the top (a deep link, needs.js) lands under the
     stuck heading, not behind it; and a control Tab (or Shift+Tab) scrolls
     to does too — a sticky band must never cover focus (AGENTS.md). The
     document is what scrolls, so the padding goes on it, for this tab only. */
  .needs-lane .team-need { scroll-margin-block: 52px 8px; }
  :root:has(.team-shell.is-lanes) { scroll-padding-block-start: 52px; }
}

/* THE SETTINGS SUB-VIEW (owner only). */
.needs-back { margin: 14px 0 0; }
.needs-set-title { margin: 14px 0 0; font-size: 17px; font-weight: 700; scroll-margin-top: 80px; text-wrap: balance; }
.needs-settings { display: flex; flex-direction: column; gap: 16px; margin-block-start: 16px; }
.needs-set-group {
  margin: 0;
  padding: 12px 14px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  min-width: 0;
}
.needs-set-legend { padding: 0 6px; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
.needs-set-row {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  align-items: center;
  gap: 4px 14px;
  padding-block: 10px;
  padding-inline: 12px 0;
  border-block-end: 1px solid var(--team-hair);
  border-inline-start: 3px solid transparent;
}
.needs-set-row:last-child { border-block-end: 0; }
.needs-set-row[data-tone="red"] { border-inline-start-color: var(--danger); }
.needs-set-row[data-tone="amber"] { border-inline-start-color: var(--warn); }
.needs-set-row[data-tone="blue"] { border-inline-start-color: var(--info); }
.needs-set-row[data-tone="green"] { border-inline-start-color: var(--success); }
.needs-set-on {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.needs-set-on input { width: 20px; height: 20px; margin: 0; accent-color: var(--accent); }
.needs-set-n { display: flex; align-items: center; gap: 10px; min-width: 0; }
.needs-set-input {
  flex: 0 0 5.5rem;
  width: 5.5rem;
  max-width: 5.5rem;
  min-height: 44px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.needs-set-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.needs-set-input[aria-invalid="true"] { border-color: var(--danger); }
.needs-set-unit { font-size: 13px; color: var(--secondary); min-width: 0; }
.needs-set-range { grid-column: 2; margin: 0; font-size: 12px; color: var(--muted); }
.needs-set-row .needs-error { grid-column: 1 / -1; }

@media (max-width: 640px) {
  .needs-set-row { grid-template-columns: minmax(0, 1fr); }
  .needs-set-range { grid-column: 1; }
  .needs-tools { justify-content: flex-start; }
}

/* ------------------------------------------------------------- header */

/* Bare, not a card — the design has the title sitting directly on the page. */
.rec-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  justify-content: space-between;
}
.rec-head h1 {
  margin: 0;
  font-size: 26px;
  letter-spacing: -.02em;
}
.rec-head p {
  margin: 7px 0 0;
  max-width: 52ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--secondary);
  text-wrap: pretty;
}

/* The same header on the coach's Records pane.

   The title there is the dashboard's own eyebrow rather than an <h1>: the pane
   already sits under the client's name, and a second 26px heading would compete
   with it for the same job. Centred rather than baseline-aligned because there
   is no descender-bearing title to align the unit tabs to. */
.rec-head-team {
  align-items: center;
  margin-block-end: 2px;
}

/* ------------------------------------------------------------- totals */

/* Also bare. The TILES carry the border — wrapping this grid in a card put a
   box inside a box, which is the same double-chrome mistake the choice chips
   had. */
.rec-totals {
  margin-block-start: 18px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
}
.rec-total {
  padding: 13px 15px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--panel);
}
.rec-total-k { font-size: 11.5px; font-weight: 500; color: var(--muted); }
.rec-total-v {
  margin-block-start: 5px;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -.01em;
  /* Audit F46. These are stacked tiles read down a column; proportional digits
     make the figures ripple against each other, which is the one thing a
     column of numbers exists not to do. */
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------- chart + heaviest */

.rec-cols {
  margin-block-start: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}
/* The chart keeps its width and the list gives way: a squeezed line chart stops
   being readable well before a squeezed list does. */
.rec-chart-card { flex: 1 1 400px; min-width: 290px; }
.rec-best-card { flex: 1 1 260px; min-width: 250px; }

.rec-card-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
}
.rec-card-title { font-size: 14.5px; font-weight: 700; margin: 0; }
.rec-card-sub { margin: 3px 0 0; font-size: 11.5px; color: var(--muted); }
/* The heaviest-lifts card has no control beside its title, so its subtitle
   clears the heading by 4px rather than 3px. */
.rec-best-card .rec-card-sub { margin-block-start: 4px; }

.rec-pick { display: flex; gap: 8px; align-items: center; }
/* Not full width: this is a control in a header row, and the global form rule
   would stretch it across the card. */
/* 16px is the floor for anything typed into or picked from (the global rule
   above). The padding comes down by the 2px the larger type adds, so the card
   header keeps the height it was drawn at rather than growing under it. */
.rec-pick select {
  width: auto;
  max-width: 15rem;
  margin: 0;
  padding: 6px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px;
  font-weight: 600;
}

.rec-chart { margin-block-start: 14px; }
.rec-chart svg { width: 100%; height: auto; display: block; }

/* SVG paint lives here, not in fill/stroke attributes: a presentation
   attribute holding var(--accent) is parsed as CSS but is not reliably
   resolved in every engine, and a chart that renders black is worse than
   none. */
.rec-axis { stroke: var(--border); stroke-width: 1; }
.rec-guide { stroke: var(--grid-line); stroke-width: 1; stroke-dasharray: 3 4; }
.rec-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.rec-dot-best { fill: var(--accent); }
/* Hollow, filled with the page rather than `none`, so the line does not show
   through the ring. */
.rec-dot-now { fill: var(--bg); stroke: var(--accent); stroke-width: 2.5; }
.rec-tick { fill: var(--muted); font-size: 9px; }
.rec-peak { fill: var(--accent); font-size: 10px; font-weight: 700; }

.rec-figures {
  margin-block-start: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--secondary);
}
.rec-fig { color: var(--text); }
.rec-fig-best { color: var(--accent); }
.rec-up { color: var(--success); }
/* Flat or down. Deliberately NOT the danger colour: a lighter week is a deload
   as often as a setback, and painting it red would be the app making a
   coaching judgement it is not entitled to make. */
.rec-flat { color: var(--secondary); }

.rec-best-list {
  margin-block-start: 13px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.rec-best-row {
  display: flex;
  gap: 8px;
  align-items: baseline;
  justify-content: space-between;
}
.rec-best-name { font-size: 13px; font-weight: 500; color: var(--text); }
.rec-star { color: var(--accent); }
.rec-best-v { font-size: 13px; font-weight: 700; color: var(--accent); white-space: nowrap; }
.rec-best-when { margin-block-start: 4px; font-size: 11px; color: var(--muted); }

.rec-bar {
  margin-block-start: 5px;
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
.rec-bar-fill {
  height: 100%;
  inline-size: var(--pct, 0%);
  /* Gold is reserved for the starred lifts, so the star and the bar say the
     same thing. Everything else is the muted bar colour. */
  background: var(--bar-muted);
}
.rec-bar-fill.is-star { background: var(--accent); }

/* -------------------------------------------------- block vs block */

.rec-compare-card { margin-block-start: 16px; }

/* ------------------------------------------------------- the milestone ledger

   §14 slice one, piece 3. A dated list, oldest first, and deliberately the
   plainest thing on this page: no medal, no icon, no colour that says "good".
   The research brief's §0 argues that a currency undermines what it rewards,
   and a row that LOOKS like a badge is a badge whatever its words say. So this
   reads like a log, because that is what it is.

   Nothing here moves. The one animation in this slice is the personal-best
   badge in gym mode; see decisions.md for why it stayed at one. */
.rec-miles { margin-block-start: 16px; }

.rec-mile-list {
  list-style: none;
  margin: 15px 0 0;
  padding: 0;
  display: grid;
  gap: 2px;
}

.rec-mile {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  align-items: baseline;
  justify-content: space-between;
  padding: 9px 0;
  border-block-start: 1px solid var(--border);
}
.rec-mile:first-child { border-block-start: 0; }

/* min-width:0 so a long lift name truncates inside the row rather than pushing
   the date off the end — AGENTS.md's rule for flex children. */
.rec-mile-what {
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
}

.rec-mile-when {
  font-size: 11.5px;
  color: var(--muted);
  /* Dates are compared down the column by eye, so the digits have to line up. */
  font-variant-numeric: tabular-nums;
}

.rec-compare {
  margin-block-start: 15px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 14px;
  align-items: end;
}
/* dir="ltr" on the pair keeps last-block-then-this-block in the same order as
   the legend below, in both languages. */
.rec-cols-pair {
  display: flex;
  gap: 6px;
  align-items: flex-end;
  height: 104px;
}
.rec-col {
  flex: 1;
  border-radius: 3px 3px 0 0;
  block-size: var(--pct, 0%);
  /* A zero-height bar is invisible and reads as missing data rather than as a
     very small number. */
  min-block-size: 3px;
}
.rec-col-prev { background: var(--bar-prev); }
.rec-col-cur { background: var(--accent); }

.rec-compare-name {
  margin-block-start: 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--nav-link);
}
.rec-compare-delta { margin-block-start: 2px; font-size: 11.5px; font-weight: 600; }

.rec-legend {
  margin-block-start: 14px;
  display: flex;
  gap: 16px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--secondary);
}
.rec-key { display: flex; gap: 6px; align-items: center; }
.rec-swatch { width: 10px; height: 10px; border-radius: 2px; }
.rec-swatch-prev { background: var(--bar-prev); }
.rec-swatch-cur { background: var(--accent); }

/* The empty state, and the unit toggle that the design has no equivalent for
   (it assumes kilograms). Borrowed from gym mode's own designed unit tabs so
   the two screens agree. */
.rec-empty { margin-block-start: 18px; }


/* The lobby's heaviest-lifts heading, with the link through to the full
   records page sitting on the same line. */
.gym-pr-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
  justify-content: space-between;
}
.gym-pr-head .h3 { margin-block-end: 0; }

/* The unit switch in the records header. Aligned to the bottom of the title
   block by the header's own align-items:flex-end. */
.rec-units { display: flex; gap: 8px; align-items: center; }

/* ======================================================= team dashboard
 *
 * The design's isTeam screen, round 3: a full-height app frame of
 * rail · queue · working area. Values transcribed from the design, not
 * approximated from the client-side card classes — that mistake is written up
 * in docs/decisions.md and this screen is the first one built after it.
 *
 * Tabs and selection are real links carrying ?tab= and ?id=, so the whole
 * screen works without script and the mobile push-to-detail is CSS only.
 */

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

.team-shell { min-height: 100vh; display: flex; flex-direction: column; }

/*
 * The <main> landmark added for audit F3, doing the job `.team-work` used to
 * get by being a direct child of the shell: fill what the header and the
 * nudges leave, and let the pane inside scroll rather than the page. The
 * `min-height: 0` is the load-bearing half — without it a flex item refuses to
 * shrink below its content and the internal scroll moves onto the document.
 */
.team-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;

  /*
   * AND IT MUST OPT OUT OF THE DOCUMENT SHELL'S `main`.
   *
   * There is a bare `main { max-width: 1180px; margin-inline: auto }` for the
   * CLIENT shell, plus `main > * { padding-inline: var(--shell-pad) }`. Those
   * are element selectors, so the moment this landmark was added (audit F3)
   * the dashboard silently inherited both: a full-bleed app frame became a
   * 1180px column centred in the viewport, with side padding on the rail row
   * it never had. The owner saw it as "the team dashboard is not showing full
   * width" the same day it shipped.
   *
   * The class beats the element selector, but only for properties it actually
   * declares — which is the whole lesson. A landmark added to a full-bleed
   * frame has to say so.
   */
  max-width: none;
  margin-inline: 0;
  /*
   * AND THE BLOCK PADDING, which is the half this comment used to miss.
   *
   * `main { padding: 2rem 1.5rem }` is the client shell's. A later rule zeroes
   * `padding-inline` for every `main`, so the sides were handled and the 2rem
   * top and bottom were not: the frame floated with a 32px band of page
   * background between the header and the rail, and another under the panes.
   * The owner saw it as "a space between the header/footer and the actual
   * dashboard — the containers seem disconnected" (2026-09-11).
   *
   * Exactly the lesson the paragraph above states and then does not finish: a
   * landmark added to a full-bleed frame has to say so for EVERY property the
   * element rule carries, and the list is max-width, margin-inline and padding.
   */
  padding-block: 0;
}
.team-main > * { padding-inline: 0; }

/* ------------------------------------------------------------- header */

.team-header {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-block-end: 1px solid var(--border);
  background: var(--panel);
}
.team-brand { display: flex; align-items: center; gap: 11px; }
.team-brand-name { font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
.team-brand-date { margin-block-start: 2px; font-size: 11px; color: var(--muted); }

.team-headed { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.team-who { display: flex; align-items: center; gap: 8px; }
.team-who-name { font-size: 12.5px; font-weight: 600; }

/* The signed-in role is a FACT, drawn as a label. The design's first draft had
   a role switcher; in the app, changing your own role would be a
   privilege-escalation control. */
.team-role {
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--secondary);
}
.team-role-owner {
  background: var(--accent-bg);
  border-color: var(--accent-border);
  color: var(--accent);
}

.team-menu { position: relative; }
.team-menu > summary { list-style: none; cursor: pointer; padding: 4px; }
.team-menu > summary::-webkit-details-marker { display: none; }
.team-more { display: none; }
.team-menu[open] ~ .team-more {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: absolute;
  inset-inline-end: 20px;
  margin-block-start: 8px;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
  z-index: 20;
}
.team-more a, .team-more .linklike {
  padding: 7px 12px;
  border-radius: 7px;
  color: var(--nav-link);
  font-size: 13px;
  text-decoration: none;
  text-align: start;
}
.team-more a:hover, .team-more .linklike:hover { background: var(--surface-2); color: var(--text); }

/* -------------------------------------------------------------- frame */

.team-work { flex: 1; display: flex; flex-wrap: nowrap; align-items: stretch; min-height: 0; }

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

.team-rail {
  flex: 0 0 186px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 14px 12px;
  border-inline-end: 1px solid var(--border);
  background: var(--panel);
}
.team-tab {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  white-space: nowrap;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  color: var(--nav-link);
  font-size: 13px;
  font-weight: 600;
  text-align: start;
  text-decoration: none;
}
.team-tab:hover { background: var(--surface-2); }
.team-tab.is-current {
  border-color: var(--accent-border);
  background: var(--accent-bg);
  color: var(--accent);
}
.team-tab-label { flex: 1; }
.team-tab-count {
  flex: none;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  text-align: center;
  direction: ltr;
  font-size: 11px;
  font-weight: 700;
  background: var(--surface-2);
  color: var(--text);
}
.team-tab.is-current .team-tab-count { background: var(--accent); color: var(--accent-ink); }
/* A zero is not rendered at all. It used to be dimmed rather than hidden, on
   the argument that "nothing to do" is information — but six grey zeros on
   every quiet morning is a row of noise, and the count that appears is louder
   for being the only one. See team.eta. */

.team-rail-foot {
  margin-block-start: auto;
  padding: 12px 11px 4px;
  border-block-start: 1px solid var(--border);

  /*
   * STICKY, or it is invisible on exactly the tabs with the most content.
   *
   * `margin-block-start: auto` pins this to the bottom of the rail, and the
   * rail stretches to the tallest thing on the page. On Security, Pricing and
   * Review that put it ~900px down a 720px viewport — rendered, correct, and
   * below the fold, which is why it read as "doesn't show".
   *
   * Sticking it to the bottom of the viewport keeps the number where the coach
   * can see it while they scroll, on every tab. It needs its own background:
   * the rail's is on the rail, and a transparent sticky element lets the tabs
   * scroll through it.
   */
  position: sticky;
  inset-block-end: 0;
  background: var(--panel);
}
.team-spend-k { font-size: 11px; color: var(--muted); }
.team-spend-v { margin-block-start: 4px; font-size: 12.5px; font-weight: 700; color: var(--accent); }
.team-spend-bar {
  margin-block-start: 7px;
  height: 5px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
.team-spend-fill { height: 100%; background: var(--accent); inline-size: var(--pct, 0%); }

/* --------------------------------------------------------------- queue */

.team-list {
  flex: 0 0 274px;
  border-inline-end: 1px solid var(--border);
  background: var(--bg);
  align-self: stretch;
  overflow-y: auto;
}
.team-list-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  padding: 12px 15px;
  border-block-end: 1px solid var(--border);
}
.team-list-title { font-size: 13px; font-weight: 700; }
.team-list-meta { font-size: 11px; color: var(--muted); }

.team-search { padding: 11px 15px 4px; }
/* The rail's search box is alone on its row, so the 16px floor simply makes
   it taller; nothing beside it has to be held. */
.team-search input {
  width: 100%;
  margin: 0;
  padding: 8px 11px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px;
}

.team-vip-band {
  padding: 8px 15px;
  border-block-end: 1px solid var(--team-hair);
  background: var(--team-vip-bg);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}

.team-item {
  display: block;
  width: 100%;
  padding: 12px 15px;
  border: 0;
  border-block-end: 1px solid var(--team-hair);
  background: transparent;
  text-align: start;
  text-decoration: none;
  color: inherit;
}
.team-item:hover { background: var(--panel); }
.team-item.is-current { background: var(--surface); }
/* A VIP message carries a gold edge, so the tier reads before the name does. */
.team-item.is-vip { border-inline-start: 2px solid var(--accent); }

.team-item-row { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }
.team-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.team-item-title { font-size: 13px; font-weight: 600; color: var(--text); }
.team-item-age { margin-inline-start: auto; font-size: 11px; color: var(--muted); }
.team-item-sub {
  display: block;
  margin-block-start: 4px;
  font-size: 11.5px;
  color: var(--secondary);
  text-align: start;
}

.team-tier {
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  color: var(--tier-text);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.team-tier.is-vip {
  background: var(--accent-bg);
  border-color: var(--accent-border);
  color: var(--accent);
  font-weight: 700;
  letter-spacing: .08em;
}

/* WAITING TO BE CLAIMED (backlog §28). --warn, not --accent and not --danger:
   a client who has not signed in yet is neither a thing to celebrate nor a
   fault, it is a fact that is going to need chasing. It borrows the base
   .team-tier box exactly, so it sits at the same height as the VIP chip beside
   it and cannot change the row's rhythm. */
.team-tier.is-pending {
  background: var(--warn-bg);
  border-color: var(--warn-border);
  color: var(--warn);
}

/* ---- the claim card on the client pane (§28) --------------------------- */

/* The card itself borrows .team-card wholesale; this only gives it the same
   separation from the card below that .team-assign has. Named so the pane's
   one-time-code block can be found from a stylesheet as well as from a
   template. */
.team-claim { margin-block-end: 12px; }

/* THE CODE IS THE CONTENT OF THIS CARD, so it is typeset as a code and not as
   a paragraph: monospace, wide tracking and a size that survives being read
   off a phone held at arm's length across a gym. `user-select: all` makes one
   tap select the whole thing, which is the closest thing to a copy button a
   page with no script is allowed. It stays within its box at 390px because it
   is allowed to wrap — twenty characters plus four dashes at this size is
   wider than a narrow phone, and a code that overflows is a code somebody
   reads out wrong. */
.team-claim-code {
  margin-block: 10px 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: .12em;
  line-height: 1.5;
  color: var(--accent);
  overflow-wrap: anywhere;
  user-select: all;
}

/* The same address, as text to paste. Small, muted and wrapping: it is the
   convenience beside the code, not a second headline. */
.team-claim-link {
  margin-block: 0 10px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--muted);
  overflow-wrap: anywhere;
  user-select: all;
}

/* "Claimed on 3 Shahrivar" — a line, not a card. The card's work is done the
   day the code is spent; what is left is a fact worth keeping on the record,
   at the weight of the meta line above it. */
.team-claim-done { margin-block: 0 12px; }

.team-list-empty { padding: 26px 18px; text-align: center; }
.team-empty-title { font-size: 13px; font-weight: 600; color: var(--secondary); }
.team-empty-body { margin-block-start: 5px; font-size: 12px; line-height: 1.55; color: var(--muted); }

/* ------------------------------------------------ the clients directory

   THE OWNER, 2026-09-28: "in the client page of the team dashboard, we get an
   infinite downscroll as there are more clients". The directory was one row
   per client in one column, and the page grew by a row per sign-up — 120
   clients measured 8,560px at 1440 and 8,640px at 390.

   Two answers, one per width. The directory is PAGED at 25 (routes/team.js),
   which alone bounds the phone, where the list is the page. On a desktop the
   dashboard is also held to one screen for this tab (`.team-shell.is-framed`,
   set by team-layout.eta), and the rows scroll inside their own column while
   the search and the chips above them stay put and the pane beside them does
   not move. Other tabs are untouched: they still scroll as a page. */

.team-client-chips { padding-block-start: 8px; }
/* Content-sized rather than `.team-box`'s equal halves: six states do not
   divide a 274px column, and equal cells would give "All" the width of
   "Needs a program". */
.team-client-chips .team-box { flex: 0 1 auto; padding: 5px 9px; font-size: 12px; touch-action: manipulation; }
/* The counts are quiet on every chip but the standing one — six accent badges
   in a row is one loud stripe, and says nothing about which is chosen. */
.team-client-chips .team-box-n {
  background: var(--surface-2);
  color: var(--secondary);
  font-variant-numeric: tabular-nums;
}
.team-client-chips .team-box.is-current .team-box-n { background: var(--accent); color: var(--accent-ink); }

.team-client-chips .team-box:focus-visible,
.team-pager-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Inset, because the rows sit inside a scroller that clips anything drawn
   outside their box. */
.team-list.is-paged .team-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The row a fragment lands on (`#client-…`) must not hide under nothing — but
   there is nothing sticky above it inside the column, so only a little air. */
.team-list.is-paged .team-item { scroll-margin-block: 8px; }
/* A name with no spaces in it (an email typed as a name, a long Latin
   handle) wraps inside the row instead of running out of the column. */
.team-list.is-paged .team-item-title { min-width: 0; overflow-wrap: anywhere; }

.team-list-scroll[aria-busy="true"] { opacity: .6; transition: opacity .15s ease; }
@media (prefers-reduced-motion: reduce) {
  .team-list-scroll[aria-busy="true"] { transition: none; }
}

.team-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 15px;
  border-block-end: 1px solid var(--team-hair);
  font-size: 12px;
}
.team-pager-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  text-decoration: none;
  touch-action: manipulation;
}
.team-pager-link:hover { border-color: var(--accent-border); }
/* A missing Previous (page one) or Next (the last page) keeps its place, so
   the range stays centred and the other button does not jump sides. */
.team-pager-link.is-off { visibility: hidden; min-width: 1px; padding: 0; border: 0; }
/* The arrows are drawn here so they point the reading direction in both
   languages — the team-back idiom. */
.team-pager-link.is-prev::before { content: "\2190"; }
.team-pager-link.is-next::after { content: "\2192"; }
[dir="rtl"] .team-pager-link.is-prev::before { content: "\2192"; }
[dir="rtl"] .team-pager-link.is-next::after { content: "\2190"; }
.team-pager-at { color: var(--muted); font-variant-numeric: tabular-nums; text-align: center; }
.team-empty-act { margin-block-start: 12px; }

@media (min-width: 901px) {
  /*
   * ONE SCREEN, THREE COLUMNS. `height` rather than the shell's usual
   * `min-height`: that is what lets `.team-main` and `.team-work` (both
   * already `min-height: 0` flex children, for exactly this) hand a bounded
   * height down to the columns, so each scrolls on its own. The pane already
   * has `overflow-y: auto`; it simply never had a height to overflow.
   */
  .team-shell.is-framed { height: 100vh; height: 100dvh; min-height: 0; }
  .team-shell.is-framed .team-work { overflow: hidden; }
  .team-shell.is-framed .team-rail { overflow-y: auto; }
  /*
   * AND EVERY SCROLLER A CONTAINING BLOCK. A scroller clips only what it
   * contains for positioning, and the pane is full of `.visually-hidden`
   * labels — absolutely positioned against the viewport, at wherever they
   * would have sat in an unscrolled page. They escaped the clip and stretched
   * the document to 1,578px of nothing under a 900px frame. Measured.
   */
  .team-shell.is-framed .team-rail,
  .team-shell.is-framed .team-list,
  .team-shell.is-framed .team-list-scroll,
  .team-shell.is-framed .team-detail { position: relative; }

  .team-list.is-paged {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .team-list.is-paged > * { flex: none; }
  .team-list.is-paged > .team-list-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Where the fixed tools end and the scrolling rows begin — without it a
       row scrolled half out of view reads as cut off by the order links. */
    margin-block-start: 8px;
    border-block-start: 1px solid var(--border);
  }
}

@media (max-width: 900px) {
  /* 44px of tap target on the phone, for the chips, the pager and the order
     links — this directory's whole set of controls. */
  .team-client-chips .team-box,
  .team-pager-link { min-height: 44px; }
  .team-client-chips .team-box { padding-inline: 12px; }
  /*
   * THE ORDER LINKS, ONE ROW. Five of them at 44px would wrap to three rows
   * and push the first client a third of the way down the phone, so they
   * scroll sideways instead — the chip rail's own answer to the same problem.
   */
  .team-list.is-paged .team-sortrow {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-block-end: 2px;
  }
  .team-list.is-paged .team-sortrow::-webkit-scrollbar { display: none; }
  .team-list.is-paged .team-sortrow .team-sort-k { flex: none; }
  .team-list.is-paged .team-sortrow .team-sort {
    flex: none;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 10px;
    white-space: nowrap;
    touch-action: manipulation;
  }
}

/* -------------------------------------------------------- working area */

.team-detail { flex: 1 1 420px; min-width: 0; background: var(--team-deep); overflow-y: auto; }
.team-back { display: none; }
.team-detail-empty {
  padding: 64px 26px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.team-detail-empty .team-empty-title { font-size: 15px; }
.team-detail-empty .team-empty-body { max-width: 36ch; font-size: 13px; line-height: 1.65; }

.team-pane { padding: 18px 20px 26px; }

.team-pane-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-start;
  justify-content: space-between;
}
.team-pane-title { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.team-pane-title h2 { margin: 0; font-size: 18px; font-weight: 700; }
.team-pane-when { font-size: 12px; color: var(--muted); font-weight: 400; }
.team-pane-meta { margin-block-start: 5px; font-size: 12.5px; color: var(--secondary); }
.team-pane-actions { display: flex; flex-wrap: wrap; gap: 7px; }

/* WHAT THIS CLIENT HAS DONE LATELY, under the pane head (owner, 2026-09-01).

   Deliberately NOT `.team-card`. The three client-win notices moved off the
   dashboard strip onto the record of the person they are about, and nothing
   here is asked of the coach — a bordered panel with the same weight as the
   assign control would say otherwise. A rule and muted type, which is the
   weight the meta line directly above already has.

   `border-block-start` and `padding-inline-start` rather than top/left: this
   pane is read right-to-left in Farsi and the marker has to change sides with
   it. */
.team-pane-news {
  margin-block-start: 12px;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--team-hair);
}

.team-pane-news-k {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--secondary);
}

.team-pane-news-list {
  margin: 5px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.team-pane-news-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px 8px;
  font-size: 12.5px;
  line-height: 1.55;
}

/* A flex child needs `min-width: 0` before it is allowed to be narrower than
   its own content, and an exercise name arrives from a coach's own program: a
   long one with no space in it would otherwise push the date out of the pane.
   The same shape the guidelines ask for wherever text meets flex. */

/* The sentence at the weight of the meta line above it, and the date at the
   weight every other date in this pane already has (`.team-history-at`). The
   news is context, not the subject: the subject is the client whose name is
   two lines up. `--muted` on a timestamp is what the audit of 2026-08-28 named
   as the first place to point an APCA pass — matched here rather than improved
   alone, because one brighter date in a pane of dimmer ones is a worse screen
   than a consistent one, and the row in backlog §9 says so. */
.team-pane-news-b {
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--secondary);
}

/* Tabular figures, because these ARE a column: five dates stacked one under
   another are compared down the page, and proportional digits make the same
   month read as two different widths. */
.team-pane-news-at {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}
.team-pane-actions .inline { display: inline; }
.team-pane-contact { text-align: end; }

.team-split {
  margin-block-start: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}
.team-split-main { flex: 1 1 320px; min-width: 280px; }
.team-split-side {
  flex: 1 1 240px;
  min-width: 230px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.team-card {
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--panel);
}
.team-card + .team-card { margin-block-start: 12px; }
/*
 * A PANE-LEVEL CARD IS 12px FROM WHATEVER IS ABOVE IT, not only from another
 * card. The sibling rule above cannot see a card whose predecessor is a FORM
 * wrapping a card (the client record on the Details view: the record card sits
 * inside <form class="team-details-form">, so the intake card after it matched
 * nothing and was drawn welded to it — 0px against the 12px every other pair
 * on the pane has; the owner's report, 2026-09-23), a sort row (the CRM's
 * "Group by") or a program head. Stated on the pane's own children so it
 * COLLAPSES with the 14px the pane head and sub-nav already carry, and so a
 * card inside a split column or a disclosure is untouched.
 */
.team-pane > * + .team-card { margin-block-start: 12px; }
.team-card-warn { border-color: var(--danger-border); background: var(--danger-bg); }

/* Add-a-coach, folded at the foot of the Coaches directory (§20.3). The list
   column has no padding of its own — the rows carry it — so this does. */
.team-invite { margin: 12px 15px 15px; }
.team-invite .team-card { margin-block-start: 10px; }
.team-invite label { display: block; margin-block-start: 10px; font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.team-invite input, .team-invite select { width: 100%; margin: 4px 0 0; }
.team-invite .team-pane-actions { margin-block-start: 12px; }
.team-card-head { display: flex; gap: 8px; align-items: center; justify-content: space-between; }
.team-card-k { font-size: 11.5px; color: var(--muted); }

/* ---- the articles tab (§22) -------------------------------------------- */

/* One category or tag's edit form in the taxonomy disclosure. */
.team-taxo-row {
  padding-block: 12px;
  border-block-end: 1px solid var(--border);
}
.team-taxo-row:last-of-type { border-block-end: 0; }

/* The body textarea earns monospace: it holds markdown, and hyphens and
   backticks need telling apart at a glance. */
/* 16px: a textarea is typed into, and the floor applies. Held back while
   `feature/article-design` (#48) was in review over these files; it merged, so
   the reason to leave it expired with it. */
.team-article-body { font-family: var(--mono, monospace); font-size: 16px; line-height: 1.55; }

/* Publish / schedule / unpublish / delete on one wrapping row. */
.team-article-pubrow {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin-block-start: 10px;
}
.team-article-schedule { display: flex; gap: 8px; align-items: center; }

/* The pictures section of the articles editor (2026-08-28). A list rather
   than a grid: each row carries a form, and a form in a grid cell fights the
   cell for its width. */
.team-article-imgwarn {
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid var(--warn-border);
  border-radius: 8px;
  background: var(--warn-bg);
  color: var(--warn);
  font-size: 13px;
}
.team-image-list { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 16px; }
.team-image {
  display: flex;
  gap: 14px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}
/* The thumbnail is capped by CSS and carries its REAL width/height in the
   markup: the attributes give the browser the aspect ratio to reserve, the
   cap decides how big it is drawn. */
.team-image-thumb {
  flex: 0 0 auto;
  width: 96px;
  height: auto;
  max-height: 96px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
}
/* min-width: 0 so the read-only markdown field can shrink instead of pushing
   the row wider than the pane (AGENTS.md: flex children need min-w-0). */
.team-image-body { min-width: 0; flex: 1 1 auto; }
/* The 16px floor, like every other control someone puts a caret in. Read-only
   is not the same as unfocusable — this field exists to be selected and copied
   out of, and iOS zooms the page to do it. It is alone on its row inside a
   `min-width: 0` flex child, so the larger type only makes it taller. */
.team-image-md { width: 100%; font-size: 16px; }
.team-image-cover {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-dim);
}
.team-image-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 10px; }

@media (max-width: 560px) {
  .team-image { flex-direction: column; }
  .team-image-thumb { width: 100%; max-height: 160px; }
}


.team-eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.team-readonly {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.team-kv { margin-block-start: 10px; display: flex; flex-direction: column; gap: 8px; font-size: 13px; font-weight: 500; }
.team-kv > div { display: flex; justify-content: space-between; gap: 10px; }
/* Audit F46 — the payment pane's owed/claimed/paid rows, which exist to be
   read against each other and are the amounts a coach approves money on. */
.team-kv span:last-child { font-variant-numeric: tabular-nums; }
.team-kv span:first-child { color: var(--secondary); }
.team-warn-text { color: var(--danger); }
.team-warn {
  margin-block-start: 11px;
  padding: 10px 12px;
  border: 1px solid var(--danger-border);
  border-radius: 9px;
  background: var(--danger-bg);
  font-size: 12px;
  line-height: 1.55;
  font-weight: 600;
  color: var(--danger);
}

/* The receipt, big enough to read a bank reference off — the round-3 brief's
   central ask for this pane. */
.team-proof {
  display: block;
  margin-block-start: 8px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-2);
}
.team-proof img { display: block; width: 100%; height: auto; }

.team-reference {
  margin-block-start: 8px;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 9px;
  background: var(--surface-2);
  font-family: ui-monospace, Menlo, monospace;
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
}
.team-contact { font-size: 13px; font-weight: 500; color: var(--text); }

.team-mode { margin-block-start: 7px; font-size: 16px; font-weight: 700; color: var(--accent); }
.team-instructions { margin-block-start: 12px; }
.team-instructions label { display: block; margin-block-end: 7px; font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.team-instructions textarea { width: 100%; margin: 0; }

.team-flag-list { margin-block-start: 10px; display: flex; flex-direction: column; gap: 7px; font-size: 12.5px; }
/*
 * WRAPS, because some of these labels are whole questions. The row is a
 * label/value pair and reads best as two columns, but `.team-flag-k` was
 * `flex: none` — so a key wider than the row could not shrink and could not
 * break, and simply ran out of the card. Measured at 390 on the client pane's
 * Details view: "Total training experience and how consistent it has been" is
 * 376px of key in a 288px row, so the intake group, the card and the pane all
 * reported 88px of content with nowhere to go.
 *
 * `flex: 0 1 auto` keeps the key at its natural width wherever there IS room —
 * every reading at 768 and above is unchanged — and lets it shrink where there
 * is not; `flex-wrap` then drops the VALUE to its own line rather than
 * squeezing it to one word per line, which is the same answer
 * `.team-card-warn` already gives a screening answer three rules down.
 */
.team-flag-list > div { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }
.team-flag-k { flex: 0 1 auto; min-width: 0; color: var(--muted); }
/* Intake facts are short values that sit well on one line. A SCREENING answer
   is free-form prose — "left knee, avoid deep flexion" — and squeezing it into
   the right-hand half wrapped it one word per line. It stacks instead. */
.team-card-warn .team-flag-list > div { flex-direction: column; align-items: flex-start; gap: 3px; }
.team-card-warn .team-flag-list > div > span:last-child { color: var(--text); line-height: 1.55; }

.team-chips { margin-block-start: 14px; display: flex; flex-wrap: wrap; gap: 7px; }
.team-chip {
  padding: 5px 11px;
  border-radius: 7px;
  font-size: 11.5px;
  font-weight: 600;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--secondary);
}
.team-chip.is-warn { border-color: var(--danger-border); background: var(--danger-bg); color: var(--danger); }
.team-chip.is-quiet { color: var(--muted); }
.team-chip.is-ok { border-color: var(--success-border); background: var(--success-bg); color: var(--success); }

.team-days { margin-block-start: 14px; display: flex; flex-wrap: wrap; gap: 6px; }
.team-day {
  padding: 6px 12px;
  border-radius: 7px;
  color: var(--secondary);
  font-size: 12.5px;
  font-weight: 500;
  text-decoration: none;
}
.team-day.is-current { background: var(--surface-2); color: var(--text); font-weight: 600; }

.team-table {
  margin-block-start: 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
}
/*
 * THE BUILDER'S TABLE DOES NOT CLIP, because a popup grows out of it.
 *
 * `.team-table` sets `overflow: hidden` so the square corners of its head and
 * its rows are cut by the 12px radius. That is right for every other table on
 * the dashboard and wrong for this one: the exercise combobox is
 * `position: absolute` on `.bld-row`, so the table was clipping it to its own
 * height. The owner, 2026-09-06: the suggestions "don't show up… they are
 * limited by the size of the entire program's container", and adding empty
 * rows made more of the list appear — because each row made the table taller.
 * Measured: the list ran 854->1358 and the table ended at 910, so 56px of a
 * 504px popup was visible.
 *
 * The corners are kept by rounding the two elements that actually paint one —
 * the head and the last row — rather than by clipping the whole box. A radius
 * on a child is a paint effect; `overflow` is a containing decision, and only
 * one of them also traps popups.
 */
.team-table.bld { overflow: visible; }
.team-table.bld > .team-table-head {
  border-start-start-radius: 11px;
  border-start-end-radius: 11px;
}
.team-table.bld > .team-row:last-child {
  border-end-start-radius: 11px;
  border-end-end-radius: 11px;
}

.team-table-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  justify-content: space-between;
  padding: 11px 15px;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}
.team-table-title { font-size: 13px; font-weight: 600; }
.team-table-meta { font-size: 11.5px; color: var(--muted); }
.team-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
  padding: 10px 15px;
  border-block-end: 1px solid var(--team-hair);
}
.team-row:last-child { border-block-end: 0; }
.team-row.is-warn { background: var(--danger-bg); }
.team-row-name { flex: 1 1 200px; min-width: 170px; font-size: 13.5px; font-weight: 500; color: var(--text); }
.team-row-scheme { flex: none; width: 74px; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--secondary); }
.team-row-note { flex: none; font-size: 12px; color: var(--muted); }
.team-row-warn { flex: none; font-size: 11.5px; font-weight: 600; color: var(--danger); }

/* The discount-code manager on the Pricing pane (§20.4). The list is
 * `.team-row`s like everything else on the team side; these two are only the
 * facts line and the folded create form, which borrows the invite form's
 * field rhythm because it is the same kind of thing. */
.team-disc-meta { flex: 2 1 260px; min-width: 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.team-disc-new { margin: 12px 15px 15px; }
.team-disc-new .team-card { margin-block-start: 10px; }
.team-disc-new label { display: block; margin-block-start: 10px; font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.team-disc-new input, .team-disc-new select { width: 100%; margin: 4px 0 0; }
.team-disc-new .team-pane-actions { margin-block-start: 12px; }

/* ------------------------------------------------ announcements (§3)
 *
 * The composer writes ONE announcement in two languages, so the two boxes sit
 * side by side rather than behind a toggle: what the coach needs to see is
 * whether the Farsi says what the English says. They stack on a phone, where
 * side-by-side would give each 160px.
 *
 * The preview reuses the same two-column frame, so what is being confirmed
 * lines up with what was written.
 */
.team-ann-form { margin: 0; }
.team-ann-langs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
}
.team-ann-lang { min-width: 0; }
.team-ann-langk {
  margin-block-end: 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.team-ann-lang label { display: block; margin-block-start: 10px; font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.team-ann-lang input, .team-ann-lang textarea { width: 100%; margin: 4px 0 0; }
.team-ann-input { resize: vertical; line-height: 1.6; }
.team-ann-form select { width: 100%; margin: 4px 0 0; }
.team-ann-form label[for="ann-audience"] {
  display: block;
  margin-block-start: 14px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--secondary);
}

/* The preview is what the CLIENT sees, so it is drawn as their row is: the
   title in the page's strong weight, the body with the writer's line breaks
   kept (`pre-wrap`, the same rule .notify-prose gets on the client side). */
.team-ann-preview { border-color: var(--accent); }
.team-ann-title { display: block; font-size: 14.5px; }
.team-ann-body {
  margin: .3rem 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--secondary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.team-ann-reach { margin: 14px 0 0; font-size: 13px; font-weight: 500; color: var(--text); }
.team-ann-send { margin-block-start: 4px; }
/* Said once, in the danger colour, directly above the only button that can
   do it. A send cannot be recalled. */
.team-ann-warn { margin: 8px 0 10px; font-size: 12px; color: var(--danger); }

.team-ann-sent { margin-block-start: 18px; }
.team-ann-meta { flex: 2 1 260px; min-width: 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }

/*
 * The CRM tab's three read-only tables.
 *
 * Built on `.team-row` rather than on a table of their own — a month of
 * confirmed payments and a list of quiet clients are the same shape as every
 * other row on the team side, and a second row system would drift from this
 * one the first time either changed.
 */
/* The figure, pushed to the end of the row and never wrapped off it. */
.team-crm-sum {
  flex: none;
  margin-inline-start: auto;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* How long since the client last did anything the app can see.
   DELIBERATELY NOT RED: silence is information, not an alarm — a client on
   holiday looks exactly like a client leaving, and colouring one of them
   decides which before anybody has looked. */
.team-crm-quiet {
  flex: none;
  margin-inline-start: auto;
  display: flex;
  gap: 7px;
  align-items: baseline;
  font-size: 12px;
  color: var(--muted);
}
/* A whole row that is a link. No button, no chevron, no hover lift — it goes
   to the client pane, which is where the coach was going anyway. */
.team-crm-row { color: inherit; text-decoration: none; }
.team-crm-row:hover { background: var(--surface-2); }

/* Narrow: the figure gets its own line rather than crushing the name. */
@media (max-width: 620px) {
  .team-crm-sum, .team-crm-quiet { margin-inline-start: 0; }
}

/* The shape beside the ledger (slice two). The chart is decorative — every
   figure it draws is printed in the table beside it — so it is quiet: one
   muted colour, no gridlines, a hairline baseline. Geometry arrives as SVG
   attributes from lib/crm-dashboard.js because the CSP forbids inline style;
   colour lives here, where the theme owns it. */
.crm-chart { margin-block: 10px 14px; }
.crm-chart svg { width: 100%; height: auto; display: block; }
.crm-chart-title {
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  letter-spacing: .04em;
  margin-block-end: 3px;
}
/* The event-log viewer's rows: timestamp · entity·event · client · payload.
   The payload is shape-only JSON and renders as quiet code; it wraps rather
   than scrolls, because a phone-width owner still has to be able to read it. */
.crm-ev { flex-wrap: wrap; }
.crm-ev-client { color: inherit; font-size: 12px; }
.crm-ev-payload {
  font-size: 11px;
  color: var(--muted);
  overflow-wrap: anywhere;
  flex: 1 1 100%;
}

/* The latency panel's sub-blocks: one heading per metric, table or the
   too-thin sentence beneath it. */
.crm-latency { margin-block-start: 12px; }
.crm-latency-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-block-end: 2px;
}

/* The funnel's bars. One scale for all five — fraction of what was requested
   — via the same data-pct → --pct mapping the records bars use, because the
   CSP forbids an inline width. Muted, single colour: the shape is the story,
   and a red "lost here" tint would paint a judgement about people who may
   simply not have paid yet. */
.crm-funnel-bar {
  flex: 1 1 auto;
  height: 8px;
  border-radius: 4px;
  background: var(--grid-line);
  overflow: hidden;
  margin-inline: 12px;
  min-width: 60px;
}
.crm-funnel-fill {
  display: block;
  height: 100%;
  inline-size: var(--pct, 0%);
  background: var(--accent);
  opacity: .55;
}

/* Declined/refunded counts beside a bucket's gross (finding 9). Muted and
   NOT red: a declined payment is a fact the gross label omits, not an alarm,
   and the no-red rule for silence holds for money too. */
.crm-ruled { color: var(--muted); font-size: 12px; }

/* -------------------------------------------- article performance (§25)
 *
 * Almost nothing new: the pane reuses the CRM's chart, funnel bar, row and
 * summary classes deliberately, so the dashboard has ONE look for "a number
 * beside a shape" rather than two that drift. What is here is the three
 * things that view needs and no other does.
 */

/* The row for the article you are standing on, inside the list of all of
   them. A tint and a heavier name, never colour alone — the title is bolder
   as well, so the cue survives a monochrome screen and a colour-blind reader
   (AGENTS.md: redundant status cues).

   The hover is stated for it SEPARATELY. `.team-crm-row.is-here` and
   `.team-crm-row:hover` have identical specificity, so the later of the two
   wins outright and the current row would otherwise be the one row on the
   list that gives no feedback under the pointer — which is exactly backwards
   from "interactive states increase contrast". */
.team-crm-row.is-here { background: var(--accent-bg); }
.team-crm-row.is-here:hover { background: var(--accent-border); }
.team-crm-row.is-here .team-row-name { font-weight: 700; }

/* An article title is a coach's free text and can be one very long unbroken
   word. `.team-row-name` carries a 170px floor so it cannot shrink to fit,
   which means without this the title runs out of the row rather than wrapping
   inside it. Scoped to the linked rows: the program rows this class was built
   for are exercise names and wrap on their own spaces. */
.team-crm-row .team-row-name { overflow-wrap: anywhere; }

/* A VISIBLE FOCUS RING on the three link shapes this pane is built from.
   There is no global `a:focus-visible` in this stylesheet — each component
   states its own (see the note by the articles section) — and none of these
   three had one, so the range chips, the list's order and every row on it
   were tabbable with nothing to show for it. Grouped: it is one decision, and
   it fixes the same three classes on the CRM and client panes with it. */
.team-sort:focus-visible,
.team-sub:focus-visible,
.team-crm-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The one caveat that has to be read rather than skimmed: a window longer
   than the anonymise period cannot answer uniqueness across all of it. Warm
   rather than red — it is a limit of the measurement, not an error. */
.astats-caveat { color: var(--secondary); font-weight: 500; }

/* Retention, under the pane. Quiet, and the last thing on the screen. */
.astats-retention {
  margin-block-start: 16px;
  font-size: 12px;
  color: var(--muted);
}

.crm-bar { fill: var(--accent); opacity: .55; }
.crm-axis { stroke: var(--border); stroke-width: 1; }
/* The span's two ends, under the plot — Latin data, pinned by the template. */
.crm-chart-axis {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-block-start: 2px;
}

/*
 * Narrow: the exercise gets its own line.
 *
 * The design's row is desktop-only — name at `flex: 1 1 200px; min-width: 170px`
 * with the scheme pinned at 74px beside it. Below roughly 600px of PANE the
 * name is squeezed to its 170px floor and the scheme sits 10px away from it,
 * which is what "the name and the reps are way too into each other" is. A long
 * name then wraps to three lines with the scheme baseline-aligned to the first,
 * and the note wraps to a line of its own regardless.
 *
 * Stacked instead: the name takes the full width, and the scheme, the technique
 * and any warning sit under it as one quiet line. Same information, in the order
 * you read it.
 */
@media (max-width: 900px) {
  .team-row { gap: 3px 10px; padding: 11px 14px; }
  .team-row-name { flex: 1 1 100%; min-width: 0; }
  .team-row-scheme { width: auto; }
}

/*
 * A day of daily reports inside a .team-row: label over value, per cell.
 *
 * The history was a line of bare values — a date, a number, two words — and a
 * number with no title above it could be the weight, the sleep or the steps.
 * This is the .team-tile idiom the summary above the list already uses, at row
 * scale: the key is .team-tile-k exactly (11px/500/muted), and the value takes
 * .team-row-scheme's 13px/600 with .team-row-name's --text, because here the
 * label is the quiet half and the value is the one being read.
 *
 * The 74px floor is .team-row-scheme's width, so cells still line up down the
 * list on days that report the same fields.
 */
.team-daily-cell { flex: none; min-width: 74px; }
.team-daily-k { font-size: 11px; font-weight: 500; color: var(--muted); }
.team-daily-v { margin-block-start: 4px; font-size: 13px; font-weight: 600; color: var(--text); }

/*
 * The note takes a line of its own at every width — the same `flex: 1 1 100%`
 * the narrow rule gives .team-row-name. A sentence sharing a line with six
 * numbers is what forces the numbers to wrap unpredictably, and prose at the
 * end of a row of figures is the part a coach stops reading.
 */
.team-daily-note { flex: 1 1 100%; min-width: 0; }
.team-daily-note .team-daily-v { font-size: 13.5px; font-weight: 500; line-height: 1.45; }

/* The message itself, at reading size with the design's gold edge. */
.team-message {
  margin: 16px 0 0;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-inline-start: 2px solid var(--accent);
  border-radius: 12px;
  background: var(--panel);
  font-size: 15px;
  line-height: 1.75;
  color: var(--text);
  text-wrap: pretty;
}
/* The answer already sent, shown back to the coach. The gold edge means
   "the client said this" everywhere else on this screen, so our own words
   take the quiet border instead — otherwise the pane has two golden columns
   and neither says whose voice it is. */
.team-message.is-reply {
  border-inline-start: 2px solid var(--border-strong);
  font-size: 14px;
  white-space: pre-wrap;
}

.team-reply-note { margin: 10px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--secondary); }

.team-tiles {
  margin-block-start: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
}
.team-tile { padding: 12px 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--panel); }
.team-tile-k { font-size: 11px; font-weight: 500; color: var(--muted); }
/* Audit F46. tabular-nums because these are read ACROSS as a row of counts,
   and down a column across clients — proportional digits make 4 and 11 the
   same width (AGENTS.md). Arrived independently on two branches, which is
   itself the finding being real. */
.team-tile-v {
  margin-block-start: 4px;
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------- the engagement strip
 *
 * The coach's read of what a client bought. Deliberately NOT the client's
 * .engagement: that one is sized for the account page's body type, and reusing
 * it here would drop 17px headings into a pane whose scale is 11–12.5px.
 */
.team-eng { margin-block-end: 14px; }
.team-eng-head {
  margin-block-start: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 2px 8px;
  align-items: baseline;
  justify-content: space-between;
}
.team-eng-block { font-size: 14px; font-weight: 700; }
.team-eng-meta { font-size: 11.5px; color: var(--muted); }
.team-eng-bar { margin-block-start: 8px; display: flex; gap: 3px; }
.team-eng-bar > span {
  flex: 1 1 0;
  height: 3px;
  border-radius: 2px;
  background: var(--border-strong);
}
.team-eng-bar > span.is-done { background: var(--accent); }

/*
 * The history, as a real table (owner's §16.9).
 *
 * It was a stack of flex rows, which read fine at ten events and became an
 * unreadable ribbon at two hundred — the dates and the events never lined up
 * with each other, because each row sized itself. A table is what makes a
 * column a column.
 *
 * The wrapper scrolls, not the page: one long change line — "knee, not sure →
 * torn MCL, seen a doctor" — must not be able to push the dashboard sideways
 * on a phone.
 */
.team-history-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-block-start: 12px;
}
.team-history-count { margin-inline-start: 5px; color: var(--muted); font-weight: 400; }

.team-history-wrap { margin-block-start: 10px; overflow-x: auto; }
.team-history { width: 100%; border-collapse: collapse; }
.team-history th {
  padding: 0 8px 6px;
  border-block-end: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  text-align: start;
  white-space: nowrap;
}
.team-history-sort { color: inherit; text-decoration: none; }
.team-history-sort:hover { color: var(--text); }
.team-history-row td { padding: 7px 8px; border-block-end: 1px solid var(--border); vertical-align: baseline; }
.team-history-row:last-child td { border-block-end: 0; }
.team-history-at { width: 74px; font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.team-history-what { min-width: 12rem; font-size: 12.5px; font-weight: 500; color: var(--nav-link); }

/* The amount, the revision, the reason a payment was declined — the detail that
   qualifies the event without competing with it. */
.team-history-sub { margin-inline-start: 6px; font-size: 11.5px; font-weight: 400; color: var(--muted); }

/* `90kg → 88.5kg`. The old value is quiet and the new one is not: the question
   this line answers is "what is it now", with "what was it" as the context. */
.team-change { margin-inline-start: 6px; }
.team-change-was { color: var(--muted); }
.team-change-now { color: var(--text); font-weight: 600; }

/* Money moving is the event a coach scans this list for. */
.team-history-row.is-payOk .team-history-what { color: var(--success); }
.team-history-row.is-payNo .team-history-what,
.team-history-row.is-cancelled .team-history-what { color: var(--danger); }
.team-history-row.is-delivered .team-history-what { color: var(--text); }

/*
 * THE REFERENCE TAB'S SAVE LIST, which is not the client history above.
 *
 * It borrowed those classes and got none of them: they are written for a
 * TABLE (`.team-history-row td`), and a `width` or `min-width` on an inline
 * span does nothing at all. Its own two classes now, and a list rather than a
 * grid — each row is one fact, and a rule between rows is all the separation
 * two short runs of digits need.
 */
.ref-history { margin: 8px 0 0; padding: 0; list-style: none; }
.ref-history-row {
  display: flex;
  gap: 10px;
  padding-block: 7px;
  border-block-end: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text);
  /*
   * Asked for, and the shipped face does not answer. Archivo 500 has no
   * tabular figures — measured, ten `0` glyphs are 80.0px against ten `1`
   * glyphs at 70.0px — so the clock column still shifts by a pixel or two
   * between rows. Kept because it is the correct request and costs nothing if
   * the face ever gains the feature; the claim that it lines the digits up is
   * removed, because it does not. (QA, #134.)
   */
  font-variant-numeric: tabular-nums;
}
.ref-history-row:last-child { border-block-end: 0; }
/* The day carries the row; the clock only separates two saves within it. */
.ref-history-date { font-weight: 500; }
.ref-history-time { color: var(--muted); }

.team-links { display: flex; flex-direction: column; gap: 8px; }
.team-links a {
  padding: 11px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  text-align: start;
  text-decoration: none;
}
.team-links a:hover { border-color: var(--accent-border); }

/* -------------------------------------------------------------- mobile */

/* The chip rail exists for the phone only; the grouped rail is the desktop's.
   Stated here so that the pair is never both on. */
.team-chiprail { display: none; }

@media (max-width: 900px) {
  /* The chips are the rail; the queue becomes the page; picking an item pushes
     to the working area. All of it from the URL having an id or not, so no
     script is involved. */
  .team-work { flex-wrap: wrap; align-content: flex-start; }

  .team-rail { display: none; }

  .team-chiprail {
    flex: 0 0 100%;
    align-self: flex-start;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 10px 12px;
    border-block-end: 1px solid var(--border);
    background: var(--panel);
    overflow-x: auto;
    /* One row, always. Without this the chips wrap into a five-line block and
       the queue starts a third of the way down the phone again. */
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .team-chiprail::-webkit-scrollbar { display: none; }

  .team-chiptab {
    flex: none;
    display: flex;
    align-items: center;
    gap: 6px;
    /* 44px of tap target, which is the reason for the padding and the reason
       min-height is stated rather than left to the line box.

       IT SAID 40 UNTIL 2026-09-14, deliberately and four pixels short. The
       audit's floor is 44 and this is the dashboard's ENTIRE navigation on a
       phone — every tab a coach reaches is one of these chips, so it is the
       most-pressed target on the screen and it was the one that missed.

       Found by rendering the Needs-you pane in Chromium at a true 390px and
       measuring the current chip: 125 x 40. Three passes had reasoned about
       this screen from the stylesheet and none had put a browser in front of
       it; the two targets fixed alongside this one (41.6 and 42) were both
       found by arithmetic, and arithmetic did not find this one because
       nobody had thought to check a rule that states its own number.

       The padding is untouched, as with the other two — a chip that grew
       taller while losing width would be a worse target, not a better one. */
    min-height: 44px;
    padding: 0 13px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface-1);
    color: var(--nav-link);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
  }
  .team-chiptab.is-current {
    border-color: var(--accent-border);
    background: var(--accent-bg);
    color: var(--accent);
  }
  .team-chiptab.is-current .team-tab-count { background: var(--accent); color: var(--accent-ink); }

  /* The queue is the page until an item is explicitly PICKED — keyed off
     ?id= being present, not off something being selected, or the default
     selection would push a coach straight past the list they came for. */
  .team-list { flex: 1 1 100%; border-inline-end: 0; }
  .team-list.is-pushed { display: none; }

  .team-detail { flex: 1 1 100%; }
  .team-detail:not(.is-pushed) { display: none; }

  .team-back {
    display: flex;
    align-items: center;
    gap: 7px;
    width: 100%;
    /* 44, the phone floor — it is the only way back to the list. */
    min-height: 44px;
    padding: 10px 16px;
    border: 0;
    border-block-end: 1px solid var(--border);
    background: var(--panel);
    color: var(--secondary);
    font-size: 12.5px;
    font-weight: 600;
    text-align: start;
    text-decoration: none;
  }
  /* The arrow is drawn here rather than typed into the template: a literal "←"
     in a Farsi line is a neutral character that lands where the bidi algorithm
     puts it and still points at the English margin. */
  .team-back::before { content: "\2190"; font-size: 14px; line-height: 1; }
  [dir="rtl"] .team-back::before { content: "\2192"; }
  /* The queue's name yields before the count does: "1 waiting" is the half a
     coach is reading the bar for, and the Farsi names run long. */
  .team-back-k {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .team-back-n {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--muted);
    font-size: 11px;
    font-weight: 700;
  }
}

/* --------------------------------------------- dashboard: manage sections
 *
 * The rail carries two groups now. Work is the queues; Manage is the standing
 * configuration — reference files, prices, accounts, logs. The heading is what
 * keeps "what needs me today" answerable from a rail of twelve.
 */

.team-rail-group {
  padding: 12px 12px 5px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The first heading sits against the rail's own top padding. */
.team-rail-group:first-child { padding-block-start: 2px; }

/* ------------------------------------------- collapsible rail groups
 *
 * <details>/<summary>, so collapsing needs no script and no stored state.
 * The group holding the open tab is rendered `open` by the template — see
 * team.eta — so a coach can never collapse their way out of seeing where
 * they are.
 */
/*
 * A REAL BOX, not `display: contents`.
 *
 * `display: contents` was the tidy-looking version — it keeps the tabs as
 * direct flex children of the rail, so their spacing needs no new rule. It
 * also breaks the collapse outright: with no box on the <details>, closing it
 * rotated the triangle and left every tab at `display: flex`, full height.
 * The disclosure looked like it worked and hid nothing.
 *
 * So the <details> becomes a flex column that reproduces the rail's own gap
 * for the tabs inside a group; the rail's gap then sits between the groups.
 */
.team-rail-sect {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.team-rail-sect > summary.team-rail-group {
  display: flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.team-rail-sect > summary.team-rail-group::-webkit-details-marker { display: none; }
.team-rail-sect > summary.team-rail-group:hover { color: var(--secondary); }

/* The disclosure triangle, drawn rather than inherited, so it can sit at the
   START of the label and rotate. `::marker` is not reliably stylable and the
   default arrow sits where the writing direction puts it. */
.team-rail-sect > summary.team-rail-group::before {
  content: "";
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-inline-start: 4px solid currentColor;
  border-block-start: 3.5px solid transparent;
  border-block-end: 3.5px solid transparent;
  transition: transform 120ms ease;
}
.team-rail-sect[open] > summary.team-rail-group::before { transform: rotate(90deg); }
/* RTL: the closed triangle must point INTO the text, which is the other way. */
[dir="rtl"] .team-rail-sect > summary.team-rail-group::before { transform: rotate(180deg); }
[dir="rtl"] .team-rail-sect[open] > summary.team-rail-group::before { transform: rotate(90deg); }

.team-rail-group-k { flex: 1 1 auto; }

/* What is waiting inside a COLLAPSED group. Shown open too — the number is the
   same fact either way, and hiding it on open would make it flicker. */
.team-rail-group-n {
  flex: 0 0 auto;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0;
}

/* A section with no queue — pricing, security — takes the whole frame. */
.team-detail.is-only { flex: 1 1 100%; }

/* 14px of air above a row of buttons that FOLLOWS something — which is every
   use of it but one. The challenge card (§14) puts an actions row INSIDE
   `.team-inline-form`, where it is a flex item on a line whose other items are
   the fields, and the row is `align-items: center`: a 14px block-start there
   is not air above anything, it is 7px of the item's own margin box below the
   centre. Measured on the client pane at 1440, 2026-08-29: the Save and Retire
   buttons sat 7px under the middle of the three fields they act on. */
.team-actions-row { margin-block-start: 14px; }
.team-inline-form > .team-actions-row { margin-block-start: 0; }

.team-inline-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-block-start: 10px; }
.team-inline-form select { width: auto; min-width: 9rem; margin: 0; }

/*
 * A TEXT FIELD IN ONE OF THESE ROWS IS A SHORT ANSWER, NOT A PAGE FIELD.
 *
 * The page rule gives every text input `width: 100%` and a 32rem ceiling,
 * which in a row shared with a button means the button is what gets squeezed.
 * Reversed here: the field takes what the row leaves, from an 11rem basis, and
 * `min-width: 0` lets it shrink past its own content instead of holding the
 * row wider than the card — the payments pane head is the narrow case, and at
 * 390px the pair wraps in place rather than pushing the head out of the pane.
 *
 * DIRECT CHILD, and that is load-bearing: `flex-basis` is a length along the
 * container's own main axis, and this one is a WIDTH only because
 * `.team-inline-form` is a row. The pause card (§14) wraps each field in a
 * `label.team-pause-field`, which is `flex-direction: column` — so as a
 * descendant selector this rule reached the reason box and gave it an 11rem
 * HEIGHT. Measured on the client pane at 1440, 2026-08-29: a single-line text
 * input drawn 176px tall, which pushed the row to 203px and left the two date
 * fields and the Save button floating in the middle of it.
 *
 * There are exactly two inputs this selector has ever matched — the payments
 * pane's decline reason, which is the direct child it was written for, and the
 * pause card's, which never should have been one.
 */
.team-inline-form > input[type="text"] { flex: 1 1 11rem; min-width: 0; max-width: 20rem; margin: 0; }

/*
 * ONE HEIGHT FOR EVERY CONTROL IN A ROW (the owner's report, 2026-09-23).
 *
 * A page field is 16px type on the page's 1.6 line inside 11px of padding —
 * 49.6px — and .btn.btn-small is 13px type in 10px of padding — 37.6px. Put
 * side by side in a row they are 12px apart in height, and it was every row:
 * the assign-coach select against its button on the client's Overview, the
 * eight resolver rows on a program with unmatched names, the by-hand form on
 * Generate, the coach's role select, and the pause card's three fields (the
 * two date inputs at a third height again, 43.2px, because `type=date` was
 * never in the 16px rule).
 *
 * The payments pane's decline field was the one row already matched — by
 * hand, to the button's box, with a comment saying the height must be
 * matched rather than the type because 16px is the floor for anything typed
 * into. That is the rule, stated once as a token instead of once per row:
 * the field is EXACTLY the button's box, the button is AT LEAST it, so both
 * arrive at the same number whatever the font does. A closed select and a
 * single-line input clip nothing at a fixed height; the browser centres the
 * text in the box on its own, which is why the block padding goes to 0
 * rather than being halved.
 *
 * 44px on a phone — the same two conditions the sub-nav and the builder's row
 * controls use — because AGENTS.md's mobile floor is 44 and a row is the one
 * place the button and the field can be raised together without the button
 * drifting from the rest of the pane on a laptop.
 */
.team-inline-form,
.team-resolve,
.team-deliver { --control-h: 38px; }
@media (pointer: coarse), (max-width: 560px) {
  .team-inline-form,
  .team-resolve,
  .team-deliver { --control-h: 44px; }
}
.team-inline-form input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.team-inline-form select,
.team-resolve select,
.team-deliver select {
  height: var(--control-h);
  padding-block: 0;
  font-size: 16px;
  /* A name the box cannot fit ends in an ellipsis, not mid-letter (QA on
     #183 measured "Coach Alexandra Fernández-Whitfield" cut at "Fernández-\"
     in English and at its START in Farsi); the whole name is read in the
     opened list. */
  text-overflow: ellipsis;
  line-height: 1.2;
}
.team-inline-form .btn,
.team-resolve .btn,
.team-deliver .btn { min-height: var(--control-h); }
/* A captioned column (the pause card's "First day" over its input, the
   block-challenge card's "Weight") is taller than the button beside it by its
   caption, so centring put the button 12px above the fields' middle. These
   two rows align on the controls' bottom edge, which is the one line every
   captioned and uncaptioned control in them shares. */
.team-pause .team-inline-form,
.bch-add { align-items: flex-end; }
/* At 16px a date input is 163px wide, and two of them no longer share a 320px
   row: they wrapped to a line each, and the pause card grew 78px on a phone.
   Half the row each, on the narrow condition only — a laptop keeps the
   intrinsic width, and a coarse pointer on a wide screen must not get a
   460px date field. */
@media (max-width: 560px) {
  .team-pause-field:not(.team-pause-reason) { flex: 1 1 calc(50% - 4px); min-width: 0; }
  .team-pause-field input { width: 100%; min-width: 0; }
}
/* THE BUTTON ON A PHONE ROW (the QA run on #181, 2026-09-23).
   Equal heights did not make the rows fit: at 390 the assign card's row is
   320px and label 53 + select 161 + Assign 143 is 373, so the button dropped
   a line and sat at the start of it, 161px of nothing to its right; the
   pause card's reason field and its button each took a line of their own.
   A wrapped button that looks accidental is the bug; a full-width button on
   its own line UNDER the fields is a layout — provided the fields above it
   share the row. So on the narrow condition the select beside its label
   takes what the label leaves, the reason field takes the whole row (it
   did already, by arithmetic; now by rule), and both buttons take the whole
   row too, at the row's own 8px gap. `flex-basis`, not `width`: the row is
   a flex container and the button stays an item of it. The by-hand and
   resolver rows are not here — their select and button still fit one line. */
@media (max-width: 560px) {
  .team-assign .team-inline-form select { flex: 1 1 9rem; min-width: 0; }
  .team-pause-reason { flex-basis: 100%; }
  .team-assign .team-inline-form .btn,
  .team-pause .team-inline-form .btn { flex: 1 1 100%; }
}

/*
 * WHOSE CLIENT THIS IS — the assign card on the client pane, and the bulk
 * form under the Clients list (owner-requested, 2026-08-29).
 *
 * A dashboard card like .team-pause and .team-challenge beside it, reusing
 * .team-inline-form for its row of controls rather than inventing a fourth
 * form shape for this one pane.
 */
.team-assign { margin-block-start: 14px; }

/*
 * SAYING SOMETHING TO A CLIENT WHO IS NOT IN A CONVERSATION (2026-08-30).
 *
 * The fourth card in the same stack, and it needs nothing the stack does not
 * already carry: a top margin, and a composer that behaves like the pane's
 * other textareas. `.freetext` supplies the type and the resize behaviour. It
 * is deliberately NOT styled as a chat — no bubbles, no thread — because it is
 * one message, and a box that looks like a conversation is a promise this
 * surface does not make.
 */
.team-note { margin-block-start: 14px; }
/* The base rules already give both controls their full width, 16px type and
   surface; all this stack needs is the gap under their labels. */
.team-note textarea,
.team-note select { margin-block-start: 6px; }

/*
 * THE COACH'S WAY INTO A CONVERSATION THAT DOES NOT EXIST YET (2026-08-29).
 *
 * Another card in the same stack, so it needs nothing but its own top margin
 * and a composer that behaves like the pane's other textareas. `.freetext`
 * already carries the type and the resize behaviour; what is added here is
 * the label above it, which the pane's other cards get from .team-invite and
 * this one is not inside.
 */
.team-chat-open { margin-block-start: 14px; }
.team-chat-open label {
  display: block;
  margin-block-start: 10px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--secondary);
}
.team-chat-open textarea { margin: 4px 0 0; }
/*
 * The bulk form's checkbox list.
 *
 * A real <fieldset>, so the group has one accessible name instead of fifteen
 * unrelated ticks. It scrolls at 15 clients rather than at 50, because the
 * form sits inside the QUEUE column and a fifty-row list there pushes the
 * coach select and its button off the bottom of the screen — the two controls
 * the list exists to be used with.
 */
.team-assign-pick {
  margin: 10px 0 0;
  padding: 0;
  border: 0;
  max-block-size: 17rem;
  overflow-y: auto;
}
.team-assign-pick legend {
  padding: 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--secondary);
}

/*
 * One client per row: the tick, the name, and who holds them today.
 *
 * `min-block-size` is the touch target — a bare checkbox is 16px and the whole
 * row is what a thumb lands on, so the row is what has to be big enough. The
 * checkbox overrides .team-invite's `width: 100%`, which is written for the
 * text fields on the add-a-coach form and would otherwise draw a tick box the
 * width of the column.
 */
.team-assign-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  align-items: center;
  min-block-size: 44px;
  padding: 4px 0;
  border-block-end: 1px solid var(--border);
  font-size: 13px;
  font-weight: 400;
  color: var(--text);
}
.team-assign-row:last-child { border-block-end: 0; }
.team-assign-row input[type="checkbox"] {
  inline-size: auto;
  min-inline-size: 0;
  margin: 0;
  flex: none;
}
/*
 * A CLIENT'S NAME IS USER CONTENT, so it has to survive being long. As a flex
 * child it would otherwise hold the row — and therefore the whole queue column
 * — wider than the pane: `min-inline-size: 0` lets it shrink past its own
 * content and `overflow-wrap` lets a single unbroken name break rather than
 * scroll the column sideways.
 */
.team-assign-name {
  font-weight: 600;
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.team-stack { display: flex; flex-direction: column; gap: 4px; margin-block-start: 10px; }
.team-stack label { font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.team-stack input { margin: 0 0 6px; }
.team-stack button { align-self: flex-start; margin-block-start: 6px; }

/* Reference files and stack traces. Monospaced and LTR: these are Markdown with
   English structural keywords, whatever language their content is in. */
/* THE EXERCISE PICKER above the markdown editor (builder pane, slice six).
   Chips are the muscle filter; the list is plain buttons, keyboard-reachable
   as buttons are, at the 44px the rest of the pane's controls keep. */
.rev-chip {
  min-height: 32px;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.rev-chip[aria-pressed="true"] {
  background: var(--accent-bg);
  border-color: var(--accent-border);
  color: var(--accent);
}
/* A filter chip is 32px tall by design, which is under the 44px a thumb
   needs: on a coarse pointer the chip grows rather than the hit area being
   faked with padding nobody can see. */
@media (pointer: coarse) {
  .rev-chip { min-height: 44px; padding-inline: 14px; }
}
.rev-chip:focus-visible,
.rev-option:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rev-picker-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-block-start: 6px;
  /* Twelve rows pushed Save seven phone-screens down (the design pass on
     the first cut); the list scrolls inside itself instead. */
  max-height: 40vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* ---------------------------------------------- the in-row exercise combobox
 *
 * The picker moved INTO the Exercise cell on 2026-09-04 (the owner: "i dont
 * want a separate bar to the left to search for an exercise"). The panel it
 * replaced was a block in the flow; this is a popup, so it has to be taken out
 * of the flow or every row below would jump down as it opens.
 *
 * Anchored to the ROW rather than the cell: the cell is a bare <input> with no
 * element to position against, and the row is already the box the columns are
 * laid out in. `inset-inline-start: 0` therefore puts it under the Exercise
 * column in both directions without a single logical property being flipped by
 * hand.
 */
.bld-row { position: relative; }
.bld-combo {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 0;
  z-index: 20;
  /* As wide as the Exercise column at least, and never wider than the row —
     an option is a name plus two pieces of meta and reads badly in 160px. */
  min-width: min(320px, 100%);
  max-width: 100%;
  margin-block-start: 4px;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  /* Opaque: it sits over the rows beneath it, which are text. */
  background: var(--panel);
  box-shadow: 0 10px 28px rgb(0 0 0 / 45%);
}
/* The meal table's list is the row's, and so is the Technique column's — both
   sit in a cell at the far end of the row from where `.bld-combo` hangs. See
   exercisepicker.js. */
.bld-combo.is-food,
.bld-combo.is-technique { inset-inline-end: 0; max-width: none; }
.bld-combo-muscles {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-block-end: 8px;
}
.bld-combo-status {
  margin: 0 0 6px;
  color: var(--muted);
  font-size: 12px;
}
.bld-combo-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* The list scrolls inside itself rather than pushing the page: twelve rows
     under a cell halfway down a forty-row day is most of a phone screen. */
  max-height: 46vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* The keyboard's highlight. Not :hover — arrowing and pointing are different
   gestures and only one of them can be in two places at once. */
.rev-option.is-active {
  border-color: var(--accent-border);
  background: var(--accent-bg);
}

.rev-picker-rule {
  margin-block-start: 6px;
  padding-block-start: 6px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 12px;
}
.rev-option {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  align-items: baseline;
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
  /* A Glossary name can be long in either script; the row wraps it. */
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Pointing gets feedback too, and a quieter kind: the loud state above belongs
   to the keyboard, and a pointer that lit an option the same way would make two
   options look chosen at once. Behind `hover: hover` so a touch screen's sticky
   hover does not leave a row lit after the finger has gone. */
@media (hover: hover) {
  .rev-option:hover { border-color: var(--border-strong); background: var(--surface-2); }
}
.rev-option-name { font-weight: 600; }
/* A TECHNIQUE'S DEFINITION IS A SENTENCE, so it takes its own line rather than
   sitting beside the name the way a portion and a calorie count do. Without
   this the option's baseline row puts a two-word name and eleven words of
   prose on one line at desk width and breaks them unevenly at every other. */
.rev-option-def { flex-basis: 100%; font-size: 13px; color: var(--muted); }
/* An option's meta is a portion and a calorie figure beside a name — numbers
   read against each other down the list, so they line up. */
.rev-option-meta { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }

.team-code {
  display: block;
  width: 100%;
  margin-block-start: 10px;
  padding: 13px 15px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--panel);
  color: var(--text);
  font-family: ui-monospace, Menlo, monospace;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-x: auto;
  resize: vertical;
}
/*
 * .team-code is worn by a <pre>, a <div> and a <textarea>, and the size is the
 * one thing the three cannot share. A stack trace and a set of recovery codes
 * are READ — 12.5px is what keeps a wide line on one row. The textarea is
 * typed into, so it takes the 16px floor.
 *
 * Both are element-qualified rather than one overriding the other on the bare
 * class: `npm run check` resolves a selector against the markup, not against
 * the cascade, so `.team-code { font-size: 12.5px }` reads to it — correctly —
 * as a rule that lands on a textarea below the floor. Saying which shape each
 * size is for is also simply truer than relying on source order.
 */
pre.team-code,
div.team-code { font-size: 12.5px; }
textarea.team-code { font-size: 16px; }

.team-price-field { flex: none; width: 8.5rem; }
/* A card number is sixteen digits and a bank line is a sentence, so neither
   fits the price field's 8.5rem. Growable with a floor instead, and min-width:0
   so a long value shrinks the field rather than pushing the row sideways. */
.team-dest-field { flex: 1 1 13rem; min-width: 0; }
/* Each of these sits under its own label in a stacked cell, not on a line with
   a button, so the 16px floor makes the cell taller and nothing has to be held
   against it. 8.5rem still holds a price and 13rem a card number at 16px. */
.team-price-field input,
.team-dest-field input {
  width: 100%;
  margin: 0;
  padding: 7px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px;
}

/* A failed send, an unresolved error: the one tier pill that is not neutral. */
.team-tier.is-fail {
  background: var(--danger-bg);
  border-color: var(--danger-border);
  color: var(--danger);
}

/* The price field carries its own market and currency label — the two markets
   are not in the same currency, and an unlabelled pair of boxes is a guess. */
.team-price-field label,
.team-dest-field label {
  display: block;
  margin-block-end: 4px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ------------------------------- dashboard: a client's three sub-views
 *
 * Overview, Program and Photos are three ways of looking at ONE client, so they
 * sit inside the pane rather than on the rail — putting them there would ask
 * "which client" twice.
 */

/*
 * Spacing under the pane's two header elements.
 *
 * Every block that follows them carried its own top margin — .team-days 14px,
 * .team-tiles 16px, .team-table 12px — except a bare .team-card, which has
 * none: `.team-card + .team-card` spaces cards from EACH OTHER, and the first
 * one in a pane has no card before it. So Programs, Intake, Photos and the
 * generation summary all opened with their first card welded to the tab strip.
 *
 * Fixed on the header rather than on the card, so it COLLAPSES: the pane is a
 * block container, adjacent margins take the larger of the two, and everything
 * that already spaced itself correctly is untouched.
 */
.team-pane-head,
.team-subnav { margin-block-end: 14px; }

.team-subnav {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-block-start: 14px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--border);
}
.team-sub {
  padding: 6px 12px;
  border-radius: 8px;
  color: var(--secondary);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.team-sub:hover { background: var(--surface-2); color: var(--text); }
.team-sub.is-current { background: var(--accent-bg); color: var(--accent); }

/*
 * THE PANE'S OWN NAVIGATION, AT 44PX WHERE A THUMB AIMS AT IT (the owner's
 * answer to the slice-ten design pass, question 10, 2026-09-22).
 *
 * The sub-nav and the day tabs were 32px — `padding: 6px 12px` on 12.5px type
 * — with 4px between neighbours, on a row of up to six targets that wraps to
 * two lines on a phone. The shell's tab rail directly above them is taller and
 * the builder's row controls below them are raised to 44px on the same
 * condition; the pane's primary navigation was the smallest thing on it.
 * AGENTS.md: "hit target >=24px (mobile >=44px)".
 *
 * The same two conditions the rest of the stylesheet uses for "a phone":
 * a coarse pointer at any width, or a narrow window whatever the pointer.
 * Width is untouched apart from the inline padding, so the row still wraps
 * where it wrapped; it only gets taller.
 */
@media (pointer: coarse), (max-width: 560px) {
  .team-sub,
  .team-day {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 14px;
  }
  .team-subnav,
  .team-days { gap: 6px; }
}

.team-prog-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-start;
  justify-content: space-between;
  margin-block-start: 16px;
}

/* Every problem used to be a machine code and every one of them was red. Now
   that they are sentences and only some of them refuse delivery, the list reads
   at the weight of a warning and the ones that BLOCK are the red ones — colour
   is never the only cue, so the sentence and the weight carry it too. */
.team-problems {
  margin: 10px 0 0;
  padding-inline-start: 18px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--secondary);
}
.team-problems > li.is-blocking { color: var(--danger); font-weight: 600; }

.team-resolve {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-block-start: 10px;
}
.team-resolve-name { flex: 1 1 12rem; font-size: 13px; font-weight: 500; color: var(--text); }
.team-resolve select { width: auto; flex: 1 1 14rem; margin: 0; }

/* The day heading inside the program table — a band, not a row. */
.team-day-head {
  padding: 9px 15px;
  border-block-end: 1px solid var(--team-hair);
  background: var(--surface);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--accent);
}

.team-raw { margin-block-start: 16px; }
.team-raw > summary {
  cursor: pointer;
  padding: 9px 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--secondary);
}
.team-raw > summary:hover { color: var(--text); }
.team-raw .team-code { max-height: 32rem; overflow-y: auto; }

/* ------------------------------------------------------------- photo sets */

.team-photoset { margin-block-start: 14px; }
.team-photoset-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
}
.team-photoset-date { font-size: 13px; font-weight: 600; }
.team-photoset-meta {
  display: flex;
  gap: 7px;
  align-items: center;
  font-size: 11.5px;
  color: var(--muted);
}

/*
 * DELETING A WHOLE SITTING, from the bar above the strip.
 *
 * A REAL 44px BOX. `.btn.btn-small` is 13px text in 10px of padding — about
 * 38px tall — and this control destroys a client's photographs irreversibly.
 * The audit's finding F26 was exactly this shape of mistake one level down (a
 * 23px photo delete under every thumbnail in a phone grid), and the fix there
 * was a real box; the same rule has to hold for the control that removes eleven
 * photos rather than one. inline-flex because a `min-height` on an
 * inline-block anchor grows the box without moving the text into the middle of
 * it.
 *
 * THE COLOUR IS `.btn-danger`'s, not a second copy of it. That class is already
 * the app's outlined-red treatment — a column of solid red buttons down a list
 * of sittings would read as an instruction — so this adds the box and nothing
 * about how danger looks.
 */
.team-photoset-del {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  /* No 300ms wait for a second tap that is not coming. */
  touch-action: manipulation;
}

/* Download and Delete, as one group.

   The bar is `justify-content: space-between`; without this box the two
   controls become two more spread items and end up at opposite ends of the
   row, with the destructive one nearest the date it names. Together they also
   wrap as a pair on a phone instead of one dropping a line on its own. */
.team-photoset-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

/* Same 44px target as the delete beside it: `btn-small` alone measures 39,
   and a control that happens to be a link is still a control (AGENTS.md). */
.team-photoset-dl {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  touch-action: manipulation;
}

.team-photo-strip {
  margin-block-start: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}
.team-photo { margin: 0; }
.team-photo img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
}
.team-photo figcaption {
  margin-block-start: 5px;
  font-size: 11px;
  color: var(--muted);
  text-transform: capitalize;
}

/* The client record, edited in place. Two columns on a laptop, one on a phone —
   the same auto-fit grid the rest of the dashboard uses. */
.team-field-grid {
  margin-block-start: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}
.team-field-grid label,
.team-details-form label {
  display: block;
  margin-block-end: 5px;
  font-size: 12px;
  font-weight: 500;
  color: var(--secondary);
}
.team-field-grid input,
.team-field-grid select { width: 100%; margin: 0; }
.team-details-form textarea { width: 100%; margin: 0; }
.team-details-form > .team-card > label { margin-block-start: 14px; }
/* Telegram is set by the client linking their own account, so it reads as a
   fact rather than as a field somebody forgot to fill in. */
.team-field-grid input[readonly] { background: var(--panel); color: var(--secondary); }

/*
 * The badge chooser on the pricing tab. It sits under the price matrix and is
 * saved by the same button, so it is spaced away from the table rather than
 * dropped straight against it — a control that looks like the last row of a
 * table gets read as one.
 */
/*
 * Deliver, with its "when does this start" question beside it.
 *
 * Inline rather than stacked: the question only appears when the client is
 * mid-block, and at that moment it and the button are one decision. Baseline
 * alignment so the select does not shove the button off the row's line.
 */
.team-deliver { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; min-width: 0; max-width: 100%; }
.team-deliver .btn { margin-block-start: 0; }
/*
 * THE "WHEN" SELECT HAD A FLOOR AND NO CEILING, and a floor wins: `min-width`
 * is applied after `max-width`, so 16rem meant 256px on a 350px pane whatever
 * else was in the row. It ran the action strip past the head at 320px in both
 * languages, and at 390px in English — before the pill was ever touched, and
 * found while measuring it (2026-09-11). A preference now, not a floor: 16rem
 * where there is 16rem, the row's width where there is not.
 */
/*
 * `margin: 0` because a caption-over-field column is a `<label>`, and the
 * global label rule gives every label 6px under it. The row aligns on the
 * controls' bottom edge, so that 6px put the select and the date box 6px ABOVE
 * Deliver, which had no label round it — one of the three bottoms the owner
 * saw (2026-10-01).
 */
.team-deliver-when { display: flex; flex-direction: column; gap: 4px; min-width: 0; max-width: 100%; margin: 0; }
/*
 * AS WIDE AS ITS LONGEST OPTION, and no wider than the row. A closed select's
 * own width is the widest option's, so `auto` is the whole sentence; the
 * 16rem preference this replaced cut "When their current block ends (date)"
 * at "e…" on a 780px row with 300px to spare (the owner's report, 2026-10-01).
 * Still a ceiling and no floor (F68): on a phone, where the longest option is
 * 3-42px wider than the whole row, the row is what it gets and the tail ends
 * in an ellipsis — the opened list shows every option whole.
 */
.team-deliver-when select { width: auto; min-width: 0; max-width: 100%; margin: 0; }
/* The date box and Deliver, which wrap as one — see the group's note in
   _team-builder-head.eta. nowrap is the point, as it is for `.bld-buttons`. */
.team-deliver-go { display: flex; flex-wrap: nowrap; gap: 10px; align-items: flex-end; min-width: 0; max-width: 100%; }
.team-deliver input[type="date"] {
  height: var(--control-h);
  padding-block: 0;
  font-size: 16px;
  line-height: 1.2;
  width: min(11rem, 100%);
  min-width: 0;
  margin: 0;
}
/* The build-time start control, inside Setup & framework: the deload row's
   44px labels, one per answer, and the date box under the last of them. */
.team-start-options { margin-block-start: 12px; display: flex; flex-direction: column; gap: 0 14px; }
.team-start-date { display: flex; flex-direction: column; gap: 4px; margin-inline-start: 26px; max-width: 100%; }
.team-start-date input { width: min(11rem, 100%); min-width: 0; font-size: 16px; margin: 0; }

.team-badge-field { margin-block-start: 18px; max-width: 30rem; }
.team-badge-field > label {
  display: block;
  margin-block-end: 5px;
  font-size: 12px;
  font-weight: 500;
  color: var(--secondary);
}
.team-badge-field select { width: 100%; max-width: none; margin: 0; }

.team-intake-group { margin-block-start: 14px; }
.team-intake-group:first-of-type { margin-block-start: 10px; }
.team-intake-title {
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
  padding-block-end: 6px;
  border-block-end: 1px solid var(--border);
}

/* ====================================================== gym mode: lobby
 *
 * A launcher, from the round-4 design. The old lobby led with an all-time
 * heaviest-lifts list — a trophy cabinet that barely changed, duplicated by the
 * records screen, and sitting where the client most needs to know what to do
 * next. It leads with the next session now.
 */

.page-gym-days { max-width: 640px; padding: 18px 16px 40px; }

.gym-block-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
  justify-content: space-between;
}
.gym-block-eyebrow {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}
.gym-block-focus { margin-block-start: 4px; font-size: 18px; font-weight: 700; }

/* ------------------------------------------------------------ resume card */

.gym-resume {
  margin-block-start: 16px;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 18px;
  background: linear-gradient(180deg, var(--gym-card-top), var(--surface));
}
.gym-resume-eyebrow {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}
.gym-resume-title {
  margin-block-start: 9px;
  font-size: 27px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.02em;
}
.gym-resume-meta { margin-block-start: 9px; font-size: 12.5px; color: var(--secondary); }
.gym-resume-go {
  margin-block-start: 20px;
  width: 100%;
  padding: 19px;
  border: 0;
  border-radius: 13px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 17px;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  display: block;
}
.gym-resume-go:hover { background: var(--accent-hover); }

/* A session already open reads differently from one not started — resuming and
   starting fresh must not look the same. */
.gym-resume.is-live {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--gym-live-top), var(--gym-live-bot));
}
.gym-live-head { display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }
.gym-live-pill {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 9px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .09em;
}
.gym-live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.gym-live-since { font-size: 12px; color: var(--gym-live-line); }
.gym-resume.is-live .gym-resume-meta { color: var(--gym-live-text); }
.gym-live-bar {
  margin-block-start: 12px;
  height: 6px;
  border-radius: 3px;
  background: var(--gym-live-track);
  overflow: hidden;
}
.gym-live-fill { height: 100%; border-radius: 3px; background: var(--accent); inline-size: var(--pct, 0%); }
.gym-restart {
  margin-block-start: 10px;
  width: 100%;
  padding: 13px;
  border: 1px solid var(--accent-border);
  border-radius: 11px;
  background: transparent;
  color: var(--gym-live-line);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* ------------------------------------------------------------- this week */

.gym-week-bar {
  margin-block-start: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  justify-content: space-between;
}
.gym-week-count { font-size: 15px; font-weight: 600; }
/* Deliberately not a streak: a streak punishes a planned rest day, and this
   app is not going to nag somebody for following their programme. */
.gym-week-sub { margin-block-start: 4px; font-size: 12px; color: var(--muted); }
.gym-pips { display: flex; gap: 7px; }
.gym-pip { width: 26px; height: 8px; border-radius: 4px; background: var(--gym-pip); }
.gym-pip.is-done { background: var(--accent); }

.gym-week-bar.is-full {
  border-color: var(--success-border);
  background: var(--gym-done-bg);
}
.gym-week-bar.is-full .gym-week-count { color: var(--gym-done-text); display: flex; align-items: center; gap: 8px; }
.gym-week-bar.is-full .gym-week-sub { color: var(--gym-done-sub); }
.gym-week-bar.is-full .gym-pip { background: var(--gym-done-pip); }
.gym-tick {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--success-border);
  color: var(--gym-done-text);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ------------------------------------------------------------- day list */

.gym-days-label {
  margin-block-start: 26px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.gym-day-list { margin-block-start: 10px; display: flex; flex-direction: column; gap: 8px; }
.gym-day-card {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 13px;
  background: var(--panel);
  color: var(--text);
  text-decoration: none;
}
/* The next session is already the resume card above, so its row only has to
   agree with it rather than compete. */
.gym-day-card.is-next { border-color: var(--accent); background: var(--gym-live-bot); }
.gym-day-num {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
  background: var(--surface-2);
  color: var(--muted);
}
.gym-day-card.is-done .gym-day-num { background: var(--success-bg); color: var(--success); }
.gym-day-card.is-next .gym-day-num { background: var(--accent); color: var(--accent-ink); }
.gym-day-body { flex: 1 1 auto; text-align: start; min-width: 0; }
.gym-day-focus { display: block; font-size: 14.5px; font-weight: 600; }
.gym-day-meta { display: block; margin-block-start: 3px; font-size: 11.5px; color: var(--muted); }
.gym-day-right {
  flex: none;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent);
}
.gym-day-card.is-done .gym-day-right { color: var(--success); }

/* ---------------------------------------------------------------- totals */

/*
 * THREE TILES THAT FIT THE WORDS IN THEM (owner, 2026-08-29).
 *
 * This was `repeat(auto-fit, minmax(92px, 1fr))`, which on a 360px phone is
 * three columns of 92px — 68px of content once the 12px padding is off. That
 * is not enough for what goes in them, and FARSI IS THE CASE TO DESIGN FOR:
 * "999کیلوگرم" measures 96px, and the number and the unit are welded together
 * with no space, so there is no wrap opportunity between them and the line
 * simply ran out of the tile. Reproduced at 360px and 390px in both languages
 * before anything was changed here; `.gym-total-k` is the same story one size
 * down — "ست‌های ثبت‌شده" is 75px in a 67px box.
 *
 * So the tiles REFLOW rather than being made to fit: two across on a phone
 * with the load tile — the one carrying a unit, and so always the longest —
 * taking the whole of the second row, and three across from 34rem, which is
 * where three of them have the width the words need. Nothing is truncated,
 * nothing is hidden, and no text got smaller: `overflow: hidden` here would
 * have hidden the bug rather than fixed it.
 */
.gym-totals {
  margin-block-start: 20px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 9px;
}
/* The load tile. Last in the markup, alone on its row, and the widest content
   in the strip — a five-digit volume with a unit welded to it. */
.gym-total:last-of-type { grid-column: 1 / -1; }

@media (min-width: 34rem) {
  .gym-totals { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .gym-total:last-of-type { grid-column: auto; }
}

.gym-total {
  /* A column, so the values line up across the row even when one label wraps
     to two lines and its neighbours do not. `min-width: 0` because a grid
     item's default `min-width: auto` is what lets long content push a track
     wider than its share and the page sideways with it. */
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--panel);
}
.gym-total-k { font-size: 12px; color: var(--muted); }
.gym-total-v {
  /* Pushed to the foot of the tile: see the column above. */
  margin-block-start: auto;
  padding-block-start: 3px;
  font-size: 17px;
  font-weight: 700;
  /* THE UNIT IS ITS OWN FLEX ITEM. `<bdi>999</bdi>کیلوگرم` has no whitespace
     in it on purpose — a space between a number and its unit is not how either
     language writes one — so as a line of text it cannot break at all. As flex
     items the digits and the unit can fall onto two lines when there is no
     room for one, which is a reflow rather than a clip, and they never break
     inside the unit's own word. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* The floor under all of it: a number long enough to have no break
     opportunity of its own still wraps rather than leaving the tile. */
  overflow-wrap: anywhere;
}

/*
 * The totals strip IS the way to the records screen.
 *
 * It is an <a> now, so it takes the link colours the rest of the page uses and
 * has to be told not to. The tiles inside keep their own look; what says
 * "this goes somewhere" is the row at the end — the name of the destination
 * and a chevron — rather than underlined text.
 */
a.gym-totals { text-decoration: none; color: inherit; }
a.gym-totals:hover .gym-total { border-color: var(--accent-border); }
a.gym-totals:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 11px; }
.gym-totals-go {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 7px;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
}
/* Drawn, not typed: an arrow character in an RTL line lands where the bidi
   algorithm puts it and still points at the English margin. */
.gym-totals-caret {
  width: 0;
  height: 0;
  border-inline-start: 5px solid currentColor;
  border-block-start: 4px solid transparent;
  border-block-end: 4px solid transparent;
}
[dir="rtl"] .gym-totals-caret { transform: rotate(180deg); }

/* ==================================================== gym mode: session
 *
 * From the round-4 design. Four shapes on one screen — a plain exercise, a
 * superset done as rounds, a drop set, and the four measures — because a
 * prescribed row is not always one exercise with one weight.
 *
 * Every input is 48px tall and every tap target is generous: this is a phone,
 * at arm's length, held in one hand, in a room full of people waiting for the
 * rack.
 */

.page-gym { max-width: 640px; padding: 0 0 90px; }

/* --------------------------------------------------------- sticky head */

.gym-head {
  position: sticky;
  inset-block-start: 0;
  z-index: 5;
  padding: 14px 18px;
  border-block-end: 1px solid var(--border);
  background: var(--panel);
}
.gym-head-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.gym-back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--secondary);
  text-decoration: none;
}
.gym-day-title { margin-block-start: 3px; font-size: 19px; font-weight: 700; }
.gym-count { text-align: end; }
.gym-count-k { font-size: 11.5px; color: var(--muted); }
.gym-count-v { font-size: 20px; font-weight: 700; }

.gym-progress {
  margin-block-start: 11px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}
/* The second half of the rule above: the transition, and the reduced-motion
   answer it never had. Same fraction, moved by transform. */
.gym-progress-bar { height: 100%; background: var(--accent); transition: transform .25s ease; }
@media (prefers-reduced-motion: reduce) {
  .gym-progress-bar { transition: none; }
}

.gym-units { margin-block-start: 11px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.gym-units-k { font-size: 11.5px; color: var(--muted); }
.gym-unit-tabs { display: flex; gap: 4px; padding: 3px; border-radius: 9px; background: var(--gym-bg); }
.gym-offline-pill {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 600;
}

/* -------------------------------------------------------------- cards */

.gym-body {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.gym-card {
  border: 1px solid var(--border-strong);
  border-radius: 15px;
  background: var(--surface);
  padding: 16px;
  /*
   * Audit F15. The round links in a superset jump to `#ex-N` and `.gym-head`
   * is sticky, so the exercise you asked for landed underneath it — on the one
   * screen where the thing you jumped to is the thing you are about to do.
   *
   * THE NUMBER IS A FLOOR, NOT A MEASUREMENT. The head is 14px of padding
   * either side of a title row, the progress bar and the unit tabs, which
   * computes to about 130px at the default type size and grows with the
   * reader's. Erring large costs a little empty space above the card; erring
   * small costs the top of it. It wants confirming on a device, and it is on
   * the audit's browser-pass list with the other rendered questions.
   */
  scroll-margin-block-start: 148px;
}
/* A skipped exercise stays legible — it is a decision, not an error, and the
   client may change their mind mid-session. */
.gym-card.is-skipped { opacity: .5; }
.gym-card.is-skipped .gym-sets,
.gym-card.is-skipped .gym-ss-body,
.gym-card.is-skipped .gym-drop-sets { display: none; }

.gym-card-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: flex-start;
  justify-content: space-between;
}
.gym-card-title { flex: 1 1 180px; min-width: 0; }
.gym-name-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.gym-name { font-size: 16.5px; font-weight: 600; }
.gym-card-meta {
  margin-block-start: 5px;
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  font-size: 12.5px;
  color: var(--secondary);
}
.gym-last { color: var(--muted); }
.gym-swapped { font-size: 11.5px; color: var(--muted); }
/* What "3 × 10-5-5" is actually asking for, read mid-set. Not another item in
   .gym-card-meta: that row is dot-separated figures and this is a sentence. */
.gym-scheme-key {
  /* Logical, not physical: this app renders RTL and the shorthand would pin
     the sides by page geometry rather than by reading order. */
  margin-block: 5px 0;
  margin-inline: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--secondary);
}

.gym-badge {
  padding: 3px 9px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* A Notes cell is the PROGRAM's words, and gym mode now renders the program's
   language rather than the reader's — so a Farsi note lands on an English page,
   where `html[dir="rtl"]` does not match and the page-level fix never fires.
   Letter-spacing pulls a cursive script apart; the badge drops it for its OWN
   direction instead of the document's. Same remedy .article-title makes one
   level up. A browser without :dir() ignores this rule and keeps today's
   rendering, which is why it stands alone rather than joining a selector list. */
.gym-badge:dir(rtl) { letter-spacing: normal; }

.gym-card-tools { display: flex; align-items: center; gap: 8px; }
/*
 * THE STAR SITS AT THE START OF THE HEAD (owner, 2026-09-28), before the
 * name, on the side the name starts from — left in English, right in Farsi,
 * because the head is laid out in logical order. It keeps its 44px box: the
 * glyph is 20px, and the button around it is the thumb's target, pulled back
 * by half its padding so the glyph still lines up with the card's edge.
 */
.gym-card-head > form.gym-star-form { display: flex; flex: none; margin: 0; margin-inline-start: -12px; margin-block-start: -11px; }
.gym-star {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: none;
  /*
   * UNCHOSEN IS AN OUTLINE YOU CAN SEE (owner, 2026-09-28). It was a filled
   * star in --border-strong, 1.3:1 on the card — invisible until it was
   * gold. Now an outline in --secondary (6.8:1 on --surface, past the 3:1 a
   * graphic needs) with a transparent fill, and the chosen star is filled
   * gold: shape AND colour differ, so the two states never rely on colour
   * alone. 26px glyph in the same 44px box. `aria-pressed` carries the state
   * for a screen reader.
   */
  color: transparent;
  -webkit-text-stroke: 1.5px var(--secondary);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
}
.gym-star.is-on { color: var(--accent); -webkit-text-stroke-color: var(--accent); }
.gym-star:hover { -webkit-text-stroke-color: var(--accent); }

/* A LIFT TARGET ON THE CARD (challenges v2): the 🎯 beside the name, and the
   line under the meta that says what it is and how close this block has come.
   Numbers tabular, so "80 × 5" and "75 × 5" line up as they change. */
.gym-target-mark { font-size: .95em; }
.gym-target {
  margin-block: 6px 0;
  margin-inline: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--secondary);
  font-variant-numeric: tabular-nums;
}
.gym-target.is-met { color: var(--text); font-weight: 600; }

/* ---------------------------------------------------------- the swap */

.gym-swap { position: relative; }
/*
 * ONE BOX FOR THE CARD'S PAIRED ACTIONS (owner, 2026-09-28): Swap and Ask
 * coach sit side by side and are the same kind of thing, so they are drawn by
 * ONE rule — same height, padding and type — rather than two rules that
 * agreed until one of them grew. They did not: Swap grew to 44px on a coarse
 * pointer and Ask coach stayed 34px, so the pair read as two sizes. 44px on
 * every pointer now, because this row is read and hit at arm's length.
 * A locked Ask coach (`.is-vip-locked`) only changes the edge and the label.
 */
.gym-swap-btn,
.gym-formcheck {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  box-sizing: border-box;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  touch-action: manipulation;
}
.gym-swap-btn { list-style: none; }
.gym-swap-btn:focus-visible,
.gym-formcheck:focus-visible,
.gym-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gym-swap-btn::-webkit-details-marker { display: none; }
.gym-swap[open] .gym-swap-btn {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}
.gym-swap-panel {
  margin-block-start: 12px;
  border: 1px solid var(--accent-border);
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
}
.gym-swap-head {
  padding: 11px 14px;
  border-block-end: 1px solid var(--border);
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
}
.gym-sub {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 14px;
  border: 0;
  border-block-end: 1px solid var(--team-hair, #1c212a);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  text-align: start;
  cursor: pointer;
}
.gym-sub:hover { background: var(--surface); }
.gym-sub-meta { font-size: 11.5px; font-weight: 400; color: var(--muted); }
/* The panel with nothing to offer, because every substitute is already a row
   on the day. Body text rather than the muted note: it is the answer to the
   question the client opened the panel to ask. */
.gym-swap-empty {
  margin: 0;
  padding: 12px 14px;
  border-block-end: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.6;
  color: var(--text);
}
.gym-swap-note { margin: 0; padding: 12px 14px; font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* A day being read back rather than trained. The sentence sits in the foot
   beside the control, so the state and the way out of it are one thing. */
.gym-foot-done {
  margin: 0;
  flex: 1 1 100%;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--muted);
}
/*
 * A readonly box is still focusable and still selectable — the client can read
 * and copy their numbers — but it must not look like somewhere to type.
 *
 * THE INK STAYS `--text`. The first version put the value in `--muted` as well,
 * which dimmed the one thing this whole state exists to show: a client opens a
 * finished day TO READ THOSE NUMBERS, and rendering them in the app's faintest
 * token on a recessed ground made them the hardest thing on the card. The
 * ground and the line carry "you cannot type here"; the number is data and
 * keeps its contrast.
 */
.gym-field input[readonly] {
  background: var(--surface-2);
  border-color: var(--border);
}

/* The way BACK from a swap. Same row height as the substitutes above it — it
   is the same kind of choice — but in the accent, because it is the one entry
   in the panel that undoes rather than does. */
.gym-unswap { color: var(--accent); }

/* Sets logged against a lift this row has since swapped away from. Read-only:
   nothing here is a control, because there is nothing to decide. */
.gym-orphans {
  margin-block-start: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
}
.gym-orphans-head { font-size: 12px; font-weight: 600; color: var(--muted); }
.gym-orphans-list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 6px;
}
.gym-orphans-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 14px;
}
/* An exercise name is user-supplied and can be long; without this the count
   beside it is pushed off the row rather than the name wrapping. */
.gym-orphans-list li > bdi { min-width: 0; overflow-wrap: anywhere; }
.gym-orphans-n { flex: none; font-size: 12px; color: var(--muted); }
.gym-orphans-note { margin: 8px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* ------------------------------------------------------------ fields */

.gym-field { display: flex; flex-direction: column; gap: 5px; flex: 0 0 74px; }
/*
 * WIDE ENOUGH FOR A SCHEME, not just for a number.
 *
 * The prescription is the placeholder here, and a myo set or a tempo row
 * writes it as "10-5-5" or "12-10-8" — seven characters in a 62px box with
 * 10px of padding each side, centred and (until the rule above) bold. It was
 * clipped, so the one thing the row is telling the client to do was the thing
 * they could not read. Reported by the owner, 2026-09-05.
 *
 * `flex: 0 1` rather than a bigger fixed basis alone: the box holds 84px where
 * the row has it and still SHRINKS on a narrow phone rather than pushing the
 * Log button off the end. It deliberately does not GROW — a reps box wider
 * than the weight box beside it reads as the more important of the two, and it
 * is not. The padding goes because the digits need the width more than the box
 * needs the air.
 */
.gym-field-reps { flex: 0 1 84px; }
.gym-field-reps input { padding-inline: 4px; }
.gym-field-added { flex-basis: 92px; }
/* The assisted box may GROW, unlike the others: it carries the minus sign
   inside it, and with script on the Log button is hidden and leaves the row
   room it would otherwise waste. Capped so it does not read as the row's
   main field on a wide screen. */
.gym-field-assist { flex: 1 1 116px; max-width: 168px; }
.gym-field-wide { flex: 1 1 120px; }
.gym-field-k {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.gym-field input {
  width: 100%;
  height: 48px;
  margin: 0;
  padding: 0 10px;
  text-align: center;
  border: 1px solid var(--gym-input-line);
  border-radius: 10px;
  background: var(--panel);
  color: var(--text);
  /*
   * 16px, and this is the one that matters most in the app: this box is tapped
   * between sets, one-handed, with wet hands. At 15px iOS zoomed the viewport
   * on every focus and left it zoomed, so the Log button went off-screen and
   * the client pinched back out for the next set. The box is 48px tall — 16px
   * simply fits, and .gym-field-wide beside it has been 17px all along.
   */
  font-size: 16px;
  font-weight: 700;
}
.gym-field-wide input { text-align: start; font-size: 17px; }

/*
 * A NUMBER NOBODY TYPED MUST NOT LOOK LIKE ONE THEY DID.
 *
 * Every prefill in a set row is a PLACEHOLDER, deliberately — the prescription,
 * the weight carried down from the set above, the drop suggestion. But the box
 * itself is `font-weight: 700` and placeholders inherit it, so all three
 * rendered in the same bold face as a figure the client had entered. The owner:
 * *"i want the autofilled ones to be grayer... so that its more obvious that
 * they're pre filled."*
 *
 * `--muted` and not dimmer: the 2026-08-28 audit names that token as the first
 * place to point an APCA pass, and a number a client reads at a rack is the
 * last place to go below it. The weight drop from 700 to 500 does most of the
 * work, and it buys the reps box some room as well.
 *
 * `opacity: 1` because Firefox dims placeholders on top of the colour, which
 * would put these under the token after all.
 */
.gym-field input::placeholder {
  color: var(--muted);
  font-weight: 500;
  opacity: 1;
}
/*
 * Carried or suggested rather than prescribed. `is-carried` is set by gym.js
 * (carryForward and suggestDrops) and had NO rule anywhere — the class was
 * being written and nothing read it. It reads as one step lighter still,
 * because this number is the app's own arithmetic and not the coach's.
 */
.gym-field input.is-carried::placeholder { font-weight: 400; }

/* Assistance is a negative: the sign is part of the control, not something the
   client types, because less help is better and a minus they might omit would
   invert the meaning. */
.gym-assist-box {
  display: flex;
  align-items: center;
  height: 48px;
  border: 1px solid var(--gym-input-line);
  border-radius: 10px;
  background: var(--panel);
}
.gym-assist-box input {
  /*
   * THE INPUT TAKES WHAT THE SIGN LEAVES. It was `width: 100%` from
   * `.gym-field input` inside a flex row that also held the sign and the unit
   * word, so it was shrunk to whatever was over — 20px at 390 in Farsi, where
   * «کیلوگرم» is wide. The unit moved to the label; `flex: 1 1 0` with
   * `min-width: 0` hands the input the rest of the box at any width.
   */
  flex: 1 1 0;
  /* 0, not auto: an auto width let the input's natural size set the field's
     minimum, and with script off the row ran 14px past the card at 360
     (QA of #230). flex-grow still hands it the rest of the box. */
  width: 0;
  min-width: 0;
  height: 100%;
  padding-inline: 4px 10px;
  border: 0;
  background: transparent;
  border-radius: 0;
}
/*
 * THE RING GOES ON THE BOX, which is what looks like the field. The global
 * `input:focus` ring drew around the bare input inside it — a rectangle
 * floating within a rounded box. Transparent rather than removed, so a forced-
 * colours mode that paints outlines still shows one on the input itself.
 */
.gym-assist-box input:focus,
.gym-assist-box input:focus-visible { outline: 2px solid transparent; }
.gym-assist-box:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent-border);
}
/* The unit switch waits for a set still saving before it swaps (swap.js), up
   to 4s on a bad connection. The tabs dim for that moment, so a press that has
   not visibly done anything yet is not pressed again. No transition: nothing
   to honour or skip under reduced motion. */
[aria-busy="true"] .gym-unit-tabs { opacity: .6; }
.gym-assist-sign { flex: none; padding-inline: 12px 0; font-size: 15px; font-weight: 700; color: var(--muted); }

/* ------------------------------------------------------ plain sets */

.gym-sets { margin-block-start: 13px; display: flex; flex-direction: column; gap: 10px; }
.gym-set-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 9px; }
.gym-set-label {
  flex: none;
  width: 52px;
  align-self: center;
  padding-block-start: 14px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.gym-set-label.is-done { color: var(--success); }
.gym-log-set {
  flex: 1 1 90px;
  height: 48px;
  border: 1px solid var(--border-strong);
  border-radius: 11px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.gym-log-set:hover { border-color: var(--accent-border); }
.gym-log-set.is-done {
  border-color: var(--success-border);
  background: var(--success-bg);
  color: var(--success);
}
.gym-hint { margin-block-start: 9px; font-size: 11.5px; line-height: 1.5; color: var(--muted); }

/* -------------------------------------------------------- superset */

/*
 * A SUPERSET'S TWO LIFTS, AND THE BREAK BETWEEN THEM.
 *
 * This was a plain block, so the line wrapped at whatever space happened to
 * land at the edge — including a space INSIDE one of the names. The owner,
 * 2026-09-06, on «فیس پول سیم‌کش + پول اور سیم‌کش»: the «کش» of the first lift
 * sat alone on the next line, which reads as a third exercise rather than as
 * the tail of the first.
 *
 * The break belongs at the `+`, which is the only place in this string where a
 * new thing starts. So the row is a wrapping flex whose items are the two
 * names, each unbreakable — the same reason a unit or a brand name is kept
 * whole. Worst case a name longer than the card overflows its own line rather
 * than the card: `min-width: 0` lets the item shrink, and the ellipsis is
 * better than a name split across two lines in the wrong direction.
 *
 * `align-items: baseline` so the `+` sits on the same line as the words in
 * both scripts, and `row-gap` so a wrapped pair does not touch.
 */
.gym-ss-name {
  margin-block-start: 9px;
  font-size: 15.5px;
  font-weight: 600;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  row-gap: 2px;
}
/*
 * The names WRAP; they do not truncate.
 *
 * The first version of this rule pinned each name `nowrap` with an ellipsis,
 * which bought nothing the flex boundary was not already buying and cost two
 * things: `overflow: hidden` makes an item a scroll container, whose baseline
 * is synthesized from its border box — so `align-items: baseline` above
 * silently stopped aligning the names to the `+`, which is the one thing that
 * property is here for. And a lift name is not decoration to be cut off: this
 * is the line the client reads to know what they are about to do.
 *
 * So a name longer than the card wraps inside itself, which is the ORIGINAL
 * defect — but only when it cannot fit at all, rather than at the first
 * convenient space. That is the right trade, and it is the fallback rather
 * than the common case.
 */
/*
 * THE `+` TRAVELS WITH THE NAME AFTER IT.
 *
 * As three bare flex items — name, `+`, name — a wrap could land AFTER the
 * operator, leaving a `+` alone at the end of a line (at the left edge in
 * RTL) with the second lift starting the next. That is the same defect as the
 * stranded «کش» this whole rule exists to fix, one level up. Grouping each
 * lift with its leading operator means the only place a break can fall is
 * BEFORE a `+`, where a new thing genuinely starts.
 */
.gym-ss-lift { display: inline-flex; align-items: baseline; min-width: 0; }
.gym-ss-name bdi { overflow-wrap: break-word; }
.gym-plus { color: var(--accent); padding-inline: 4px; }

.gym-rounds { margin-block-start: 13px; display: flex; gap: 7px; }
.gym-round {
  flex: 1;
  padding: 9px 0;
  text-align: center;
  border: 1px solid var(--border-strong);
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
}
.gym-round.is-done { color: var(--success); }
.gym-round.is-current {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}

.gym-ss-body {
  margin-block-start: 12px;
  padding: 13px;
  border: 1px solid var(--border-strong);
  /* The gold edge is what says "these belong together" — a superset is one
     unit of work, not two exercises that happen to be adjacent. */
  border-inline-start: 3px solid var(--accent);
  border-radius: 12px;
  background: var(--panel);
}
.gym-ss-part { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 9px; }
.gym-ss-letter {
  flex: none;
  width: 24px;
  height: 24px;
  border: 1px solid var(--accent-border);
  border-radius: 7px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gym-ss-who { flex: 1 1 150px; min-width: 130px; }
.gym-ss-part-name { font-size: 14px; font-weight: 600; }
.gym-ss-part-meta { margin-block-start: 3px; font-size: 11.5px; color: var(--muted); }
.gym-ss-fields { flex: 0 0 auto; display: flex; gap: 9px; align-items: flex-end; }

.gym-ss-join { display: flex; align-items: center; gap: 9px; margin-block-start: 10px; }
.gym-ss-arrow { flex: none; width: 24px; text-align: center; font-size: 11px; color: var(--muted); }
.gym-ss-rule { flex: 1; height: 1px; background: var(--gym-pip); }
.gym-ss-into {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.gym-ss-part + .gym-ss-join + .gym-ss-part { margin-block-start: 12px; }
.gym-ss-join + .gym-ss-part { margin-block-start: 12px; }

.gym-log-round {
  width: 100%;
  margin-block-start: 13px;
  padding: 16px;
  border: 0;
  border-radius: 12px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.gym-log-round:hover { background: var(--accent-hover); }

/* -------------------------------------------------------- drop set */

.gym-drop-sets { margin-block-start: 13px; display: flex; flex-direction: column; gap: 10px; }
.gym-drop-set {
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--panel);
  padding: 13px;
}
/* The set being worked on right now. */
.gym-drop-set.is-now { border-color: var(--accent); background: var(--surface); }
.gym-drop-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.gym-drop-n { font-size: 13px; font-weight: 700; }
.gym-drop-now {
  padding: 3px 9px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .09em;
}

/* The entries are bracketed by a gold edge because they are ONE set — three
   loose rows would read as three sets, which is the whole confusion to avoid. */
.gym-drop-list {
  margin-block-start: 9px;
  border-inline-start: 2px solid var(--accent);
  padding-inline-start: 13px;
}
.gym-drop-row { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; }
.gym-drop-k {
  flex: none;
  width: 52px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.gym-drop-v { font-size: 15px; font-weight: 700; }
.gym-drop-v.is-drop { font-size: 13.5px; font-weight: 600; color: var(--nav-link); }
.gym-drop-hint { margin: 11px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); }

.gym-add-drop-form { margin-block-start: 10px; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 9px; }
.gym-add-drop {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 48px;
  padding: 0 15px;
  border: 1px dashed var(--accent-border);
  border-radius: 11px;
  background: var(--gym-live-bot);
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* ----------------------------------------------------------- footer */

/*
 * The personal best.
 *
 * ALWAYS IN THE DOM, hidden by `display: none` rather than by the `hidden`
 * attribute, because `hidden` IS `display: none` and a display change cannot be
 * transitioned — which is the trap the research brief names. gym.js adds
 * `is-shown`; the movement lives here.
 *
 * (There is a second, identical `.gym-best` block earlier in this file. This
 * one is later and therefore the one that wins; the duplicate is left for the
 * design pass rather than removed in a branch about something else.)
 */
.gym-best {
  display: none;
  margin-block-start: 10px;
  padding: 8px 11px;
  border: 1px solid var(--success-border);
  border-radius: 8px;
  background: var(--success-bg);
  color: var(--success);
  font-size: 11.5px;
  font-weight: 600;
}
.gym-best.is-shown { display: block; }
.gym-best-line { display: block; }
/*
 * The second line is the warmth, and it is a FACT stated plainly — "heavier
 * than you have ever gone here" — never an adjective about the person. Dimmer
 * than the line above it so the badge still reads as one thing.
 */
.gym-best-note {
  display: block;
  margin-block-start: 2px;
  font-weight: 500;
  font-size: 11px;
  opacity: .85;
}

/*
 * THE ONE MOVEMENT IN THE APP, and it is deliberately the only one.
 *
 * This interface is otherwise motionless (thirteen transitions in seven
 * thousand lines), and that stillness is part of its character. One thing
 * moving, once, where nothing ever moves is enough — so the badge arrives
 * rather than appearing, and nothing else in this slice moves at all.
 *
 * transform and opacity only, 180ms, one beat, no loop: compositor-friendly per
 * AGENTS.md, and well inside WCAG 2.2.2's five seconds. Under
 * prefers-reduced-motion the rule simply does not exist and the badge is
 * present the instant the class lands — the reduced variant is the plain fact,
 * which is the whole content anyway.
 */
@media (prefers-reduced-motion: no-preference) {
  .gym-best.is-shown { animation: gym-best-in 180ms ease-out both; }
}
@keyframes gym-best-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.gym-skip-form { margin-block-start: 10px; }
.gym-skip {
  padding: 4px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
}
.gym-skip:hover { color: var(--text); border-color: var(--accent-border); }

.gym-foot {
  position: sticky;
  inset-block-end: 0;
  display: flex;
  gap: 10px;
  padding: 14px 16px;
  border-block-start: 1px solid var(--border);
  background: color-mix(in srgb, var(--panel) 96%, transparent);
  backdrop-filter: blur(8px);
}
.gym-foot-form { flex: 1; display: flex; }
.gym-foot-leave {
  flex: 1;
  padding: 14px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 14.5px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}
.gym-finish {
  flex: 1;
  padding: 14px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 14.5px;
  font-weight: 700;
  cursor: pointer;
}
.gym-finish:hover { background: var(--accent-hover); }

/* Gym mode's fields are <label> elements, so they inherit the global label
   margins (1rem top, .35rem bottom) and `form > label:not(:first-child)`'s
   15px — which stacked to about 80px of dead space per set row and made the
   session screen look broken. The controls here are laid out by flex gap, so
   every inherited margin is reset rather than fought.

   Same lesson as the records screen: a designed control that happens to reuse
   a semantic element inherits that element's app-wide styling, and the design's
   own spacing has to be reasserted. */
.gym-set-form,
.gym-add-drop-form,
.gym-ss-body,
.gym-card form { margin: 0; }

.gym-field,
form > .gym-field:not(:first-child) { margin: 0; }

.gym-field input,
.gym-assist-box input { margin: 0; }

/* One set is ONE row.
 *
 * With flex-wrap the Log button wrapped to a second line on a phone even
 * though the four controls fit the container exactly — the basis widths add up
 * to the content width and the rounding tips it over. The result was 126px per
 * set instead of 69px, which on a four-set exercise is 228px of dead space and
 * is what made the screen look broken.
 *
 * nowrap, and the fields may shrink instead. They stay comfortably tappable
 * down to a 320px screen because the label and the button are the only fixed
 * costs. */
.gym-set-form,
.gym-add-drop-form { flex-wrap: nowrap; }

.gym-set-form .gym-field,
.gym-add-drop-form .gym-field { min-width: 0; }

/* The superset's per-part row keeps wrapping: its name column is elastic, so
   on a narrow screen the two inputs drop under the name rather than crushing
   it, which is the behaviour the design draws. */
.gym-ss-part { flex-wrap: wrap; }

/* The A/B letter belongs beside the exercise NAME.
 *
 * The row is align-items:flex-end so the inputs' bottoms line up, which pinned
 * the 24px letter to the bottom of a two-line name block — it appeared next to
 * "target ×15" instead of next to the exercise. */
.gym-ss-letter { align-self: flex-start; margin-block-start: 2px; }

/* The card's meta reads as one sentence, so the separators are drawn by CSS
   rather than typed between the parts — a literal "·" in the markup ended up
   orphaned at the start of a wrapped line. */
.gym-card-meta { gap: 0 9px; }
.gym-card-meta > * + *::before {
  content: "·";
  margin-inline-end: 9px;
  color: var(--muted);
}

/* The gym session's sticky bars are full-bleed, but their CONTENT follows the
   640px column the rest of the screen uses. Left unconstrained the two footer
   buttons stretched the width of a desktop window, which is not what the design
   draws and reads as a mistake. */
.gym-head > *,
.gym-foot { max-width: 640px; margin-inline: auto; }
.gym-foot { width: 100%; }

/* What the last generation run did. */
.team-notice {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-block-start: 14px;
  padding: 11px 14px;
  border-radius: 10px;
  font-size: 12.5px;
  font-weight: 600;
}
.team-notice.is-running {
  border: 1px solid var(--accent-border);
  background: var(--accent-bg);
  color: var(--accent);
}
.team-notice.is-ok {
  border: 1px solid var(--success-border);
  background: var(--success-bg);
  color: var(--success);
}
.team-run-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: team-pulse 1.4s ease-in-out infinite;
}
@keyframes team-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
/* A generation takes a minute or two, and a pulsing dot is the only motion on
   this screen — respect a reader who has asked for none. */
@media (prefers-reduced-motion: reduce) { .team-run-dot { animation: none; } }

/* The log button grows to fill the row, which is right on a 390px phone — the
   design's frame — and wrong at the 640px column, where it stretched to 359px
   and read as a bar rather than a button. It still reaches the full thumb width
   on a phone because the cap is above what a phone row leaves it. */
.gym-log-set { max-width: 180px; }
.gym-add-drop { max-width: 220px; }

/* ------------------------------------------- dashboard: acting from the list
 *
 * The design's answer to click economy. A routine row carries its action; a row
 * that needs judgement carries the reason to open it instead. That asymmetry is
 * the point — see rowActionFor in routes/team.js.
 */

.team-item-wrap {
  border-block-end: 1px solid var(--team-hair, #1c212a);
  background: transparent;
}
.team-item-wrap.is-current { background: var(--surface); }
.team-item-wrap.is-vip { border-inline-start: 2px solid var(--accent); }
.team-item-main { display: flex; align-items: flex-start; gap: 10px; }
.team-item-wrap .team-item { flex: 1 1 auto; min-width: 0; border-block-end: 0; background: transparent; }

.team-check {
  flex: none;
  margin: 14px 0 0 15px;
  margin-inline-start: 15px;
  margin-inline-end: 0;
}
.team-check input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
}

.team-row-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  align-items: center;
  padding: 0 15px 12px;
}
.team-row-act {
  padding: 7px 13px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.team-row-act:hover { background: var(--accent-hover); }
/* The reason to open, not a second action — it must not compete with the
   primary button on rows that have one. */
.team-row-open {
  padding: 7px 11px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
}
.team-row-open:hover { color: var(--text); border-color: var(--accent-border); }

.team-select-toggle {
  display: block;
  padding: 9px 15px;
  border-block-end: 1px solid var(--team-hair, #1c212a);
  font-size: 12px;
  font-weight: 600;
  color: var(--secondary);
  text-decoration: none;
}
.team-select-toggle:hover { color: var(--accent); }

.team-bulk-bar {
  position: sticky;
  inset-block-end: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 15px;
  border-block-start: 1px solid var(--border);
  background: var(--panel);
}
.team-bulk-go {
  flex: 1 1 auto;
  padding: 9px 13px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.team-bulk-clear {
  padding: 9px 11px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
}

/* A finished queue should feel finished rather than broken. */
.team-empty-tick {
  width: 76px;
  height: 76px;
  border: 1px solid var(--success-border);
  border-radius: 50%;
  background: var(--gym-done-bg);
  color: var(--gym-done-pip);
  font-size: 30px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-block-end: 16px;
}

/* ------------------------------- review: the whole program in the pane */

.team-pane-error { margin-block-end: 14px; }
.team-sub-eyebrow { margin-block-start: 16px; }

/*
 * THE INDENT IS ON THE <li>, NOT THE <ul>, and that is the whole point: every
 * item here is `dir="auto"`, so a note resolves its OWN direction from the
 * words in it and its marker is drawn on the side that direction starts. The
 * indent used to be `padding-inline-start` on the list, which resolves from
 * the PANE's direction — so an English standing note read on /fa put its
 * bullet on the left while the 18px of room for it sat on the right. Measured
 * at 1440 on the review pane: 15px of marker outside the list's own box, and
 * the bullets touching the card's inner edge with none of the 18px this rule
 * asks for. A margin on the item follows the item.
 */
.team-notes-list {
  margin: 10px 0 0;
  padding-inline-start: 0;
  font-size: 13px;
  line-height: 1.75;
  color: var(--nav-link);
}
.team-notes-list > li { margin-inline-start: 18px; }

/* The macro targets: four figures a coach checks at a glance before reading
   the meal plan under them. */
.team-macros {
  margin-block-start: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px;
}
.team-macro {
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.team-macro-k { font-size: 11px; color: var(--muted); }
.team-macro-v { margin-block-start: 3px; font-size: 15px; font-weight: 700; }

.team-meals { margin-block-start: 14px; }
.team-meal-part { flex: 1 1 8rem; font-size: 12.5px; color: var(--secondary); }

/* Two tables in a row — the meal plan under the nutrition block, mainly. */
.team-table + .team-table { margin-block-start: 14px; }

/* The flagged-for-review chip is a LINK now — it points at the generation
   summary, where the model's note actually lives. */
a.team-chip { text-decoration: none; }
a.team-chip:hover { border-color: var(--accent-border); }

/* --------------------------- generate: tabs, flags, and program summaries */

/* The screening count rides on the Intake tab, so a coach cannot press
   Generate without having been told there is something to read. */
.team-sub-count {
  margin-inline-start: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--danger-bg);
  color: var(--danger);
  font-size: 10.5px;
  font-weight: 700;
}

/* A screening answer that is not "no". Framework §10 turns one into a
   substitution, so it is the expensive thing to skim past. */
.team-flag-list > .is-flagged .team-flag-k { color: var(--danger); }
.team-flag-list > .is-flagged { border-inline-start: 2px solid var(--danger-border); padding-inline-start: 9px; }

.team-prog-card + .team-prog-card { margin-block-start: 12px; }
.team-prog-when { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 9px; font-size: 13px; font-weight: 600; }
.team-prog-name { font-size: 13.5px; font-weight: 700; color: var(--text); }
.team-prog-rev { font-size: 11.5px; font-weight: 500; color: var(--muted); }
.team-prog-tags { display: flex; gap: 6px; }
.team-tier.is-sent { border-color: var(--success-border); background: var(--success-bg); color: var(--success); }

/* THE STAGE CHIP (lib/program-stage.js, slice five of the builder pane).
   Sent reads as the existing success chip; a run in flight and a program
   awaiting delivery read as the accent, because both are "something is about
   to happen"; draft, generated, edited and superseded are the plain chip —
   the ordinary states, not news — in the chip's own text colour, which is
   the one already measured for contrast. */
.team-tier.is-stage-sent { border-color: var(--success-border); background: var(--success-bg); color: var(--success); }
.team-tier.is-stage-generating,
.team-tier.is-stage-awaiting { border-color: var(--accent-border); background: var(--accent-bg); color: var(--accent); }

.team-prog-facts {
  margin-block-start: 9px;
  display: flex;
  flex-wrap: wrap;
  gap: 0 9px;
  font-size: 12.5px;
  color: var(--secondary);
}
.team-prog-facts > * + *::before { content: "·"; margin-inline-end: 9px; color: var(--muted); }

.team-prog-focus { margin-block-start: 6px; font-size: 12.5px; color: var(--nav-link); }
.team-prog-chips { margin-block-start: 9px; }
.team-prog-nutrition {
  margin-block-start: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 0 9px;
  align-items: baseline;
  font-size: 12px;
  color: var(--secondary);
}
.team-prog-nutrition > * + *::before { content: "·"; margin-inline-end: 9px; color: var(--muted); }

/* Another run, from inside the review. At the foot of the whole pane, because
   asking for one is what you decide AFTER reading the program — not a thing to
   trip over on the way in. */
.team-regen-box { margin-block-start: 20px; }
.team-regen-box .freetext { margin-block-start: 8px; }

/*
 * WHAT THE LAST RUN WAS ASKED, quoted above an amendment box that now opens
 * empty (the owner's answer to the slice-ten design pass, question 5,
 * 2026-09-22). A quotation, not a field: an inline-start rule rather than a
 * box, so it cannot be mistaken for the textarea under it. `pre-wrap` because
 * a coach's instructions are paragraphs and the line breaks are theirs.
 */
.team-regen-last { margin-block: 2px 14px; }
.team-regen-quote {
  margin: 6px 0 0;
  padding-inline-start: 12px;
  border-inline-start: 2px solid var(--border-strong);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  /* About six lines, then it scrolls: a paragraph-long instruction grew the
     box past its desk column (review of #173). */
  max-height: 10rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.team-regen-quote:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A text button, and a 44px one: it sits in a form a coach fills in on a
   phone. `[hidden]` restated because `display` here would otherwise beat the
   attribute, and the button is hidden until review.js can make it work. */
.team-regen-reuse {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
}
.team-regen-reuse[hidden] { display: none; }
.team-regen-reuse:hover { color: var(--text); }
.team-regen-reuse:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The previous block's days, above its markdown — the same section head and
   row classes the Program tab draws, spaced the way consecutive days are. */
.team-prev-day { margin-block-start: 18px; }
.team-prev-table { margin-block-start: 10px; }
/* A long note WRAPS here. `.team-row-note` is `flex: none` for the Program
   tab's short notes, and inside `.team-table`'s `overflow: hidden` a 150-
   character note became an 852px line clipped at the table's edge (review of
   #173). A delivered block's notes are whatever the coach wrote. */
.team-prev-table .team-row-note { flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; }

/* A TAB PRESS LANDS ON THE TABS (question 6): the sub-view links carry
   `#views`, and this keeps a sliver of the pane above the row so it reads as
   the top of something rather than as cut off. The proposal's own number. */
#views { scroll-margin-block-start: 12px; }

/*
 * AT DESK WIDTH, THE AMENDMENT BOX BESIDE THE READING RATHER THAN UNDER IT
 * (the owner's answer to the slice-ten design pass, question 4, 2026-09-22).
 *
 * The owner's sentence the box exists for is "i have to switch back and forth
 * a lot while writing instructions". Under the check-in, the box and the
 * answers it is written from were never on one screen. Here they are two
 * columns, and the box is sticky so it stays beside whatever part of the
 * reading is in view.
 *
 * ONLY ON THE READ-ONLY VIEWS. On Program the builder is the reading, it is
 * the widest thing on the pane, and a 22rem column taken off it would buy
 * nothing — so the grid applies only while the builder is hidden. `:has()`
 * reads that from the `hidden` attribute review.js toggles on a swap, so the
 * layout follows a swapped tab with no script of its own. A pane with no box
 * (a hand-written program has no request) has nothing to put in the second
 * column and keeps one.
 *
 * THE WRAPPER IS NOT THE SWAP REGION. `.rev-views` inside it is, and the box
 * stays outside that (decisions, 2026-08-30 and 2026-09-04), so a draft still
 * survives a tab switch by construction.
 *
 * FROM 1400px, NOT THE PROPOSAL'S 1100. The pane is what is left of the
 * window after the rail and the queue (viewport − 460px), so at 1100 it is
 * 640px wide and a 22rem box beside the check-in would have left the check-in
 * 230px. Measured in Chromium with the box in place: the reading column is
 * 530px at 1400, 570px at 1440 and 730px at 1600. Below 1400 the box stays
 * where it was, collapsed at the foot.
 *
 * STICKY NEEDS ITS SCROLLER TO BE THE ONE THAT SCROLLS. `.team-detail` is
 * `overflow-y: auto` with no bounded height, so it was the box's scroll
 * container while the DOCUMENT did the scrolling — the box never stuck
 * (measured at 1440x900: its top went 536 → −30; review of #173). On the
 * read-only views, where this grid applies, the pane stops being a scroll
 * container: `overflow-y: visible`, and `overflow-x: clip` rather than
 * `hidden` because `hidden` would make it one again. Not on Program, where
 * the builder is showing and the pane keeps its own overflow.
 *
 * No max-height on the box and no scroller of its own: the open box (about
 * 645px) fits a desk viewport, and a scroller nested around a textarea is two
 * scrollbars for one gesture.
 */
@media (min-width: 1400px) {
  .rev-withbox:has(> .team-regen-box):not(:has(> [data-builder]:not([hidden]))) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22rem;
    column-gap: 18px;
    align-items: start;
  }
  .rev-withbox:has(> .team-regen-box):not(:has(> [data-builder]:not([hidden]))) > .rev-views {
    grid-column: 1;
    grid-row: 1;
  }
  .rev-withbox:has(> .team-regen-box):not(:has(> [data-builder]:not([hidden]))) > .team-regen-box {
    grid-column: 2;
    grid-row: 1;
    position: sticky;
    inset-block-start: 14px;
    margin-block-start: 14px;
  }
  .team-detail:has(.rev-withbox > .team-regen-box):has(.rev-withbox > [data-builder][hidden]) {
    overflow-x: clip;
    overflow-y: visible;
  }
}

/* ------------------------------------------------- review: days and editor
 *
 * The design's own shape: a tab per day, plus Nutrition. Restored after a spell
 * of stacking every day down the page — which read fine but lost the one thing
 * tabs give you, which is knowing WHICH day has the problem before you open it.
 */

/* The red count on a day's tab. Same idea as the intake tab's screening count,
   but per day, so "2 names unmatched" says where. */
.team-day-count {
  margin-inline-start: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--danger-bg);
  color: var(--danger);
  font-size: 10.5px;
  font-weight: 700;
}

/* A quiet marker that the model left a note, so the tab carries the fact
   without carrying a number that would imply a count. */
.team-sub-dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-inline-start: 6px;
  border-radius: 50%;
  background: var(--danger);
  vertical-align: middle;
}

/*
 * The program and its markdown, side by side.
 *
 * The markdown used to be an edit MODE: a button swapped the program out for a
 * textarea, so you could read it or change it, never both. Editing a program is
 * reading it and disagreeing, which wants one screen.
 *
 * Two columns, and the editor is the narrower one — it is a working surface,
 * not the thing being reviewed. Below the breakpoint they stack, editor last.
 */
.rev-split {
  margin-block-start: 12px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  align-items: start;
}
/*
 * ONE COLUMN AT EVERY WIDTH (2026-09-06, the owner's call).
 *
 * It was 1.35fr for the program and 1fr for the raw markdown above 1100px —
 * the markdown "beside the program rather than instead of it", from slice
 * seven, when the markdown WAS the editor and the table beside it was a
 * preview. That reversed in slice seven itself: the table is the editor now
 * and the markdown is the fallback, folded away on load by review.js.
 *
 * The cost was measured at 1440px before changing it: the split is 940px and
 * the program column got 531 of them, so a standing note needed 514px in a
 * 439px box and a superset name 357 in 327. The ends of those lines could not
 * be reached — the owner, 2026-09-06: "the end of the line can't be scrolled
 * to or shown… what if we have its width be the width of the entire section
 * and put the markdown somewhere else?"
 *
 * So: the program takes the section, and the markdown keeps its own place
 * BELOW it, inside the disclosure it was already in. Nothing is hidden that
 * was not already folded, and with script off the fold is open and the
 * textarea is still the editor — which is the whole reason it stays on the
 * page rather than moving to a tab.
 */
.rev-split { grid-template-columns: minmax(0, 1fr); }
/*
 * One rhythm down the document column.
 *
 * The blocks in here carry mismatched margins of their own — .team-table 12px,
 * .team-card none — so the standing notes sat welded to the bottom of the day
 * table. Same root cause as the sub-nav: `.team-card + .team-card` spaces cards
 * from each other, and a card following a TABLE has nothing.
 *
 * Stated once for the column rather than patched per pair, and it applies to
 * the live preview too — review.js appends the same blocks in the same order.
 */
.rev-doc { min-width: 0; }
.rev-doc > * + * { margin-block-start: 14px; }

/* While there are unsaved edits the left column is a PREVIEW of text that is
   not on file yet. The chip says so; this makes it visible at a glance, so a
   coach cannot mistake a live preview for the delivered program. */
.is-dirty .rev-doc {
  border-inline-start: 2px solid var(--accent-border);
  padding-inline-start: 12px;
}

.rev-edit { min-width: 0; }

/* ------------------------------------------------ the table as an editor
 *
 * Slice seven of the builder pane (2026-09-03). The rows the server draws
 * read-only become inputs once review.js runs; the classes below dress the
 * inputs to sit in the same .team-row the read-only cells did, so the two
 * renderings are the same table with the fields lit. Inputs are 16px on a
 * phone (iOS zooms anything smaller) and the row's own size on a desk. */
.bld-help { margin-block-end: 8px; }
.bld-head { flex-wrap: wrap; gap: 6px 10px; align-items: center; }
.bld-in {
  min-height: 40px;
  padding: 6px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  min-width: 0;
}
.bld-in::placeholder { color: var(--muted); }
/*
 * THE CELL DECIDES THE WIDTH, NOT THE INPUT.
 *
 * `input[type="text"] { max-width: 32rem }` is an ATTRIBUTE selector, so its
 * specificity is (0,1,1) — it beats any single class, `.bld-focus` and
 * `.bld-name` included. Every field in this table was silently capped at
 * 512px by a rule written for a form page, and the day's focus field sat 172px
 * wider than the value this file asked for with nothing on screen to say why.
 * The grid track is the width here; this is what hands it back.
 */
.bld-in { max-width: none; }
.bld-in.bld-focus { flex: 1 1 160px; min-width: 140px; max-width: 340px; }

/* ------------------------------------------------------- A REAL TABLE
 *
 * The round-4 design pass (Claude Design's canvas, docs/design/canvas). The
 * row was a wrapping FLEX line of four fields plus five buttons, and at any
 * width narrower than about 1100px it wrapped — so the movers sat on a second
 * line under the fields they move, nothing lined up down the table, and the
 * lock and the order buttons were in columns the header did not name.
 *
 * It is a GRID now, six columns wide, in the canvas's own order: HOLD at the
 * start, EXERCISE, SETS, REPS, NOTE, ORDER at the end. Every column has a
 * heading, and every row's cell is in the same track as the heading above it,
 * which is what makes it read as a table rather than as a line of controls.
 *
 * The tracks are the canvas's, converted from its fixed pixel widths to ones
 * that survive a Farsi label — a Farsi word runs 15–25% longer, and SETS,
 * REPS and ORDER are the three columns whose headings are words rather than
 * data. Measured in Chromium at 1440 and 1100 in both languages before these
 * numbers were fixed — but see the two-line block below: at 1100 the six
 * columns never fitted, and this shape now starts at 1360.
 *
 * NO WRAPPING. `minmax(0, …)` on the two free-text tracks is what lets a long
 * exercise name shrink its own cell instead of pushing the row wider than the
 * table — the flex version's 340px basis was there to force a wrap that the
 * grid does not need.
 *
 * THE HEADINGS ROW IS A SEPARATE GRID (bugs.md, 2026-09-26). Each `.bld-row`
 * resolves its own tracks, so the two `auto` tracks were the width of the
 * word in the headings row (HOLD 51px en, 60 fa; ORDER 61 en, 42 fa) and of
 * the buttons in every other row (the lock 32, the movers 124), and every
 * heading between them sat 19px (en) or 28px (fa) off its column, the last
 * one 63 / 82. The three `auto` cells — `.bld-lockcell`, `.bld-acts`, and
 * the meal table's `.bld-kcal` — carry a `min-width` wide enough for the
 * longest word in either language AND for the buttons, so every row, headings
 * included, asks the same of its `auto` tracks. A min-width and not a fixed
 * track because the meal table's last two tracks must still collapse to
 * nothing when the kcal column is not there (below). */
.bld-row {
  display: grid;
  grid-template-columns:
    /* HOLD */ auto
    /* EXERCISE */ minmax(0, 1fr)
    /* SETS */ minmax(56px, 72px)
    /* REPS */ minmax(72px, 96px)
    /* NOTE */ minmax(0, 190px)
    /* ORDER */ auto;
  gap: 6px 10px;
  align-items: center;
  padding: 10px 14px;
}
/* The meal table is four equal free-text columns, its computed kcal, then the
   movers. The kcal column only exists once the food list has arrived, so the
   last track is `auto` twice over: with no number in it, it is 0 wide and the
   movers sit where the fifth column would have been. */
.team-meals .bld-row {
  grid-template-columns: repeat(4, minmax(0, 1fr)) auto auto;
}
/*
 * THE MIDDLE WIDTHS: TWO LINES A ROW (bugs.md, 2026-09-25).
 *
 * The six columns above need about 780px of row to give the name 180. The
 * pane is not the viewport: the nav (185) and the queue (274) sit beside it,
 * so a 1100px window leaves the row 598 and a 1024px one 522 — and the fixed
 * tracks alone (HOLD 51, SETS 72, REPS 96, NOTE 190, ORDER 124, five gaps 50)
 * come to 583. The grid hands the capped tracks their maximum BEFORE the `fr`
 * track sees any space, so the name track went to 0 and the input sat at its
 * 28px of padding and border, at every width from 901 to about 1250. Measured
 * in Chromium, both languages: 28px at 1024 and 1100, 186 at 1280.
 *
 * A floor on the name alone would overflow the row (there is no width to
 * take it from), so between the phone card and the six-column table the row
 * takes TWO LINES: the name across the top, SETS, REPS and NOTE under it,
 * with HOLD and ORDER spanning both lines at the two ends. The headings row
 * is the same shape, so every column is still under its own name. The NOTE
 * floor lets SETS and REPS give up their slack first at 1024 rather than the
 * note. Below 1024 the pane is under 520 and even two lines cannot hold the
 * five fixed tracks, so the card takes over there (the phone block below).
 *
 * Meal rows do the same: the meal name across the top, protein, carb and fat
 * under it, kcal and the movers at the end. At 1100 the four equal columns
 * gave each part 83px.
 *
 * TO 1359, NOT 1279 (bugs.md, 2026-09-26). Aligning the headings widened the
 * HOLD track from the 32px button to the 64px the Farsi word needs, and the
 * six-column shape paid for it out of the name: 154px at 1280, where 186 had
 * been thin already. Two lines run to 1359 now; the six columns start at
 * 1360, where the name has 234 (en and fa alike), and at 1440, 314.
 */
@media (min-width: 1024px) and (max-width: 1359px) {
  .bld-row {
    grid-template-columns:
      /* HOLD */ auto
      /* SETS */ minmax(56px, 72px)
      /* REPS */ minmax(72px, 96px)
      /* NOTE */ minmax(96px, 1fr)
      /* ORDER */ auto;
    grid-template-areas:
      "hold name name name order"
      "hold sets reps note order";
  }
  .bld-row > .bld-lockcell { grid-area: hold; }
  .bld-row > .bld-cell-name { grid-area: name; }
  .bld-row > .bld-cell-sets { grid-area: sets; }
  .bld-row > .bld-cell-reps { grid-area: reps; }
  .bld-row > .bld-cell-note { grid-area: note; }
  .bld-row > .bld-acts { grid-area: order; }
  .team-meals .bld-row {
    grid-template-columns: repeat(3, minmax(0, 1fr)) auto auto;
    grid-template-areas:
      "meal meal meal kcal order"
      "protein carb fat kcal order";
  }
  .team-meals .bld-row > .bld-cell-meal { grid-area: meal; }
  .team-meals .bld-row > .bld-cell-part:nth-child(2) { grid-area: protein; }
  .team-meals .bld-row > .bld-cell-part:nth-child(3) { grid-area: carb; }
  .team-meals .bld-row > .bld-cell-part:nth-child(4) { grid-area: fat; }
  .team-meals .bld-row > .bld-kcal { grid-area: kcal; }
}
/* THE COLUMN HEADINGS. Not a row of the table — no ground, no hairline, and
   the same 9px of inset the inputs below carry as their own padding, so a
   heading sits over the left edge of its column rather than 9px proud of it. */
.bld-row.bld-labels {
  padding-block: 9px;
  border-block-end: 1px solid var(--border);
  border-start-start-radius: 11px;
  border-start-end-radius: 11px;
  background: var(--surface);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  /*
   * `--secondary`, not `--muted`. These are the six words that say what the
   * columns under them ARE, at 10.5px and letterspaced — `--muted` measures
   * 3.10:1 on `--surface`, which the 2026-08-28 audit already had open as the
   * contrast item to look at, and a column heading nobody can read is a table
   * that has gone back to being a row of controls. `--muted` stays where it
   * belongs: on values and asides, not on structure.
   */
  color: var(--secondary);
}
/* A hairline between rows, as the canvas draws it. `--team-hair` is the
   queue's divider and is deliberately dimmer than `--border`; inside a table
   of fields it disappeared, and four rows of inputs with nothing between them
   read as one block rather than as a row each. */
.team-table.bld > .bld-row { border-block-end: 1px solid var(--border); }
/* The controls under the table sit on the table's own ground, and the
   destructive one is not in the group with the two adds. */
.team-table.bld > .bld-add {
  border-block-start: 1px solid var(--border);
  background: var(--panel);
  border-end-start-radius: 11px;
  border-end-end-radius: 11px;
}
.bld-add > .is-remove-text { margin-inline-start: auto; }
.bld-labels > * { padding-inline: 9px; }
.bld-labels > .bld-acts { text-align: end; display: block; }

/* One cell: the field, with its name above it. The name is hidden at desk
   width, where the column headings say it once for the whole table, and shown
   on the phone card, where there is no table to head. */
.bld-cell { display: block; min-width: 0; }
.bld-lab { display: none; font-size: 11.5px; font-weight: 500; color: var(--secondary); }
.bld-lab-opt { color: var(--muted); font-weight: 400; }
.bld-name, .bld-sets, .bld-reps, .bld-notes-in, .bld-meal, .bld-part, .bld-macro { width: 100%; }
/* A refused cell: the text the coach typed, kept, and why it was not written. */
.bld-in[aria-invalid="true"] { border-color: var(--danger); }
.bld-field-msg { display: block; margin-top: 0.25rem; font-size: 0.875rem; line-height: 1.35; color: var(--danger); }
.bld-sets, .bld-reps { text-align: center; font-variant-numeric: tabular-nums; }
.bld-notes-in { color: var(--secondary); }

/* THE ROW'S NUMBER, for the phone card only — four stacked cards with no
   numbers is four cards you have to count to talk about. It is not a column of
   the table and is not drawn as one. */
.bld-num { display: none; }
/* Four pixels between "move this down" and "delete this" is thin for a
   thumb: the destructive button is not part of the movers' group. */
/* 124px is the three movers: 3 × 36 + 4 + 4 + 8. The headings row's ORDER
   cell (`display: block`, the word) carries the same floor, so the two grids
   agree on the track — see THE HEADINGS ROW IS A SEPARATE GRID above. */
.bld-acts { display: flex; gap: 4px; justify-content: flex-end; min-width: 124px; }
.bld-acts > .is-remove { margin-inline-start: 8px; }
.bld-act {
  min-width: 36px;
  min-height: 36px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--secondary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.bld-act:hover { color: var(--text); border-color: var(--accent-border); }
/* DISABLED MEANS UNAVAILABLE, NOT ABSENT. `opacity: .4` on the whole box
   measured 2.17:1 for the glyph and 1.12:1 for the border — the first row's
   up arrow read as empty space rather than as a control that cannot fire
   (the design pass, 2026-09-03). The box stays; the ink dims. */
.bld-act:disabled { opacity: 1; color: var(--muted); border-color: var(--border); cursor: default; }
.bld-act.is-remove { color: var(--danger); }
/* "Remove this day" sat in a cluster of two gold adds wearing the neutral
   style, because this class had no rule at all. It is destructive and reads
   as destructive; it is not a button anybody should hit for a gold one. */
.bld-act.is-remove-text { color: var(--danger); border-color: var(--danger-border); }
.bld-act.is-add { color: var(--accent); border-color: var(--accent-border); }
/* The armed label is six times the width of the `×` it replaces, and it was
   taking those pixels off the Notes field beside it (measured: 374.8 → 256.5).
   It takes its own line instead, so arming a row moves nothing the coach is
   reading. */
.bld-act.is-armed {
  background: var(--danger-bg);
  border-color: var(--danger-border);
  color: var(--danger);
  flex: 1 0 100%;
  margin-inline-start: 0;
}
/* An armed button carries six times the words a `×` does. In the flex group
   it simply took its own line; inside a grid row it has to be told to, or it
   widens the ORDER track and takes those pixels off the NOTE field beside it
   — which is the thing the line-of-its-own rule was written to stop. */
.bld-row:has(.bld-act.is-armed) .bld-acts { flex-wrap: wrap; }
.bld-act:focus-visible, .bld-in:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bld-add { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 15px 12px; }
.bld-more { padding-inline: 0; }
/* THE ROW LOCK (slice nine, 2026-09-04).
 *
 * A two-state button on a row of text inputs, so `aria-pressed` and not a
 * checkbox — a checkbox there reads as "select this row", which is a different
 * promise. Locked is the LOUDER state deliberately: an unlocked row is the
 * ordinary case and should be quiet, while a lock is a claim the coach is
 * making about the next run and should be visible while scanning the table.
 *
 * The glyph carries no meaning on its own — every one of these has a real
 * aria-label that changes with the state, so the button announces what
 * pressing it will do rather than what it is. */
/* 64px: the Farsi heading over it measures 60 (with its 9px of inset each
   side), the English one 51, the button 32. */
.bld-lockcell { display: flex; min-width: 64px; }
.bld-act.is-lock {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-width: 32px;
  padding-inline: 0;
  font-size: 14px;
  line-height: 1;
}
/* A SQUARE, NOT A PADLOCK EMOJI (the round-4 design pass; backlog §9 asked for
   exactly this decision). The emoji was drawn from a colour font, so `color`
   did nothing to it and the state had to be carried by the border and the
   ground alone — in a row whose other buttons are text glyphs. A box takes the
   row's ink in any font, which is what lets the held state be a colour rather
   than a pair of chrome changes. */
.bld-lock-box { font-size: 15px; line-height: 1; }
/* The word is for the phone card, where the row becomes a stack and a lone
   12px square at the top of it says nothing. At desk width the HOLD column
   heading is the word, once for the table. */
.bld-lock-word { display: none; font-size: 12.5px; font-weight: 600; }
/* THE STATE IS THE BORDER AND THE GROUND, NOT THE INK.
 *
 * The glyph is an emoji, and an emoji is drawn from a colour font: `color`
 * does nothing to it. An earlier version of this rule set the ink and claimed
 * the locked state was "louder" for it, which was simply false on screen.
 * Whether a padlock emoji belongs in a row whose other buttons are text
 * glyphs (×, ↑, ↓) is a design question and is on backlog §9; what is not
 * negotiable is that the state it is in must be visible, so it is carried by
 * the two properties that do respond. */
.bld-act.is-lock.is-on {
  border-color: var(--accent-border);
  background: var(--accent-bg);
  color: var(--accent);
}

/* WHAT THE RUN CHANGED THAT THE COACH HAD LOCKED (slice nine, 2026-09-04).
 *
 * A list of before-and-afters, not prose: the coach is comparing two values
 * and a sentence would make them read it twice. The arrow is decorative and
 * hidden from assistive tech — the field label and the two values are what
 * carries the meaning, in that order, which is also how it reads aloud.
 *
 * A dealt-with break DIMS rather than disappearing, so the card still answers
 * "what did I just agree to" one press later. Its ink stays above the muted
 * token rather than going to opacity, for the reason the disabled builder
 * arrows did (the design pass, 2026-09-03): faded to nothing reads as absent
 * rather than as done. */
.team-locks { list-style: none; padding: 0; margin: 10px 0 0; display: flex; flex-direction: column; gap: 12px; }
.team-lock { padding-block-end: 12px; border-block-end: 1px solid var(--border); }
.team-lock:last-child { border-block-end: 0; padding-block-end: 0; }
.team-lock.is-done .team-lock-name,
.team-lock.is-done .team-lock-fields { color: var(--muted); }
.team-lock-name { font-weight: 600; margin-block-end: 4px; }
.team-lock-what { margin: 0 0 8px; color: var(--text); font-size: 13.5px; }
.team-lock-fields { list-style: none; padding: 0; margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 4px 16px; }
/* min-width: 0 so a long Notes value wraps inside its own cell rather than
   pushing the row wider than the card (the flex-child truncation rule). */
.team-lock-fields li { display: flex; align-items: baseline; gap: 6px; font-size: 13.5px; min-width: 0; }
.team-lock-was, .team-lock-now { overflow-wrap: anywhere; min-width: 0; }
.team-lock-name { overflow-wrap: anywhere; }
.team-lock-field { color: var(--muted); font-size: 12px; }
.team-lock-was { text-decoration: line-through; color: var(--muted); font-variant-numeric: tabular-nums; }
.team-lock-arrow { color: var(--muted); }
.team-lock-now { font-weight: 600; font-variant-numeric: tabular-nums; }
.team-lock-state { margin: 0; font-size: 12.5px; color: var(--muted); }

/* THE MEAL PLAN'S OWN ARITHMETIC (slice eight, 2026-09-04).
 *
 * A readout, not a control: no border, no colour that could read as pass or
 * fail. A plan under its target is a normal plan — sources are interchangeable
 * and a portion is eyeballed — so nothing here goes red. Tabular numerals so
 * four totals in a row line up at the decimal, and the numbers are pinned ltr
 * as Latin DATA in both languages (see .ltr-data, CLAUDE.md). */
.bld-kcal {
  flex: 0 0 auto;
  /* Wide enough for a four-digit Farsi number and its unit (72px measured
     for three digits) AND for the heading, so the headings row and the data
     rows resolve the same `auto` track (the separate-grids note above). */
  min-width: 84px;
  /* The same 9px .bld-labels gives its other headings, so the number and the
     word above it share an edge — the fields either side carry it as input
     padding, and a bare span would sit 9px proud of its own label. */
  padding-inline: 9px;
  text-align: end;
  font-size: 12.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.bld-totals-wrap { padding: 4px 15px 12px; }
.bld-totals { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline; }
.bld-total-h { font-size: 12px; font-weight: 600; color: var(--muted); flex: 0 0 100%; }
.bld-total { display: flex; flex-direction: column; gap: 1px; min-width: 92px; }
.bld-total-k { font-size: 11.5px; color: var(--muted); }
.bld-total-v { font-size: 15px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.bld-total-t { font-size: 11.5px; color: var(--muted); }
.bld-notes { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.bld-note { display: flex; gap: 6px; align-items: center; }
.bld-note-in { flex: 1 1 auto; }

/* ========================================================================
 * THE BUILDER, AS CLAUDE DESIGN DREW IT — the round-4 canvas, 2026-09-11.
 * `docs/design/canvas/sobati-fitness.dc.html`, screens Coach · Builder and
 * Coach · Generate at both devices.
 *
 * The canvas is built entirely from inline `style` attributes, which is how
 * that tool works and is NOT how this app ships: the CSP forbids an inline
 * style and an inline script, so every value below arrived as a class. The
 * palette needed no conversion — the canvas was drawn from this file's own
 * tokens, so #12151a IS --panel and #d4a13a IS --accent.
 * ===================================================================== */

/* ---------------------------------------------------------- the head
 *
 * Four things used to compete for the top of this pane: a title with Deliver
 * beside it, a row of warn chips, the read-only Framework-mode card, the
 * deload form, and the validation card. The canvas gives it an eyebrow, a
 * title, ONE quiet row of chips and a disclosure — and puts the two cards that
 * are SETTINGS rather than findings inside the disclosure, where a coach opens
 * them when they are changing something. */
.bld-pane-head { align-items: flex-start; gap: 14px; }
.bld-head-main { min-width: 0; flex: 1 1 300px; }
.bld-eyebrow {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  align-items: baseline;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The client's name stays a link — it is the way off this pane and onto their
   file — but it is an eyebrow now, so it wears the eyebrow's ink and finds its
   underline on hover rather than carrying one over a line of small caps. */
.bld-eyebrow a { color: inherit; text-decoration: none; }
.bld-eyebrow a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.bld-eyebrow-dot { color: var(--border-strong); }
.bld-title { margin-block-start: 6px; }
.bld-title h2 { font-size: 22px; letter-spacing: -.02em; }
.bld-title-sep { color: var(--muted); font-weight: 400; }

.bld-chips {
  position: relative;
  margin-block-start: 9px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
/* QUIET. Nothing in this row is a warning — warnings are the panel below it —
   so a chip here is a fact about the shape of the program and is drawn like
   one: the card's own ground, a hairline, and body ink. */
.bld-chip {
  padding: 3px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--secondary);
  font-size: 11.5px;
  line-height: 1.5;
}
.bld-chip.is-warn { border-color: var(--danger-border); background: var(--danger-bg); color: var(--danger); }
a.bld-chip { text-decoration: none; }
a.bld-chip:hover { border-color: var(--accent-border); }

/* SETUP & FRAMEWORK — a real <details>, so it opens and its two forms post
   with no script at all. */
.bld-setup { min-width: 0; }
.bld-setup > summary {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding-inline: 4px;
  cursor: pointer;
  list-style: none;
  color: var(--accent);
  font-size: 11.5px;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.bld-setup > summary::-webkit-details-marker { display: none; }
.bld-setup > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bld-setup-body {
  margin-block-start: 10px;
  padding: 4px 14px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--panel);
}
/* A PANEL, not a wedge in the chip row. Above the narrow breakpoint the
   disclosure sits at the end of a row of chips, so opening it in flow would
   make it as wide as its own summary; it hangs under the row instead. Below
   it, the row has already wrapped and the panel is simply the next block —
   a floated panel on a 390px screen is a panel with nowhere to hang. */
@media (min-width: 760px) {
  .bld-setup[open] > .bld-setup-body {
    position: absolute;
    inset-inline-start: 0;
    top: calc(100% + 8px);
    z-index: 4;
    width: min(640px, 100%);
    box-shadow: 0 22px 50px rgb(0 0 0 / 55%);
  }
}

/* THE ACTIONS, grouped top-right: what is unsaved, Save, Deliver. */
/*
 * `min-width: 0`, because a flex item's automatic minimum is its MIN-CONTENT
 * and this strip's min-content is the whole "when" form: at 320px the head was
 * 280px and the strip laid itself out at 354px, past the pane, in both
 * languages. Found while measuring the pill (2026-09-11).
 */
.bld-actions { align-items: center; gap: 8px; min-width: 0; }
.bld-pill {
  padding: 4px 10px;
  border: 1px solid var(--border-strong);
  /* A STADIUM AT ONE LINE, A ROUNDED BOX AT TWO, and 14px is what says both:
     the one-line box is 28.4px tall, so half of it IS 14px and the ends are
     exactly as round as 999px drew them. 999px was the bug's other half — the
     moment the sentence wrapped, those ends became semicircles 33px deep and
     the thing read as a blob rather than as a chip (2026-09-11). The string
     is short enough to stay on one line from 390px now, so this matters at
     320px and to whatever is translated into this chip next. */
  border-radius: 14px;
  background: var(--surface);
  color: var(--secondary);
  font-size: 11.5px;
  font-weight: 600;
  /* A LINE OF ITS OWN, ALWAYS — the sentence is 60 characters and a chip
     wedged into a button row is what "out of size" meant (the owner,
     2026-09-11). `max-width: 26ch` capped it at 182px and it wrapped to
     THREE lines at every width in BOTH languages, 182x65 measured, while
     the row had 461px (en) / 473px (fa) of unused space beside the buttons.

     That string was 376px (en) / 378px (fa) on one line — measured, and within
     2px of each other, so the "Farsi runs 15-25% longer" rule did NOT bite on
     the pair; the cap was a third of the sentence in BOTH. The owner then took
     the shorter wording offered in decisions.md, so it is 299px (en) / 268px
     (fa) today and ONE LINE at every width from 390px up in both languages.

     The pill takes its natural width and the row decides where it goes: it
     shares the line with Save and Deliver from about 516px of row (en), its
     own line below that, and a second line only at 320px in English, where
     299px does not fit in 280px of pane. The two buttons cannot be split by
     any of that, because `.bld-buttons` makes them one item — and the numbers
     above are why that wrapper is not made redundant by the shorter string:
     they move the band, they do not close it. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
}
/*
 * Save and Deliver, ONE item of the strip — see the wrapper's own note in
 * _team-builder-head.eta.
 *
 * THREE BOTTOMS, MEASURED (the owner's report, 2026-10-01, at 1280): Save
 * 261.4, the select and date box 278.6, Deliver 284.6. Centring is what put
 * Save 17px high — it was centred on a form its captions make half as tall
 * again as any control in it — so the row aligns on the bottom edge, the one
 * line a captioned field and a bare button share. Save sat outside the form's
 * `--control-h`, so it was 37.6px beside 38px fields on a laptop and beside
 * 44px ones on a phone; the token is the group's now. And `nowrap` on the
 * pair is what squeezed "Save program" to two lines and 53px when the form
 * beside it was wide: the group may wrap inside itself, and a button in it
 * may not wrap its own label or shrink.
 */
.bld-buttons {
  --control-h: 38px;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  min-width: 0;
  max-width: 100%;
}
@media (pointer: coarse), (max-width: 560px) {
  .bld-buttons { --control-h: 44px; }
}
.bld-buttons .btn { min-height: var(--control-h); flex-shrink: 0; white-space: nowrap; }
.bld-pill.is-warn { border-color: var(--accent-border); background: var(--accent-bg); color: var(--accent); }
/* WHY Deliver is unavailable, beside the button rather than somewhere the
   coach has to go looking. Its own line under the group, because the reason is
   a sentence and the buttons are not.

   FROM THE ROW'S START EDGE, not its end (2026-10-01). The controls start at
   the start; where the row ENDS depends on how many of them reached the last
   line — Deliver is 74px from the start on a phone and 760px from it on a
   laptop — so an end-aligned line was "under Deliver" only when the row
   happened to fill the pane, and floated off past it everywhere else, which
   the owner saw. The start edge is the one every variant shares at every
   width, in both directions (it is the right edge in Farsi). */
.bld-why { flex: 1 0 100%; margin: 0; text-align: start; font-size: 12px; color: var(--secondary); }

/* ------------------------------------------------- what stops it going out
 *
 * The canvas's "Refused delivery" state: one panel under the head, each fault
 * on a row of its own rather than a bullet in a paragraph, and a line on the
 * right saying when this was checked. A refusal has to say WHAT is wrong, not
 * that something is (AGENTS.md). */
.bld-faults {
  margin-block-start: 14px;
  padding: 14px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--panel);
}
.bld-faults.is-blocking { border-color: var(--danger-border); background: var(--danger-bg); }
.bld-faults-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  justify-content: space-between;
}
.bld-faults-title { font-size: 13.5px; font-weight: 700; color: var(--text); }
.bld-faults.is-blocking .bld-faults-title { color: var(--danger); }
.bld-faults-meta { font-size: 11.5px; color: var(--secondary); }
.bld-faults .team-problems {
  margin-block-start: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  padding: 0;
}
.bld-faults .team-problems > li {
  margin: 0;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  font-size: 13px;
  line-height: 1.5;
  text-wrap: pretty;
}
.bld-faults.is-blocking .team-problems > li { border-color: var(--danger-border); }
/*
 * THE PANEL IS THE WARNING; THE SENTENCES ARE THE ANSWER.
 *
 * `.team-problems > li.is-blocking` is bold danger ink, which is right where a
 * blocking problem sits in a list of ordinary ones and wrong inside a panel
 * that is already red end to end: three lines of bold red is a wall to get
 * past rather than an instruction to follow. Inside this panel a fault reads
 * as body text, and the heading above carries the verdict.
 */
.bld-faults .team-problems > li.is-blocking { color: var(--text); font-weight: 400; }
.bld-faults > .field-help { margin-block-start: 10px; }

/* ----------------------------------------------------------- a section
 *
 * A day, the nutrition block, the standing notes: the three sections of the
 * document, each with a heading and a line under it saying what is in it. */
.bld-section { margin-block-start: 4px; }
.bld-section-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  align-items: flex-start;
  justify-content: space-between;
}
.bld-section-id { min-width: 0; flex: 1 1 260px; }
.bld-section-titlerow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-width: 0; }
.bld-section-title { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.01em; }
.bld-sep { color: var(--muted); }
/* THE DAY'S SUMMARY LINE — "4 exercises · about 55 minutes". The count is a
   fact and the minutes an estimate, said in the word "about" and computed in
   public/dayEstimate.js, which the server and review.js both read. */
.bld-section-sub { margin: 4px 0 0; font-size: 12px; color: var(--secondary); }
.bld-sub-dot { color: var(--border-strong); }
/* WHAT HOLDING A ROW DOES, in one sentence beside the day it applies to.
   Nothing on the row said it until you had read the breaks card once. */
.bld-legend {
  margin: 0;
  display: flex;
  gap: 8px;
  align-items: flex-start;
  max-width: 36ch;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--secondary);
  text-wrap: pretty;
}
.bld-legend-box {
  flex: none;
  width: 9px;
  height: 9px;
  margin-block-start: 4px;
  border: 1px solid var(--accent-border);
  border-radius: 2px;
  background: var(--accent-bg);
}
/* The four macro targets, above the meal table. */
.bld-macros {
  margin-block-start: 14px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
}
.bld-macros .bld-lab { display: block; margin-block-end: 5px; }
/* ------------------------------------------------------- START FROM
 *
 * The blank editor's panel (the canvas's own, and the answer to backlog §9's
 * question about the seed buttons): three ways to put days on the page, each
 * with a name, what it will write, and one button. They were two buttons in
 * the "+ Day" row with a help sentence each underneath — at 390px in Farsi
 * that was four wrapped buttons over two paragraphs before the coach reached
 * the table, and an action that writes four days read exactly like one that
 * writes a single row. */
.bld-seed {
  margin-block-start: 18px;
  padding: 16px 18px;
  border: 1px solid var(--accent-border);
  border-radius: 13px;
  background: var(--accent-bg);
}
.bld-seed-h {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}
.bld-seed-help {
  margin: 8px 0 0;
  max-width: 60ch;
  font-size: 13px;
  line-height: 1.6;
  color: var(--secondary);
  text-wrap: pretty;
}
.bld-seed-row {
  margin-block-start: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 11px;
  background: var(--surface);
}
.bld-seed-words { flex: 1 1 240px; min-width: 0; }
.bld-seed-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.bld-seed-sub {
  margin-block-start: 3px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--secondary);
  text-wrap: pretty;
}
/* THE ONE THAT WRITES THE WEEK IS THE PRIMARY. It is what the client answered
   the intake with, so it is the answer this panel exists to offer; the other
   two are alternatives to it, not peers of it. */
.bld-seed-row > .bld-act { flex: none; }
.bld-seed-row > .bld-act.is-primary {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-ink);
}
.bld-seed-row > .bld-act.is-primary:hover { background: var(--accent-hover, var(--accent)); color: var(--accent-ink); }

.bld-more-help {
  align-self: center;
  max-width: 36ch;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--secondary);
  text-wrap: pretty;
}

/* ------------------------------------------------ the two rows at the foot
 *
 * The raw markdown and the amendment box. Both were full-width blocks under
 * the program — the markdown behind a summary with no chrome, the amendment
 * box a card asking what to change on EVERY sub-view. The canvas makes them a
 * pair of matching disclosure rows: a `+`, a name, and one line saying what is
 * inside. Real <details>, so both open with no script. */
.bld-fold {
  margin-block-start: 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
}
.bld-fold > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 12px 16px;
  cursor: pointer;
  list-style: none;
}
.bld-fold > summary::-webkit-details-marker { display: none; }
.bld-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The marker is drawn by hand: `display: flex` on a <summary> suppresses the
   real one, which is the same trap the by-hand disclosure on the generate
   queue fell into (2026-09-06). */
.bld-fold-mark {
  flex: none;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}
.bld-fold-mark::before { content: "+"; }
.bld-fold[open] > summary > .bld-fold-mark::before { content: "\2212"; }
.bld-fold-name { font-size: 13px; font-weight: 600; color: var(--text); }
.bld-fold-sub { min-width: 0; font-size: 12px; color: var(--secondary); text-wrap: pretty; }
.bld-fold-body { padding: 0 16px 16px; }

/* ==================================================================== phone
 *
 * THE ROW BECOMES A CARD, which is the question the brief led with and the
 * canvas answers: numbered 01, 02…, a Hold pill at the top start, the two
 * movers at the top end, then every field with its own name above it, then
 * Remove at the bottom end. A table of six columns does not survive 390px,
 * and it survives it less in Farsi, where every heading runs longer.
 *
 * FLEX, NOT A SECOND GRID. The card's shape is "two full-width fields with a
 * pair of halves between them", which is four `flex-basis` values and an
 * `order` — a grid-template-areas version needs the tracks to serve both the
 * header line and the halves, and they do not.
 *
 * UP TO 1023, NOT 900 (bugs.md, 2026-09-25). The queue stays beside the pane
 * above 900, so at 901 the row is 399px wide: HOLD, ORDER and the SETS and
 * REPS floors leave a two-line row 36px for the note. The card is the only
 * shape that holds five fields there; the two-line row takes over at 1024. */
@media (max-width: 1023px), (pointer: coarse) {
  .bld-in { font-size: 16px; min-height: 44px; }
  .bld-act { min-width: 44px; min-height: 44px; }
  /*
   * THE THREE LINKS IN THE HEAD ARE TARGETS TOO. `.bld-act` and `.bld-in` were
   * raised and the head's own controls were not — "Setup & framework" was a
   * 28px summary, a chip link about 24px, and the client link in the eyebrow
   * 11px type with no padding at all, which is the only way off this pane. All
   * three are 44px on a coarse pointer now.
   */
  .bld-setup > summary { min-height: 44px; padding-inline: 8px; }
  a.bld-chip { display: inline-flex; align-items: center; min-height: 44px; padding-inline: 12px; }
  .bld-eyebrow a { display: inline-flex; align-items: center; min-height: 44px; }

  .bld-lab { display: block; margin-block-end: 5px; }

  .team-table.bld {
    border: 0;
    background: none;
    overflow: visible;
  }
  .bld-row,
  .team-meals .bld-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-block-start: 10px;
    padding: 14px;
    border: 1px solid var(--border-strong);
    border-radius: 12px;
    background: var(--panel);
  }
  /*
   * THE COLUMN HEADINGS GO. There is no table to head — the row above is a
   * card, and the names are on the fields themselves.
   *
   * AFTER the card rule, not before it, and that is the whole rule: the meal
   * table's selector is `.team-meals .bld-row`, two classes, exactly what
   * `.bld-row.bld-labels` is. Equal specificity, so source order decides — and
   * written above, the hide LOST and the meal table drew its own column names
   * as a fifth card at 390px. The day table, matched by the one-class
   * `.bld-row`, hid correctly the whole time, which is what made it look
   * fixed. Both are named here so neither can drift.
   */
  .bld-row.bld-labels,
  .team-meals .bld-row.bld-labels { display: none; }

  .bld-num {
    display: inline;
    order: 1;
    flex: none;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
  }
  .bld-lockcell { order: 2; flex: none; }
  .bld-lock-word { display: inline; }
  .bld-act.is-lock { min-width: 44px; padding-inline: 13px; }
  /* The movers go to the row's end, and the remove button to the foot of the
     card — so `.bld-acts` stops being a group and its three buttons become
     items of the card itself. */
  .bld-acts { display: contents; }
  .bld-acts > .bld-act { order: 3; flex: none; }
  .bld-acts > .bld-act:first-child { margin-inline-start: auto; }
  .bld-acts > .is-remove {
    order: 9;
    margin-inline-start: auto;
    margin-block-start: 2px;
    padding-inline: 15px;
    border-color: var(--danger-border);
    background: var(--danger-bg);
  }
  .bld-cell-name, .bld-cell-note, .bld-cell-meal { order: 5; flex: 1 0 100%; }
  .bld-cell-note { order: 7; }
  .bld-cell-sets, .bld-cell-reps, .bld-cell-part {
    order: 6;
    flex: 1 1 calc(50% - 5px);
    min-width: 120px;
  }
  .bld-kcal {
    order: 8;
    flex: 1 0 100%;
    padding-block-start: 9px;
    padding-inline: 0;
    border-block-start: 1px solid var(--border);
    text-align: start;
  }
  /* The armed label is a sentence; on a card it takes the width it needs
     rather than squeezing the two movers beside it. */
  .bld-act.is-armed { flex: 1 0 100%; margin-inline-start: 0; }

  /* The two "add a section" buttons are full width on a phone, as the canvas
     draws them: a 44px target that is 90px wide in a row of three is a target
     you miss. */
  .bld-add .bld-act { flex: 1 0 100%; }
  .bld-seed-row > .bld-act { flex: 1 0 100%; }
  .bld-more-help { flex: 1 0 100%; max-width: none; }
}

/* ------------------------------------------------- the markdown editor
 *
 * THE PHONE-ONLY CHECKBOX FOLD IS GONE (the round-4 design pass, 2026-09-11).
 *
 * It wrapped `.rev-edit-body` and existed only under 900px, because the desk
 * drew two columns and a <details> is closed at every width. Both halves of
 * that reasoning had expired: the split went to ONE column on 2026-09-06, and
 * the canvas gives the markdown a disclosure row of its own at the foot of the
 * program (`.bld-fold`) at every width. Two collapses named "Raw markdown"
 * around one textarea is one more than the screen needs — which the rule that
 * used to sit here had already worked out in its own comment and left on
 * backlog §9 for a design round to decide. This is that decision.
 *
 * `.rev-save-row` went with it: Save is in the pane head now, with the other
 * two actions, reaching this form by `form=`.
 */
.rev-edit > label {
  display: block;
  margin-block-end: 7px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--secondary);
}
.rev-md {
  width: 100%;
  min-height: 26rem;
  max-height: 60vh;
  resize: vertical;
}
@media (min-width: 1100px) {
  .rev-md { min-height: 34rem; }
}
@media (max-width: 900px) {
  /* Two thirds of a phone screen was the measured cost of this textarea. Open,
     it is still a working surface — just not the whole page. */
  .rev-md { min-height: 14rem; }
}

/* ------------------------------------------------ review: generation summary
 *
 * Every run, oldest first. Regenerating appends — comparing this run with the
 * one before it is the whole reason to keep them.
 */
.team-run { margin-block-start: 12px; }
.team-run.is-current { border-color: var(--accent-border); }
.team-run-tags { margin-block-start: 9px; display: flex; flex-wrap: wrap; gap: 6px; }
/* Long enough to run away with the page when the model has a lot to say, so it
   scrolls inside its own card rather than pushing the next run off-screen. */
.team-run .team-reply-note { max-height: 22rem; overflow-y: auto; }

/* ------------------------------------------------- the engagement, on account
 *
 * One segment per block of what they bought. A 24-week VIP trains six programs
 * off one payment and the app said nothing about that — they saw a program,
 * then another program, with no sense of what was left.
 */
.engagement { margin-block-start: 18px; }
.engagement-head {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  align-items: baseline;
  justify-content: space-between;
}
.engagement-bar { margin-block-start: 8px; display: flex; gap: 4px; }

/*
 * Up and down, beside the bar that shows what they have. A client deciding
 * whether to keep paying is looking at exactly this, and putting the way down
 * somewhere quieter is how somebody stops answering instead of saying they
 * want something cheaper.
 */
.engagement-actions { margin-block-start: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
.engagement-actions .btn { margin-block-start: 0; }
.engagement-bar > span {
  flex: 1 1 0;
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
}
.engagement-bar > span.is-done { background: var(--accent); }

/* ------------------------------------------------- form check: the progress bar
 *
 * Compression runs in real time — a 60-second clip takes 60 seconds, because
 * the source has to actually play. A silent minute reads as a hang, so the bar
 * is not decoration.
 *
 * Bucketed to 5% as a class rather than an inline width: the CSP forbids the
 * style attribute, which is the same reason every other bar in this app is
 * built this way.
 */
.video-bar {
  margin-block-start: 8px;
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
  overflow: hidden;
}
/*
 * Scale, not width (audit F51). This one moves twenty times during a single
 * upload, which is exactly the case the rule is about — and it is running
 * while the browser is also compressing video. The radius comes from
 * `.video-bar`'s own `overflow: hidden`, for the same reason as the gym bar.
 */
.video-bar::before {
  content: "";
  display: block;
  height: 100%;
  width: 100%;
  background: var(--accent);
  transition: transform .2s linear;
  transform: scaleX(0);
  transform-origin: left;
}
html[dir="rtl"] .video-bar::before { transform-origin: right; }
@media (prefers-reduced-motion: reduce) {
  .video-bar::before { transition: none; }
}
.video-bar.is-0::before { transform: scaleX(0); }
.video-bar.is-5::before { transform: scaleX(0.05); }
.video-bar.is-10::before { transform: scaleX(0.1); }
.video-bar.is-15::before { transform: scaleX(0.15); }
.video-bar.is-20::before { transform: scaleX(0.2); }
.video-bar.is-25::before { transform: scaleX(0.25); }
.video-bar.is-30::before { transform: scaleX(0.3); }
.video-bar.is-35::before { transform: scaleX(0.35); }
.video-bar.is-40::before { transform: scaleX(0.4); }
.video-bar.is-45::before { transform: scaleX(0.45); }
.video-bar.is-50::before { transform: scaleX(0.5); }
.video-bar.is-55::before { transform: scaleX(0.55); }
.video-bar.is-60::before { transform: scaleX(0.6); }
.video-bar.is-65::before { transform: scaleX(0.65); }
.video-bar.is-70::before { transform: scaleX(0.7); }
.video-bar.is-75::before { transform: scaleX(0.75); }
.video-bar.is-80::before { transform: scaleX(0.8); }
.video-bar.is-85::before { transform: scaleX(0.85); }
.video-bar.is-90::before { transform: scaleX(0.9); }
.video-bar.is-95::before { transform: scaleX(0.95); }
.video-bar.is-100::before { transform: scaleX(1); }

.review-item { margin-block-start: 14px; padding: 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--panel); }
.review-item-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
.review-item-when { font-size: 12px; color: var(--muted); }
.review-item-ex { font-size: 13.5px; font-weight: 600; }
.review-item-note { margin-block-start: 8px; font-size: 13.5px; color: var(--secondary); }
.review-reply { margin-block-start: 12px; padding-inline-start: 12px; border-inline-start: 2px solid var(--accent); }
/* Constrained, because a portrait phone video is 9:16 and would otherwise be
   taller than the screen it is being watched on. */
.review-video { margin-block-start: 12px; width: 100%; max-width: 380px; border-radius: 10px; }

/* The way back into the conversation, for a form check answered there. Sits
   under the row rather than in its head: it is where the answer is, not what
   the row is about. */
.review-in-chat { margin-block-start: 10px; }
.review-list-title { margin-block-start: 28px; }

/* A save on every tab of the account form. They all submit the same form —
   the form carries every tab's fields, so saving from one tab saves the lot. */
.tab-panel-save { margin-block-start: 22px; padding-block-start: 18px; border-block-start: 1px solid var(--border); }

/* The held beat before Telegram takes the screen. */
.tg-opening { display: flex; align-items: center; gap: 8px; margin-block-start: 10px; font-size: .9rem; color: var(--secondary); }

/* ------------------------------------------------- gym: the autosave pip
 *
 * A set records itself as it is filled in, so the client needs to see that it
 * did. Small and beside the row — reassurance, not an announcement.
 */
.gym-saved { font-size: 11.5px; font-weight: 600; }
.gym-saved.is-saving { color: var(--muted); }
.gym-saved.is-ok { color: var(--success); }
/* Held on the device: not an error — it is safe and it will go — but it must
   not read as "saved" either, because the coach cannot see it yet. */
.gym-saved.is-queued { color: var(--accent); }
/* The server received this set and turned it down — the day was finished in
   another tab, most likely. The one row state that IS an error, so it is the
   one that gets a word rather than a mark: "held on device" and "not saved"
   are opposite facts and must not look alike. */
.gym-saved.is-warn { color: var(--danger); }

/* ------------------------------------------------- gym mode, round of fixes
 *
 * The Log buttons are redundant once the autosave is running — gym.js sets
 * .gym-autosaves on <html>, so without script they are still there and are
 * still the only way to log. Hidden rather than removed for exactly that
 * reason.
 */
/*
 * HIDDEN BY DEFAULT, restored by noscript.css.
 *
 * This used to be keyed on `.gym-autosaves`, a class gym.js puts on <html> —
 * and a deferred script cannot run before the first paint, so every manual Log
 * button flashed on screen for an instant on every load. Inverting it means
 * the common case (script) has nothing to undo, and the rare case (no script)
 * pays a stylesheet it alone requests.
 */
.gym-log-set,
.gym-log-round { display: none; }

/* A mark, not a sentence: a row on the phone alone must not look identical to
   one the coach can see, but it does not need to say so in words. */
.gym-saved { min-width: 1em; text-align: center; }

/* Rounds are links now. Same box as the span they replace, so tapping one is a
   target rather than a guess. */
.gym-round {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px 12px;
  text-decoration: none;
}
.gym-round.is-partial { border-color: var(--accent-border); }

/*
 * The exit link and the finish button are one row of two controls and must be
 * one height.
 *
 * They were 48 and 61: different padding, different font size, and one with a
 * border the other did not have. Matched by declaring the SAME box for both
 * rather than nudging one towards the other — a shared min-height alone did not
 * fix it, because neither was hitting the floor.
 */
.gym-foot-leave,
.gym-foot .gym-finish {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 14px 18px;
  border-radius: 9px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  /* A transparent border on the filled one, so the outlined one is not 2px
     taller — the same trap as the dashboard's primary/secondary pair. */
  border: 1px solid transparent;
}
.gym-foot-leave {
  border-color: var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
}
/* Centred, or the link stretches to the row while the button beside it does
   not — which is what made the two read as different kinds of control. The
   form wrapping the finish button loses the global form margin for the same
   reason it does in the dashboard's action rows. */
.gym-foot { align-items: center; }
.gym-foot form { margin: 0; }

/* ------------------------------------------------ form check, on the card
 *
 * Same box as the swap button beside it, because they are the same kind of
 * thing: a secondary action on the exercise you are looking at. Matching it
 * exactly rather than inventing a third size is why the tool row still reads
 * as a row.
 */
/* The box itself is drawn with `.gym-swap-btn`, above — one rule for both. */
.gym-formcheck:hover {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}

/*
 * The locked state now comes from `.is-vip-locked`, shared with the program
 * view — see _vip-locked.eta. Its own three rules lived here and said the same
 * thing in different numbers, which is how the two screens came to disagree
 * about what "locked" looks like in the first place.
 *
 * Only the background needs saying: `.gym-formcheck` fills itself, and a
 * dashed control that is also filled reads as pressed rather than unavailable.
 */
.gym-formcheck.is-vip-locked { background: none; }
.gym-formcheck.is-vip-locked:hover { background: none; }

/* ------------------------------------------------ the form-check dialog */

.gym-fc {
  width: min(520px, calc(100vw - 24px));
  /* A phone held in landscape, or a small window: the dialog scrolls itself
     rather than putting the Send button somewhere the thumb cannot reach.
     dvh, not vh, because the mobile URL bar is part of the height. */
  max-height: calc(100dvh - 32px);
  overflow: auto;
  overscroll-behavior: contain;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
}
.gym-fc::backdrop { background: rgb(0 0 0 / 0.55); }

.gym-fc-form { display: grid; gap: 12px; padding: 16px; margin: 0; }

.gym-fc-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.gym-fc-title { margin: 0; font-size: 16px; }
.gym-fc-close {
  /* 40px, because this is a thumb in a gym and a close button that needs aim
     is a close button that gets missed. */
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--secondary);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.gym-fc-close:hover { border-color: var(--accent); color: var(--accent); }

.gym-fc .gym-field { display: grid; gap: 6px; }
.gym-fc .gym-field input[type="text"],
.gym-fc .gym-field textarea,
.gym-fc .gym-field input[type="file"] { width: 100%; }

.gym-fc-help, .gym-fc-status { margin: 0; font-size: 11.5px; color: var(--muted); }

.gym-fc-send {
  height: 46px;
  border: 1px solid var(--accent);
  border-radius: 11px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.gym-fc-send:disabled { opacity: 0.6; cursor: default; }

/* The confirmation, since a dialog that closes on success has nowhere else to
   say it. Sits above the foot so it is not covered by it. */
.gym-fc-sent {
  margin: 0 0 12px;
  padding: 11px 13px;
  border: 1px solid var(--success);
  border-radius: 11px;
  background: var(--success-bg);
  color: var(--success);
  font-size: 12.5px;
  font-weight: 600;
}

/* ---------------------------------------------------------- "2 of 4", twice
 *
 * WHAT USED TO BE HERE, and where it went (2026-09-14).
 *
 * A first version of the batch form check stood in this slot — `.ask-hint`,
 * `.ask-bar`, `.ex-ask`, `.ask-locked` and their friends — and the reshaping
 * of 2026-08-17 redefined nearly every one of those selectors a hundred lines
 * further down, under "batch form check: pick, then list", without removing
 * this copy. Both shipped for four weeks. The later block won every
 * declaration the two of them both made, so it was the one drawing the screen;
 * the only thing this copy still carried alone was four `.ask-bar` properties
 * (sticky, its offset, its z-index and its inline margin), and those have
 * moved down to join it. Nothing about the rendered result changed with the
 * fold — see the note on `.ask-bar` down there for the three declarations
 * that were deliberately NOT carried, and why they were inert.
 *
 * Kept here, because nothing duplicated it: "2 of 4", which belongs to the
 * dialog head and the /form-review page rather than to the bar.
 */
.gym-fc-pos, .review-pos {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}
.gym-fc-pos[hidden] { display: none; }
.review-pos { margin: 0 0 12px; }

/* ---------------------------------------- batch form check: pick, then list
 *
 * HIDDEN BY DEFAULT, restored by noscript.css — the gym-mode Log buttons'
 * pattern, for the same reason.
 *
 * This used to be keyed on `is-armed`, a class programcheck.js added on load,
 * so that a page without script showed the ticks and Confirm plainly. It did —
 * and so did every page WITH script, for the gap between first paint and the
 * module running: a column of empty boxes down the start edge of the program
 * that vanished a moment later (bugs.md, 2026-09-08). A deferred module cannot
 * beat the first paint, so the quiet state has to be the stylesheet's default
 * and the plain state the one that costs a request nobody with script makes.
 *
 * `is-picking` is the only class now: programcheck.js toggles it while the
 * arming button is pressed. Without script nothing toggles it, noscript.css
 * puts the ticks and the bar back, and the form submits on its own — the same
 * set of choices the script collects. The one case that is worse than before
 * is a script that was requested and never ran (blocked, or failed to load):
 * the ticks stay hidden and the arming button is inert. Gym mode's Log buttons
 * already make that trade.
 */
.ask-form .ex-ask { display: none; }
.ask-form .ask-bar { display: none; }
.ask-form.is-picking .ex-ask { display: flex; }
.ask-form.is-picking .ask-bar { display: block; }

/*
 * WHERE THE CONFIRM BAR TRAVELS (2026-09-22, the owner's call).
 *
 * The sticky lived on `.ask-bar` and did nothing: a sticky box moves inside
 * its containing block, and `.ask-bar`'s containing block was THIS element,
 * which shrink-wrapped it — both boxes 93.59px, nowhere to go (bugs.md,
 * 2026-09-14). So it is this element that sticks now, inside the form: the
 * form holds the note, all four days and the bar, so there is a program's
 * worth of travel and the bar keeps the last tick company all the way down.
 *
 * TO THE BOTTOM EDGE, not the top. The ticks are on the rows being read, and
 * the thing that has to stay under the thumb is Confirm. `inset-block-start`
 * would have pinned it above the rows it confirms, which is the one place
 * nobody looks.
 *
 * `--ask-bar-clear` is the fixed chrome it must sit ON rather than behind:
 * the tab bar is `position: fixed` at the foot of every signed-in screen, and
 * the iPhone's home indicator sits inside the viewport underneath it. Same
 * arithmetic as the chat report sheet, plus 8px so the two bars read as two.
 *
 * It is a bar, not a floor: it cannot leave the form, so it comes to rest
 * under the last day and the standing notes and the nutrition below scroll
 * past it clean.
 *
 * ONLY WHILE PICKING — `:root.is-asking`, which programcheck.js sets when the
 * client presses the arming button and clears on Cancel or a finished send.
 * The owner asked for Confirm to stay reachable while choosing moves, not for
 * a card floating over every program. A superseded block (the form drawn
 * plainly, no arming button) and a client with no script both keep the bar
 * in flow at the foot of the days, exactly as it was — for them the ticks are
 * always on, so a floating card would never go away (decisions.md).
 *
 * THE FOOT TAKES NO TAPS; THE CARD DOES. `.ask-foot` is a positioned box at
 * z-index 2 over the rows, and any part of it that is not the card is a strip
 * that swallows the tap meant for the tick under it. The card fills it edge
 * to edge now, but `pointer-events` says so on purpose rather than by
 * geometry, so a margin nobody notices cannot bring the dead zone back.
 */
/*
 * ON :root, not on the form, because one of the three readers is the DOCUMENT
 * scroll — and the viewport's `scroll-padding` is read from <html> and from
 * nowhere else.
 *
 * `--ask-bar-clear` is what the bar must stay above. `--ask-bar-band` is that
 * plus the bar itself and its 12px of margin. 130px covers every width the
 * bar was measured at, down to 280px in Farsi (bugs.md, 2026-09-14, the
 * measurement table). It did not while the card was 40px narrower than the
 * days at each end: 184px wide at 320, its buttons wrapped and the bar stood
 * 172px tall. Full width it is 100px there, and 120px in Farsi at 280.
 */
:root {
  --ask-bar-clear: calc(var(--tnav-h) + env(safe-area-inset-bottom, 0px) + 8px);
  --ask-bar-band: calc(var(--ask-bar-clear) + 130px);
}
/*
 * THE BAR MUST NOT COVER THE TICK THE CLIENT IS ON — AGENTS.md is explicit
 * that sticky chrome never covers the focused element, and the bug entry
 * asked for `scroll-margin` on the rows. Measured in Chromium, scroll-margin
 * alone does not do it: focusing a control the browser considers VISIBLE
 * scrolls nothing at all, and a tick under the bar is visible by that test —
 * 8 of 24 ticks came to rest inside the bar's band. Scroll-padding on the
 * scrollport is the half that works, because it takes the band out of the
 * region the browser measures "visible" against: 0 of 24 after it. The row
 * scroll-margin is not kept alongside it — the two add up, and a row scrolled
 * in would stop twice the band's height above the bar.
 *
 * `programcheck.js` puts `is-asking` on <html> while picking, so the padding
 * is there for the state that has a floating bar and gone for every other
 * scroll on the page.
 */
:root.is-asking { scroll-padding-block-end: var(--ask-bar-band); }
/* And the install banner steps aside while picking: fixed at the foot of the
   screen at z-index 40, it sat over Confirm on an iPhone that had not
   dismissed it yet (bugs.md, 2026-09-22). It comes back on Cancel or a sent
   list, as the class does. */
:root.is-asking .install-hint { display: none; }
.ask-foot {
  margin: 16px 0 4px;
  pointer-events: none;
}
:root.is-asking .ask-foot {
  position: sticky;
  inset-block-end: var(--ask-bar-clear);
  /* Over the rows, under the tab bar (40) — both stay tappable. */
  z-index: 2;
}
.ask-start { display: inline-flex; }

/* The note the client is scrolled back up to. Same accent card as the Confirm
   bar, because it is the other half of the same state — one says what to do,
   the other is what you do when you have done it.

   `scroll-margin` rather than a computed offset: programcheck.js scrolls this
   into view at the top of the viewport, and a heading flush against the edge
   of a phone screen reads as cut off. Nothing sticky is above it on this page,
   so a fixed breathing gap is the whole correction needed. */
.ask-note {
  margin: 0 20px 12px;
  padding: 11px 13px;
  border: 1px solid var(--accent-border);
  border-radius: 12px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.5;
  scroll-margin-block-start: 12px;
}

/*
 * THE CONFIRM BAR — one rule, folded from two on 2026-09-14.
 *
 * The box is this block's own (12px/14px, a 12px radius, the accent card),
 * which is what was already on screen: it outranked the older copy above by
 * being later in the file, declaration for declaration.
 *
 * THE STICKY IS NO LONGER HERE, and that is the fix of 2026-09-22. It asked
 * for `position: sticky; inset-block-start: 8px; z-index: 2` on this element
 * for four weeks and never moved the bar a pixel, because the containing
 * block it would have travelled in is `.ask-foot`, which shrink-wrapped it.
 * Those three declarations are now on `.ask-foot` itself, turned to the
 * bottom edge and applied only while picking — see the note up there. What is
 * left here is the card.
 *
 * FULL WIDTH, AND LIFTED. The inline margins went, the card's and the
 * foot's: 20px each, stacked, made the card 40px narrower than the day cards
 * at EACH end (254 against 334 at 390px), so fragments of the row underneath
 * showed either side of a floating bar — which reads as something misplaced.
 * It is now exactly as wide as the days it floats over. The shadow is
 * the chat report sheet's, cast upward, because this is the same kind of
 * surface sitting on the same tab bar; every other floating thing in the app
 * has one. In flow (a past block, no script) it is a shadow above the foot
 * of the days, which is where it has always been drawn.
 *
 * `display: flex`, `align-items` and `gap: 10px` were in the older copy too
 * and are NOT carried, because they never drew anything: the bar's display is
 * decided by the two `.ask-form` rules above and by noscript.css, so it is
 * `none` or `block` in every state there is, and the other two properties are
 * read only by a flex container. Carrying them forward would have written
 * down a layout the bar has never had.
 */
.ask-bar {
  margin: 0 0 12px;
  padding: 12px 14px;
  border: 1px solid var(--accent-border);
  border-radius: 12px;
  background: var(--accent-bg);
  box-shadow: 0 -10px 30px rgb(0 0 0 / .35);
  pointer-events: auto;
}
.ask-bar[hidden] { display: none; }
.ask-hint { margin: 0 0 10px; font-size: 12.5px; color: var(--accent); }
.ask-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* A REAL 44px TARGET. `.btn.btn-small` measures 37.59px, which was a thumb's
   problem on a phone before and is one the client meets on every screenful
   now that the bar follows them down. A <button> centres its own label, so a
   floor on the height is the whole correction. */
.ask-actions .btn { min-block-size: 44px; }
/* READABLE WHILE DISABLED. `.btn:disabled` already takes the gold away (grey
   ground, muted ink, a border), which is the whole "cannot fire" signal; the
   `opacity: 0.55` this rule used to add on top of it composited the word to
   1.76:1 over the accent card — and Confirm is disabled until the first tick,
   so that is the state the client reads it in. Secondary ink on the same grey
   ground measures 6:1 and the box still says disabled (bugs.md, 2026-09-22). */
.ask-go:disabled { opacity: 1; color: var(--secondary); cursor: default; }

/* A 44px TARGET ON A 22px BOX. The label is the hit area and it used to
   shrink-wrap the input, so the target was the 22px box itself — under the
   mobile floor. Padding grows the label's box to 44px each way and the equal
   negative margin gives the row exactly the 22px it had, so nothing on the
   row moves and the tick is drawn where it was; only the tappable box grew
   (measured, bugs.md 2026-09-22). */
.ex-ask {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  padding: 11px;
  /* The bare `label` rules give this one 16px above and 6px below; those
     stay, minus the 11px the padding added, so the tick sits where it did. */
  margin: 5px -11px -5px;
  cursor: pointer;
}
.ex-ask input {
  /* 22px: a default checkbox is a miss waiting to happen on a phone, and this
     row is read on a phone. */
  width: 22px;
  height: 22px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.ex-row.is-askable:has([data-ask]:checked) { background: var(--accent-bg); }

/* The locked line: once, at the foot. Not a control — there is nowhere for it
   to go — so it is a paragraph that says what the feature is. */
.ask-locked {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 16px 20px 4px;
  padding: 11px 13px;
  border: 1px dashed var(--border-strong);
  border-radius: 12px;
  color: var(--muted);
  font-size: 12.5px;
}
.ask-locked-label { text-decoration: line-through; }
.ask-locked-vip {
  padding-inline-start: 8px;
  border-inline-start: 1px solid var(--border-strong);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-size: 11px;
}

/* ------------------------------------------------------------- the list */

.fc-list {
  width: min(560px, calc(100vw - 24px));
  /* Several rows, each with a question and a file picker: this one really can
     outgrow a phone, so it scrolls itself rather than pushing Send off screen.
     dvh, because the mobile URL bar is part of the height. */
  max-height: calc(100dvh - 32px);
  overflow: auto;
  overscroll-behavior: contain;
  padding: 16px;
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
}
.fc-list::backdrop { background: rgb(0 0 0 / 0.55); }

.fc-list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.fc-list-title { margin: 0; font-size: 16px; }
.fc-list-lede { margin: 6px 0 14px; font-size: 12.5px; color: var(--muted); }

.fc-rows { display: grid; gap: 12px; }
.fc-row {
  display: grid;
  gap: 9px;
  padding: 12px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface-2);
}
.fc-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.fc-row-name { font-weight: 600; font-size: 13.5px; overflow-wrap: anywhere; }
.fc-row-state { flex: 0 0 auto; font-size: 11.5px; color: var(--muted); }
.fc-row .gym-field { display: grid; gap: 5px; }
.fc-row textarea, .fc-row input[type="file"] { width: 100%; margin: 0; }

.fc-list-status { margin: 12px 0 0; font-size: 12.5px; color: var(--accent); }
.fc-list-actions { margin-block-start: 12px; }
.fc-list-actions .gym-fc-send { width: 100%; }

/* ------------------------------------------ photo sets: dates and adding
 *
 * The date is editable wherever the pane passes `editDates` — the Clients
 * pane. Generate and Review include the same partial without it and stay
 * read-only, so a coach mid-decision cannot nudge someone's timeline.
 */
.team-photoset-datef { display: flex; align-items: center; gap: 7px; }
/* A date field is typed into on a desktop and on Android, so it takes the
   16px floor like every other. The padding grows with it rather than being
   held: at 8px the box is ~37px, which is the .btn.btn-small it submits
   through, and the two finally sit on one line as the same height. */
.team-photoset-dinput {
  width: auto;
  margin: 0;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px;
}

/* Collapsed by default: the reason to open this pane is nearly always to look,
   not to add, and an upload grid open by default buries the photos. */
.team-photo-add { margin-block-start: 14px; }
.team-photo-add-head { cursor: pointer; list-style: none; }
.team-photo-add-head::-webkit-details-marker { display: none; }
.team-photo-add-head::before {
  content: "+ ";
  color: var(--accent);
  font-weight: 700;
}
.team-photo-add[open] .team-photo-add-head::before { content: "− "; }

.team-photo-add-lede { margin: 8px 0 14px; font-size: 12.5px; }
.team-photo-add-form { display: grid; gap: 14px; margin: 0; }

.team-photo-add-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.team-photo-add-meta .gym-field { display: grid; gap: 5px; }
.team-photo-add-note { flex: 1 1 220px; }
.team-photo-add-meta input { width: 100%; margin: 0; }

/* One column per pose, wrapping. `auto-fill` rather than `auto-fit` so a
   sitting with two poses does not stretch two inputs across the whole pane. */
.team-photo-add-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 10px;
}
.team-photo-add-pose {
  display: grid;
  gap: 5px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
.team-photo-add-pose-k { font-size: 11.5px; font-weight: 600; color: var(--muted); }
.team-photo-add-pose input { width: 100%; margin: 0; font-size: 11.5px; }

/* ------------------------------------------------- permissions matrix
 *
 * A grid, not a stack of forms: "what can an assistant coach do" is a COLUMN,
 * and eighteen separate little forms would make that unanswerable at a glance.
 */
.team-perm-lede { max-width: 62ch; margin-block-end: 18px; font-size: 12.5px; }

.team-perm-table {
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
.team-perm-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(2, 110px);
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-block-end: 1px solid var(--border);
}
.team-perm-row:last-child { border-block-end: 0; }
.team-perm-head {
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}
.team-perm-row:not(.team-perm-head):hover { background: var(--surface-2); }

.team-perm-cap { display: grid; gap: 2px; min-width: 0; }
.team-perm-cap-k { font-size: 13px; overflow-wrap: anywhere; }
/* What the code would say if the owner had not spoken — so a departure is
   legible as a departure rather than as the only rule there is. */
.team-perm-cap-def { font-size: 11px; color: var(--muted); }

.team-perm-cell { display: flex; align-items: center; gap: 6px; }
.team-perm-cell input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.team-perm-changed { font-size: 10px; color: var(--accent); }

.team-perm-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 16px; }

@media (max-width: 640px) {
  /* The role columns stop being columns and become labelled rows — a 110px
     column per role does not survive a phone. */
  .team-perm-row { grid-template-columns: 1fr; gap: 6px; }
  .team-perm-head { display: none; }
  .team-perm-cell::after { content: attr(data-role); font-size: 11.5px; color: var(--muted); }
}

/* ------------------------------------------------- save program, one control
 *
 * Save as PDF and Save as HTML were two buttons of equal weight in three
 * views, which asks the reader to pick a file format before deciding to save.
 * <details>, so the menu needs no script and still works with JavaScript off.
 */
.save-menu { position: relative; display: inline-block; }
.save-menu > summary.save-menu-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  list-style: none;
}
.save-menu > summary::-webkit-details-marker { display: none; }

.save-menu-caret {
  width: 0;
  height: 0;
  border-inline-start: 4px solid transparent;
  border-inline-end: 4px solid transparent;
  border-block-start: 5px solid currentColor;
  transition: transform 120ms ease;
}
.save-menu[open] .save-menu-caret { transform: rotate(180deg); }

.save-menu-panel {
  position: absolute;
  z-index: 20;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  min-width: 260px;
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.35);
}
/* In a list row the menu can be near the right edge — open it inward. */
.save-menu.is-small .save-menu-panel { inset-inline-start: auto; inset-inline-end: 0; }

/*
 * ON A PHONE IT IS A SHEET, not a dropdown.
 *
 * Measured at 390px: on the program page the panel opened 9px past the right
 * edge of the screen, and on the English account page 35px past the left — the
 * inward variant flipping the wrong way. Neither anchor can be made to fit,
 * because the panel is 260px wide and positioned against a button that can sit
 * anywhere across the row; CSS cannot clamp an absolutely positioned box to a
 * viewport it is not positioned against.
 *
 * Fixed to the bottom of the screen instead, clear of the tab bar. Two choices,
 * both fully on screen, both under a thumb — which is the same answer every
 * phone gives for a menu opened near an edge.
 */
@media (max-width: 30rem) {
  .save-menu-panel,
  .save-menu.is-small .save-menu-panel {
    position: fixed;
    z-index: 45;
    inset-inline: 12px;
    inset-block-start: auto;
    inset-block-end: calc(var(--tnav-h) + 12px);
    min-width: 0;
  }
}

.save-menu-item {
  display: grid;
  gap: 2px;
  padding: 9px 11px;
  border-radius: 9px;
  color: var(--text);
  text-decoration: none;
}
.save-menu-item:hover { background: var(--surface-2); }
.save-menu-item-k { font-size: 13px; font-weight: 600; }
.save-menu-item-hint { font-size: 11.5px; color: var(--muted); }

/* ------------------------------------------- progressive disclosure: older
 *
 * "Show request history" on /account and "Show older notifications" on
 * /notifications. Both lists showed everything a client had ever accumulated,
 * forever; both now show the recent entries and put the rest behind one
 * control that opens the LOT (owner's §16.4). Not pagination — there is no
 * second press.
 *
 * The <details> idiom the download menu uses, so it needs no script under the
 * CSP and still works with JavaScript off. The summary borrows .btn, and these
 * rules only do what a summary needs on top of it: hide the browser's own
 * marker and make it a flex row so the caret sits beside the label.
 *
 * NO `display` ON THE <details> ITSELF. A closed one hides its children
 * through ::details-content, and an author `display` rule on the element
 * defeats that — the arrow rotates and nothing collapses. That seam has cost
 * this stylesheet three separate bugs (the mobile nav, the coach's rail
 * groups, the chat viewer's 755px ghost), so the folded region here is a plain
 * .req-cards or .notify-list box and neither sets a display of its own.
 */
.disclose { margin-block-start: 12px; }

.disclose > summary.disclose-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  list-style: none;
}
/* Safari draws its own triangle without this. */
.disclose > summary.disclose-btn::-webkit-details-marker { display: none; }

/* Drawn rather than inherited, so it can rotate — ::marker is not reliably
   stylable. Direction-agnostic: it points down, then up. */
.disclose-caret {
  width: 0;
  height: 0;
  border-inline-start: 4px solid transparent;
  border-inline-end: 4px solid transparent;
  border-block-start: 5px solid currentColor;
  transition: transform 120ms ease;
}
.disclose[open] .disclose-caret { transform: rotate(180deg); }

/* ------------------------------------------------- VIP-locked action button
 *
 * The struck-through twin of a real action, matching the form-check treatment:
 * it says what the feature is rather than hiding it, and it is a <span> — there
 * is nowhere for it to go, so nothing happens on tap.
 */
/*
 * A locked VIP control: the package card's badge, borrowed.
 *
 * THE PILL IS OUT OF THE LAYOUT. Two earlier shapes put the phrase inside the
 * control and both distorted it — spelled out beside the label, two locked
 * buttons were wider than the three real ones; stacked above it, they were
 * 20px taller. Absolutely positioned on the top border, the control keeps the
 * size and shape of a live one, and only the dashed edge and the struck label
 * say it is not.
 *
 * The size is NOT restated here on purpose: `.is-vip-locked` only ever
 * decorates whatever button class the caller already uses, so a locked control
 * matches its neighbours by construction rather than by two numbers agreeing.
 */
.is-vip-locked {
  position: relative;
  border-style: dashed;
}
/* Inert only when it has nowhere to go. As a link it is a real control — the
   one thing on the page that says what to do about being locked out. */
span.is-vip-locked { cursor: default; }
a.is-vip-locked:hover { color: var(--secondary); border-color: var(--accent-border); }

/*
 * A STRUCK LABEL YOU CAN STILL READ. See decisions.md, 2026-08-21.
 *
 * This was muted text, dimmed further by the control's opacity, with a line
 * through it in the same colour — which at 13.5px in Farsi, where the line
 * crosses the letterforms rather than running under them, was close to
 * illegible. The line is the thing that should be quiet, not the words: full
 * label colour, a hairline in --muted. The same three declarations are on the
 * locked chat TAB, because "not yours yet" has to look the same everywhere.
 */
.vip-locked-label {
  color: var(--secondary);
  text-decoration: line-through;
  text-decoration-color: var(--muted);
  text-decoration-thickness: 1px;
}

/*
 * The design's "Most chosen" pill at button scale. Centred rather than inset
 * from the start edge: a card is wide enough to hang a badge off one corner, a
 * button is not, and off-centre on a 111px control reads as a mistake.
 */
.vip-locked-pill {
  position: absolute;
  top: -8px;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .06em;
  line-height: 1.5;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
/* RTL puts the same centre on the other side of the translate. */
[dir="rtl"] .vip-locked-pill { transform: translateX(50%); }

/* Unread count riding on an action button. */
.btn-count {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 11px;
  font-weight: 700;
}

/* ------------------------------------------------------------- live chat
 *
 * A messaging-app shape: the conversation fills the screen, the composer is
 * pinned under it, and the newest message is what you land on. That shape
 * because it is the one every client already knows.
 */
/*
 * THE LOG IS SIZED, NOT THE PAGE.
 *
 * This was a viewport-height flex column with the log absorbing the slack —
 * which pins the composer beautifully, and then collapses the log to a band
 * the moment anything opens beneath it. The daily-report form is 755px tall;
 * with it open the conversation was 302px of a 720px screen.
 *
 * The first attempt at fixing that added a :has() rule to let the box grow.
 * docs/inventory.md already records why that is wrong here — :has() renders
 * WRONG rather than degrading on the older Android browsers in this client
 * base — so the rule was removed, and the squeeze came back with it.
 *
 * Sizing the LOG directly needs neither. It keeps its height whatever else is
 * on the page; the report form and the fortnight table simply make the page
 * taller and it scrolls, which is what a page does. There is no ancestor rule
 * to get wrong.
 *
 * dvh because the mobile URL bar is part of the height.
 */
.chat { display: flex; flex-direction: column; }

/* Nothing under a full-screen conversation. Instagram links below a chat
   window are the sort of thing that makes an app feel like a web page.
   Page classes, not :has(), per docs/inventory.md. */
main.page-vipchat ~ .site-footer { display: none; }

/*
 * The chat page gives its space to the conversation rather than to margins —
 * and stops stretching itself.
 *
 * `main` is `flex: 1` inside a viewport-height column so the footer sits at
 * the bottom of a short page. There is no footer here, so that stretch only
 * pushed the page one header-height past the screen and made it scroll for
 * nothing.
 */
main.page-vipchat {
  padding-block: 14px;
  flex: 0 0 auto;
}

/* The status line is empty almost always, and a reserved line costs the log
   12px permanently to hold a sentence that appears while a photo is being
   prepared and then goes away again. */
.chat-status:empty { display: none; }

.chat-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-block-end: 12px;
}
.chat-title { margin: 0; font-size: 20px; }
.chat-sub { margin: 4px 0 0; font-size: 12.5px; max-width: 52ch; }

/* The head gives its spare space to the conversation. */
.chat-head { margin-block-end: 8px; }

.chat-log {
  /*
   * A DEFINITE HEIGHT — see the note on .chat. This is what lets the report
   * form open without eating the conversation.
   *
   * Sized from the chrome that is actually around it rather than as a fraction
   * of the viewport: a fraction either leaves a strip of dead space or pushes
   * the composer off the bottom, and which one you get depends on the screen.
   * The subtraction below is every fixed piece on this page, measured:
   *
   *   59  site header        56  the chat's own head
   *   28  main padding        58  the report / history row
   *   77  composer            55  the training bar's spacer
   *   28  main padding again — it is above AND below
   *
   * Capped so the conversation does not become a field on a tall desktop
   * monitor, and floored so it is still a conversation on a small phone.
   */
  height: calc(100dvh - 361px);
  min-height: 240px;
  max-height: 620px;
  flex: 0 0 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
.chat-empty { margin: auto; max-width: 40ch; text-align: center; font-size: 13px; }

/*
 * THE STATED EMPTY, WHERE THE SCROLLER WAS.
 *
 * A closed chat with nothing in it drew the box above as 483px of border
 * around zero characters (bugs.md, 09-14). The template does not render the
 * log at all in that case; this is what stands in its place.
 *
 * The design file's own shape (docs/design/canvas/sobati-fitness.dc.html,
 * `detailEmptyStyle`): a fact, a sentence, room around them, centred. Centred
 * also means the box is the same in both directions, so there is nothing here
 * for RTL to mirror wrong — every value below is logical or symmetric.
 *
 * A card rather than the file's borderless pane: the owner's call, 09-22.
 */
.chat-quiet {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-block-start: 6px;
  padding: 40px 22px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  text-align: center;
}
.chat-quiet-title { margin: 0; font-size: 16px; line-height: 1.35; color: var(--text); text-wrap: balance; }
.chat-quiet-body  { margin: 0; max-width: 42ch; font-size: 13.5px; line-height: 1.55; color: var(--secondary); text-wrap: pretty; }
.chat-quiet-act   { margin: 6px 0 0; width: 100%; }

.chat-msg { display: flex; justify-content: flex-start; }
/*
 * Side carries WHOSE MESSAGE IT IS FROM WHERE YOU SIT, not who sent it.
 *
 * The client's page marks their own messages `is-mine`; the coach's pane marks
 * the coach's. Both then render right-aligned and accented, which is what
 * every messaging app trains people to read as "me" — and one set of rules
 * serves both ends instead of two that must be kept in step.
 */
.chat-msg.is-mine { justify-content: flex-end; }

.chat-bubble {
  max-width: min(78%, 46ch);
  padding: 9px 12px;
  border-radius: 16px;
  background: var(--surface-2);
}
.chat-msg.is-mine .chat-bubble {
  background: var(--accent-bg);
  border: 1px solid var(--accent-border);
}
.chat-who { margin-block-end: 3px; font-size: 11px; font-weight: 700; color: var(--accent); }
.chat-text { margin: 0; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; white-space: pre-wrap; }
.chat-when { font-size: 10.5px; color: var(--muted); }

/* The timestamp and its receipt, on one line under the message. */
.chat-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
  margin-block-start: 4px;
}
.chat-msg.is-coach:not(.is-mine) .chat-foot,
.chat-msg.is-client:not(.is-mine) .chat-foot { justify-content: flex-start; }

/*
 * Sent · delivered · read.
 *
 * One tick, two ticks, two ticks in the accent — the vocabulary everyone
 * already has. The states mean something specific here (see migration 041):
 * delivered is recorded only when a notification channel actually accepted the
 * message, so the second tick is never a guess.
 */
.chat-tick {
  /*
   * POSITIONED, so the screen-reader label inside it cannot escape.
   *
   * `.visually-hidden` is `position: absolute`, and with no positioned
   * ancestor it resolves against the initial containing block — so the word
   * "Read" on a receipt deep inside the scrolling log landed at y=1011 on a
   * 720px screen and dragged 291px of empty scroll onto the page. The chat
   * looked short because the page around it was long for no visible reason.
   *
   * One line, and it also stops every future absolutely-positioned child of a
   * tick doing the same thing.
   */
  position: relative;
  font-size: 10.5px;
  line-height: 1;
  letter-spacing: -2px;
  color: var(--muted);
}
.chat-tick.is-read { color: var(--accent); }

/* A line the app is saying, not a person: the report ask reads as neither
   side's voice, so it is set apart from the words around it. */
.chat-system { font-style: italic; color: var(--muted); font-size: 13px; }

.chat-img { display: block; margin-block-end: 6px; }
.chat-img img {
  display: block;
  max-width: 100%;
  max-height: 320px;
  border-radius: 10px;
}

/* The movement a form check is about. Set apart from the note under it,
   because the exercise IS the question — a form check without one is just a
   message. dir=auto on the element handles a Farsi thread naming a lift in
   Latin, which is most of them. */
.chat-formcheck-ex {
  margin: 0 0 6px;
  font-weight: 600;
  font-size: 15px;
}

/* Sized like the image above rather than left to the intrinsic frame size: a
   portrait phone clip is 1080 wide and would otherwise burst the bubble. */
.chat-video {
  display: block;
  max-width: 100%;
  max-height: 320px;
  margin-block-end: 6px;
  border-radius: 10px;
  background: #000;
}

/* What the Send button is about to close, above the composer. Reads as a note
   rather than a warning: it is telling the coach what will happen, not
   stopping them. */
.team-chat-answering {
  margin: 0 0 6px;
  font-weight: 600;
}

.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin: 10px 0 0;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}

.chat-attach {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  cursor: pointer;
}
.chat-attach:hover { border-color: var(--accent); color: var(--accent); }
.chat-attach-icon { font-size: 19px; line-height: 1; }
/* A real input, kept operable by the keyboard — not display:none, which takes
   it out of the tab order and out of the accessibility tree. */
.chat-attach input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* 16px floor: composing a message to your coach zoomed the page too. The
   padding comes down from 9px to 6px so the composer keeps its shape — at
   16px on the body's 1.6 line-height one row is 25.6px, and 25.6 + 12 is the
   38px of the attach and send buttons it sits between. */
.chat-input {
  flex: 1 1 auto;
  min-height: 38px;
  max-height: 140px;
  margin: 0;
  padding: 6px 11px;
  border: 0;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 16px;
  resize: none;
}
.chat-input:focus { outline: 2px solid var(--accent-border); outline-offset: 1px; }

.chat-send {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
}
.chat-send:disabled { opacity: .55; cursor: default; }

.chat-status { margin: 6px 2px 0; min-height: 1em; font-size: 11.5px; color: var(--muted); }

/*
 * Where the composer was, once the block has lapsed.
 *
 * Sized and spaced like the composer it replaces rather than like a notice, so
 * the conversation above it keeps the same shape and the eye does not read the
 * page as broken — the chat is not gone, only the box for adding to it.
 */
.chat-closed {
  margin: 10px 0 0;
  padding: 12px 14px;
  border: 1px dashed var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  text-align: center;
}
.chat-closed p { margin: 0; font-size: 12.5px; }
.chat-closed p + p { margin-block-start: 14px; }

/*
 * THE NOTICE IS THE ONLY SENTENCE ON THE SCREEN, SO IT IS BODY TEXT.
 *
 * `--muted` #5f6875 on `--surface` #171a21 is 3.09:1 at 12.5px, under the
 * 4.5:1 AA floor, and here that sentence is the entire message rather than a
 * caption beside something. `--secondary` #9aa3af on the same ground is
 * 6.82:1 — it is what this app already calls body text on a card.
 */
.chat-closed .chat-closed-text { font-size: 13.5px; line-height: 1.55; color: var(--secondary); }

/*
 * THE ONE ACTION ON THE SCREEN IS SIZED LIKE ONE.
 *
 * AGENTS.md: a hit target is >= 44px on mobile. This one measured 37.59px,
 * box 763.8-801.4 against a `.tnav` fixed at y=776, so 25.4px of it was
 * covered at first paint and `elementFromPoint` at the button's own centre
 * returned `tnav-item`. NOT a change to `.btn-small` (130 uses across 47
 * templates, its own open item) — these two buttons stop being small.
 */
.chat-quiet-act .btn,
.chat-closed .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 44px;
  padding-inline: 22px;
  font-size: 14px;
  /*
   * AND THE KEYBOARD'S OWN SCROLL CLEARS THE BAR TOO.
   *
   * Sequential focus scrolls the button just far enough to be in the viewport,
   * and `.tnav` is fixed OVER the bottom of that viewport — so "in view" and
   * "not under the bar" are different claims, and on a 640px-tall phone with a
   * conversation on screen the browser was satisfied by the first while
   * `elementFromPoint` at the button's centre still returned `tnav-label`.
   * AGENTS.md: a sticky or fixed element never covers focus.
   *
   * The bar's own height is now `scroll-padding` on <html> for every screen
   * that draws the bar (see `.tnav`); this margin is the air only, because the
   * two add up. Measured at 360x380 (a 640 phone with its keyboard open),
   * Farsi: the margin alone left the focused button 65px under the bar — the
   * browser scrolls nothing for a control it already counts as visible — and
   * the padding on the scrollport is what moves it clear.
   */
  scroll-margin-block-end: 12px;
}
/* Full width on a phone, where a centred pill in a 358px card looks stranded.
   Its own width above that, where a 1100px button is not a button. */
@media (max-width: 560px) {
  .chat-quiet-act .btn,
  .chat-closed .btn { display: flex; width: 100%; }
}

/*
 * AND IT SAYS WHERE THE KEYBOARD IS.
 *
 * This stylesheet has no global `a:focus-visible` — each component states its
 * own — so these two anchors were falling through to the UA's `outline: auto`,
 * which computes to rgb(16, 16, 16) here: a near-black ring on brand gold, and
 * nothing at all where it overhangs the card. The house ring is the accent,
 * and `outline-offset` puts it on the surface BESIDE the gold rather than on
 * top of it, where gold-on-gold would be no ring either.
 */
.chat-quiet-act .btn:focus-visible,
.chat-closed .btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/*
 * WITH A SCROLLBACK, THE LOG GIVES BACK WHAT THE COMPOSER WAS BUDGETED.
 *
 * The live height is calc(100dvh - 361px) and 77px of that subtraction is a
 * composer a closed chat never draws. What it draws instead is the notice
 * above, ~160px at its ENGLISH length — the long one, four lines at 390px
 * against Farsi's three. 444 = 361 - 77 + 160. Without this the taller button
 * lands 65.5px under the tab bar instead of clearing it; with it the log is
 * 400px at 390x844 and the button is fully uncovered at first paint.
 */
.chat.is-closed .chat-log { height: calc(100dvh - 444px); }

/*
 * The VIP mark.
 *
 * On the LIVE buttons, not only the struck-out ones. The striped version tells
 * a standard client what they are missing; without this, a client who is
 * actually paying for the tier has no way to tell which controls their money
 * bought. Same three letters either way, so the two read as one idea.
 *
 * A mark, not a badge with a count — it says what kind of thing this is, and
 * it must not compete with the label it sits beside.
 */
.vip-mark {
  display: inline-flex;
  align-items: center;
  margin-inline-end: 6px;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: .06em;
  line-height: 1.5;
  /* Latin three-letter mark: it must not flip or reshape in a Farsi page. */
  direction: ltr;
  unicode-bidi: isolate;
  vertical-align: middle;
}
.vip-mark.is-small { font-size: 8.5px; padding: 0 4px; margin-inline-end: 4px; }

/* ------------------------------------------------- the daily report, in chat
 *
 * The card is the message. A report bubble carries no prose of its own, so the
 * grid has to read as a finished thing rather than as a table that lost its
 * header.
 */
.chat-report {
  min-width: min(260px, 60vw);
  border-radius: 10px;
}
.chat-report-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-block-end: 7px;
}
.chat-report-k {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent);
}
.chat-report-date { font-size: 11px; color: var(--muted); }

.chat-report-grid {
  display: grid;
  /* Two columns on a phone, more when there is room. Five short facts read as
     a block; one per line reads as a list of things you had to answer. */
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 8px 12px;
  margin: 0;
}
.chat-report-cell { margin: 0; }
.chat-report-cell dt { font-size: 10.5px; color: var(--muted); }
.chat-report-cell dd { margin: 1px 0 0; font-size: 13.5px; font-weight: 600; }
.chat-report-note {
  margin: 8px 0 0;
  padding-block-start: 7px;
  border-block-start: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.chat-report-gone { margin: 0; font-size: 12.5px; }
.chat-report-cta { margin-block-start: 8px; }

/* The client's form, shut by default and opening in place above the composer:
   the chat is the page, and answering a question asked here should not be a
   trip somewhere else. */
/* The two controls between the conversation and the composer, on one line. */
.chat-tools {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 8px;
  margin-block-start: 10px;
}
/* Open, either panel takes the whole row. The report is a six-field form and
   the history is a six-column table; sized to their summary buttons instead,
   both hang off the left edge in a column narrower than their content. */
.chat-tools > .chat-report-form[open],
.chat-tools > .chat-history[open] {
  flex: 1 1 100%;
  /*
   * min-width: 0, or the table inside refuses to be narrower than its own
   * content and pushes the WHOLE PAGE sideways on a phone. A flex item's
   * min-width is `auto` by default, which means "never smaller than my
   * content" — so the overflow-x:auto box below never got the chance to
   * scroll, because it was never over its own width.
   */
  min-width: 0;
}

.chat-report-form { margin-block-start: 0; }

/* ------------------------------------------------- the client's own history
 *
 * Their last fortnight, read from the record rather than the thread. Shut by
 * default and out of the way: this is a chat, and a table that greets you
 * every time you open a conversation is a table you stop reading.
 */
.chat-history-open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  list-style: none;
  cursor: pointer;
}
.chat-history-open::-webkit-details-marker { display: none; }

/* The table scrolls inside its own box. Six columns will not fit a phone, and
   a page that scrolls sideways is worse than a table that does. */
.chat-history-scroll {
  overflow-x: auto;
  min-width: 0;
  max-width: 100%;
  max-height: 40vh;
  overflow-y: auto;
  margin-block-start: 10px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
.chat-history-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  white-space: nowrap;
}
.chat-history-table th,
.chat-history-table td {
  padding: 7px 12px;
  text-align: start;
  border-block-end: 1px solid var(--border);
}
.chat-history-table thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--surface-2);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.chat-history-table tbody tr:last-child th,
.chat-history-table tbody tr:last-child td { border-block-end: 0; }
.chat-history-table tbody th { font-weight: 600; color: var(--muted); }

/*
 * WITH THE FORM OPEN, THE PAGE GROWS — and it does so with no rule of its own.
 *
 * Because .chat-log is sized directly (see above) rather than absorbing the
 * slack in a viewport-height box, anything that opens between the log and the
 * composer simply makes the page taller. The first cut needed a :has() rule to
 * undo a fixed height; docs/inventory.md already records :has() as the wrong
 * tool for layout here, since it renders WRONG rather than degrading on the
 * older Android browsers plausible in Iran. The rule that is not there is the
 * one that cannot break.
 */
.chat-report-open,
.chat-history-open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  list-style: none;
  cursor: pointer;
}
.chat-report-open::-webkit-details-marker { display: none; }
/* The house caret, and the house's flip: .disclose[open] does this for its own
   disclosures, and this <details> is not one of them. */
.chat-report-form[open] .disclose-caret { transform: rotate(180deg); }
.chat-report-flag {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 10.5px;
  font-weight: 700;
}
.chat-report-fields {
  display: grid;
  /*
   * Six short answers. auto-fit means one column on a narrow phone and a tidy
   * row of five on a desktop without a breakpoint of its own — the same grid
   * the intake form uses.
   *
   * 160px, not 148px. A `<input type="date">` renders 150px wide of its own
   * accord, so a 148px minimum was designed-in clipping — and it put the two
   * commonest iPhones on different layouts, one column at 375 and two at 390
   * with 15px between them. The phone sheet below sets its own two columns; up
   * here the minimum only has to be honest about the widest control.
   */
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 14px;
  margin-block-start: 10px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
/*
 * The fields.
 *
 * Borrowing the app's OWN vocabulary rather than inventing one: the same
 * small-caps label as gym mode's set fields (.gym-field-k) and the same input
 * treatment as every other form. The first cut used a `.field-k` class that
 * exists nowhere in this stylesheet, so the labels rendered as unstyled body
 * text — which is what "make the fields look designed" was pointing at.
 *
 * This form has NO design of its own; see docs/inventory.md. It follows the
 * house rules until it gets one, which is a deliberately conservative choice:
 * inventing a new look for a screen a designer will revisit means throwing
 * two things away instead of one.
 */
.chat-report-fields .chat-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-block: 0;
}
/* Controls line up with each other, not with their own labels — a one-line
   label beside a wrapped one otherwise puts two inputs on different lines. */
.chat-report-fields .chat-field > input,
.chat-report-fields .chat-field > select,
.chat-report-fields .chat-field > textarea {
  margin-block-start: auto;
  max-width: none;
  height: 44px;
}
.chat-report-fields .chat-field > textarea { height: auto; min-height: 62px; }

/* The free-text question spans the row: "anything else going on" is the one
   answer that is a sentence rather than a number. */
.chat-report-fields .chat-field.is-wide { grid-column: 1 / -1; }
.chat-report-actions {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.chat-report-hint { margin: 0; font-size: 11.5px; max-width: 44ch; }

/*
 * THE SAME DAY, IN THE READER'S OWN CALENDAR.
 *
 * `<input type="date">` draws its value in the BROWSER's locale and cannot be
 * told otherwise — so a Farsi client picking a day saw `08/21/2026` beside
 * chat bubbles dated `1405-05-30`, and an English one cannot tell 08/09 from
 * 09/08 without knowing which locale their phone is in. The line under the
 * field is the same date through the app's own date helper, which is what
 * every other date on the screen already went through.
 */
.chat-field-note {
  display: block;
  margin-block-start: 4px;
  font-size: 11.5px;
  color: var(--muted);
}

/*
 * A refused field says so under itself.
 *
 * A <span> and not the <p> the rest of the app uses, because these sit INSIDE
 * their <label> — that is what makes the label wrap the control, and a <p>
 * inside a label is not allowed there. So it takes the block display the
 * element does not have, and gives back the top margin: .chat-field is a flex
 * column with its own 5px gap, and the two together read as a gap and a half.
 */
.chat-report-fields .field-error {
  display: block;
  margin: 0;
}

/*
 * ------------------------------------------------ THE REPORT AS A BOTTOM SHEET
 *
 * On a phone the open form was a trapdoor. Opening it grew the page from 831
 * to 1562 with the scroll left where it was, so 140px of a 789px form was on
 * screen, its first field sat BEHIND the fixed tab bar, and Send was 477px
 * below the fold. When the coach asks for a report the form opens by itself —
 * so the thing that opened was, at that moment, entirely off-screen.
 *
 * It is a sheet at the bottom of the viewport instead: the shape a phone
 * already uses for "answer this now", sitting ON the tab bar's top edge rather
 * than behind it, scrolling inside itself.
 *
 * STILL A <details>. `<dialog>.showModal()` needs JavaScript, and this form has
 * to work without it — that is the whole reason the report lives in a
 * disclosure and not in a modal. The <summary> becomes the sheet's own header:
 * the control that opened it is the control that closes it, which is the only
 * dismissal a no-script sheet can have.
 */
@media (max-width: 599px) {
  .chat-report-form[open] {
    position: fixed;
    inset-inline: 0;
    /* ON the bar, never behind it — and above the home indicator on an
       iPhone, which sits inside the viewport. */
    inset-block-end: calc(var(--tnav-h) + env(safe-area-inset-bottom, 0px));
    z-index: 45; /* the tab bar is 40 */
    max-block-size: 78dvh;
    overflow-y: auto;
    /* A flick that reaches the end of the sheet must not start scrolling the
       conversation behind it. */
    overscroll-behavior: contain;
    padding: 0 14px 18px;
    border-block-start: 1px solid var(--border-strong);
    border-start-start-radius: 16px;
    border-start-end-radius: 16px;
    background: var(--surface);
    box-shadow: 0 -14px 34px rgba(0, 0, 0, .45);
  }

  /* The header of the sheet, and its only way out. Full-bleed and opaque, so
     the fields scroll under it rather than through it. */
  .chat-report-form[open] > .chat-report-open {
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
    margin: 0 -14px;
    padding: 14px;
    border: 0;
    border-radius: 0;
    box-shadow: inset 0 -1px 0 var(--border);
    background: var(--surface);
  }

  /*
   * The sheet IS the card, so the card inside it goes. Two rounded panels one
   * inside the other cost 33px of chrome on each side of a 390px screen.
   */
  .chat-report-form[open] .chat-report-fields {
    /* Two columns, decided rather than fitted: auto-fit put 375px and 390px on
       different layouts, and a client should not get a different form for
       owning last year's phone. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin-block-start: 0;
    padding: 0 0 4px;
    border: 0;
    background: none;
  }

  /*
   * Two of the six answers take the whole row. Day because its control is a
   * date input, which will not go under 150px; Weight because five fields in
   * two columns otherwise leave one cell empty, and an empty cell beside the
   * last answer reads as a field that failed to render.
   *
   * A class on the label rather than :has() — docs/inventory.md records why
   * :has() is the wrong tool for layout here.
   */
  .chat-report-fields .chat-field.is-phone-wide { grid-column: 1 / -1; }

  /*
   * SEND RIDES THE BOTTOM OF THE SHEET.
   *
   * The sheet is capped at 78dvh, and on a 667px-tall phone — the iPhone SE
   * and 8, the commonest 375-wide devices — that is 520px against 623px of
   * form. QA measured the result: six pixels of a thirty-eight pixel button
   * visible, the rest reachable only by scrolling inside the sheet. Raising
   * the cap would fix that height and break at the next one, because the
   * form's height is content, not a constant.
   *
   * Sticking the action row to the sheet's own bottom edge is height-proof:
   * the fields scroll under it and the one thing the sheet exists to do is
   * always on screen. It is the mirror of the summary, which is already
   * sticky at the top — open it and both ends stay put while the middle
   * moves.
   */
  .chat-report-form[open] .chat-report-actions {
    position: sticky;
    inset-block-end: 0;
    /* Opaque, or the fields scroll visibly through the button. */
    background: var(--surface);
    padding-block: 10px;
    /* The hairline says there is more above, once anything has scrolled. */
    box-shadow: 0 -1px 0 var(--border);
  }
}

/* The queue's sort control (§20.11): a quiet row of links under the list
   head, the hsort idiom at queue scale. Quieter than .team-box on purpose —
   an order is a reading preference, not a place to go. */
.team-sortrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  padding: 8px 14px 0;
  font-size: 11.5px;
}
.team-sort-k { color: var(--muted); }
.team-sortrow .team-sort {
  color: var(--muted);
  font-weight: 600;
  text-decoration: none;
  padding: 2px 6px;
  border-radius: 7px;
}
.team-sortrow .team-sort:hover { color: var(--text); }
.team-sortrow .team-sort.is-current {
  color: var(--text);
  background: var(--accent-bg);
}

/* -------------------------------------------------- coach side of the chat */

/* Two boxes on the Messages tab: the contact queue and live VIP chat. Real
   links, like the tab rail above them. */
.team-boxes {
  display: flex;
  /*
   * WRAP, because this row is no longer always two boxes. The Articles tab
   * (§22) reuses it for a FOUR-way status filter, and four labels do not fit
   * the 273px list column: measured at 1440 the row needed 288px in English
   * and 331px in Farsi, so the last chip was drawn 15px past the column in
   * English and 58px past it in Farsi — where the overflow runs the other way
   * and put "منتشرشده" on top of the tab rail. Nothing clipped it; the row
   * simply had nowhere to go, which is the case `flex-wrap` is for.
   */
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 12px 0;
}
.team-box {
  /*
   * `flex: 1 1 0` is kept, and it is what makes the Messages tab's two boxes
   * exact halves rather than two content-sized chips. With `flex-wrap` above,
   * the LINE BREAK is decided by each box's min-content — so nothing that
   * fits today moves (four across at 390 and at 768) and only the narrow list
   * column, where they never fitted, breaks: 3 + 1 in both languages.
   */
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.team-box:hover { border-color: var(--accent-border); color: var(--text); }
.team-box.is-current {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--text);
}
.team-box-n {
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 10.5px;
  font-weight: 700;
}

/* The thread inside the working area. Bounded rather than page-height: the
   pane has a head, a reply box and a sidebar to share the screen with. */
.chat-log.is-pane {
  max-height: min(52vh, 460px);
  margin-block-end: 12px;
}

.team-chat-reply {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
}
.team-chat-reply textarea { margin: 0; max-width: none; }
.team-chat-reply-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.team-chat-attach { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
.team-chat-attach input[type="file"] { max-width: 190px; font-size: 11px; }

/* "Request daily report" — a button that opens to reveal an optional note.
   A real box, not display:contents: a <details> with no box of its own cannot
   be collapsed by the browser at all. */
/* A real box, not display:contents: a <details> with no box of its own cannot
   be collapsed by the browser at all. */
.team-ask-report { position: relative; }
.team-ask-report > summary { list-style: none; cursor: pointer; }
.team-ask-report > summary::-webkit-details-marker { display: none; }

/*
 * FLOATS over the pane rather than expanding in the row.
 *
 * In flow it pushed "Open client" sideways and reflowed the whole head the
 * moment the note was opened — a menu that moves the button beside it is a
 * menu that makes you re-aim. Same treatment as the save menu, which is the
 * other <details>-as-menu in the app.
 */
.team-ask-report-form {
  position: absolute;
  z-index: 20;
  inset-block-start: calc(100% + 6px);
  /* Near the right edge of the pane, so it opens inward. */
  inset-inline-end: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(300px, 70vw);
  padding: 10px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.35);
}
.team-ask-report-form textarea { margin: 0; max-width: none; }

/* ----------------------------------------------------------- the tab bar
 *
 * Home · Program · Gym · Records · Chat, fixed to the bottom of every
 * signed-in client screen. See _tab-bar.eta for why it exists; in short, these
 * screens are used in a loop and every one of them used to hide its exits at
 * whichever end of the page the content happened to stop. It was on the four
 * training screens only until 2026-08-20, which split the app's navigation in
 * two — the class name is `tnav` from that era and is left alone rather than
 * renamed across a stylesheet, five templates and a set of tests.
 *
 * Bottom, not top: this is used one-handed in a gym, and the bottom of a phone
 * is where a thumb already is.
 */
/*
 * THE BAR NEVER COVERS WHAT THE BROWSER SCROLLS INTO VIEW. `scroll-padding`
 * on the document scrollport is read from <html> and nowhere else, and a
 * fixed bar over the foot of the viewport is exactly what it exists for: it
 * takes the bar's band out of the region the browser measures "visible"
 * against, so sequential focus, `scrollIntoView` and an anchor jump all stop
 * above it. `scroll-margin` on the target does NOT do this — a control the
 * browser already counts as visible is not scrolled at all, and one under
 * the bar counts as visible (bugs.md 2026-09-14, then the closed chat,
 * 2026-09-22). Scoped by `:has` to the screens that draw the bar; the Confirm
 * bar's `:root.is-asking` padding is wider and outranks it while picking.
 */
html:has(.tnav) { scroll-padding-block-end: calc(var(--tnav-h) + env(safe-area-inset-bottom, 0px)); }
.tnav {
  position: fixed;
  z-index: 40;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  min-height: var(--tnav-h);
  box-sizing: border-box;
  border-block-start: 1px solid var(--border);
  /* Not transparent: the page scrolls underneath it. */
  background: rgba(18, 21, 26, .97);
  backdrop-filter: blur(10px);
  /* The iPhone home indicator sits inside the viewport, so a bar flush to
     `bottom: 0` puts its labels under it. */
  padding-block-end: env(safe-area-inset-bottom, 0);
}

.tnav-item {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 54px;
  padding: 7px 4px;
  color: var(--muted);
  text-decoration: none;
  font-size: 11px;
  /* A tap that lands between two tabs should do nothing rather than guess. */
  -webkit-tap-highlight-color: transparent;
}
.tnav-item:hover { color: var(--text); }

.tnav-item.is-here { color: var(--accent); }
/* The marker is a bar at the top edge of the tab, not a background fill: a
   filled tab at the bottom of a dark page reads as a button that has been
   pressed and stuck. */
.tnav-item.is-here::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 22%;
  height: 2px;
  border-radius: 0 0 2px 2px;
  background: var(--accent);
}
.tnav-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/*
 * The chat tab, for clients who do not have one: struck through with a VIP
 * tag rather than omitted — the same treatment the program view gives its
 * locked buttons, so "not yours yet" looks the same everywhere it appears.
 */
.tnav-item.is-locked { color: var(--muted); cursor: default; }
/* The same struck label the program foot uses, and legible for the same
   reason: the line is muted, the words are not. */
.tnav-item.is-locked .tnav-label {
  color: var(--secondary);
  text-decoration: line-through;
  text-decoration-color: var(--muted);
  text-decoration-thickness: 1px;
}
.tnav-viptag {
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent-dim);
}

.tnav-icon { display: flex; }
.tnav-label { line-height: 1.1; }

/* An unread message, on the tab. A client in gym mode should not have to open
   the chat to find out their coach answered. */
.tnav-dot {
  position: absolute;
  inset-block-start: 9px;
  inset-inline-start: calc(50% + 9px);
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

/*
 * Room for it. Without this the bar covers the last thing on the page — the
 * last exercise of the last day, or the chat composer.
 *
 * On the BAR itself rather than on main via :has(). docs/inventory.md records
 * that :has() renders wrong rather than degrading on the older Android
 * browsers in this client base, and a page whose last control is unreachable
 * is exactly the failure that matters. A spacer element cannot misbehave.
 */
/* The iPhone's home indicator sits inside the viewport and the bar pads itself
   past it, so the reservation has to include it too. */
.tnav-spacer { height: calc(var(--tnav-h) + env(safe-area-inset-bottom, 0px)); }

/* =========================================================================
 * Rules for classes that were in the markup with nothing behind them.
 *
 * Found by `npm run check`. Each one below had a VISIBLE consequence — an
 * element rendering as unstyled body text where the design meant something
 * else. The ones that were merely redundant hooks on an already-styled
 * element were deleted from the templates instead; a class that does nothing
 * is not worth a rule to make it do nothing.
 * ========================================================================= */

/* A success notice in the coach panes. Its sibling `.team-warn` was styled and
   this was not, so "Saved" rendered as bare text under the pane title while a
   failure got a proper box — the two outcomes of the same action looking like
   different kinds of thing. Mirrors .team-warn in the success palette. */
.team-ok {
  margin-block-start: 11px;
  padding: 10px 12px;
  border: 1px solid var(--success-border);
  border-radius: 9px;
  background: var(--success-bg);
  color: var(--success);
  font-size: 12px;
  line-height: 1.55;
  font-weight: 600;
}

/* A chip that is a statement rather than a status — "already sent", "cannot
   deliver yet". Without this it was indistinguishable from an active chip, so
   a locked action read as an available one. */
.team-chip.is-muted {
  border-color: var(--border);
  background: transparent;
  color: var(--muted);
  font-weight: 500;
}

/* The coach layout's way back to the dashboard. */
.coach-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--secondary);
  text-decoration: none;
}
.coach-back:hover { color: var(--accent); }

/* ------------------------------------------------------- the block check-in
 *
 * A whole VIP screen with no styles at all: Framework §4b writes the next
 * block from these answers, and it was rendering as a bare stack of headings
 * and inputs on the page's default width.
 *
 * Built from the intake's own vocabulary rather than a new invention — it is
 * the same kind of screen (a short form a client fills between blocks) and
 * should not look like a different product.
 */
.checkin { max-width: 46rem; margin-inline: auto; }
.checkin-head { margin-block-end: 1.6rem; }
.checkin-form { display: flex; flex-direction: column; gap: 1.6rem; }

.checkin-group {
  padding: 1.1rem 1.2rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
}
.checkin-group > h2 {
  margin: 0 0 .2rem;
  font-size: 1.02rem;
}
.checkin-group > .field-help { margin-block-end: .9rem; }

/* ---------------------------------------------------- the permissions matrix
 *
 * The pane borrowed three class names from a naming scheme that does not
 * exist in this stylesheet, so the owner-only screen for granting capabilities
 * rendered with no container, no title treatment and no spacing.
 */
.team-detail-body { max-width: 54rem; }
.team-detail-title { margin: 0 0 .3rem; font-size: 1.05rem; }
.team-perm-lede { margin-block-end: 1.1rem; }
.team-perm-form { display: flex; flex-direction: column; gap: 1rem; }

/* ------------------------------------------------------- the form-check page
 *
 * The standalone page (also the destination of the batch flow from the
 * program view). Its wrapper and its form had no rules, so the page ran the
 * full width of the viewport with no rhythm between the fields.
 */
.review-page { max-width: 46rem; margin-inline: auto; }
.review-form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  padding: 1.2rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
}

/*
 * The form-check dialog's three fields.
 *
 * `.gym-field` is sized for gym mode's number boxes — `flex: 0 0 74px`, wide
 * enough for a weight. These three hold an exercise name, a question and a
 * file picker, and inherited that 74px basis: the exercise input was a
 * thumbnail of an input.
 */
.gym-fc-ex,
.gym-fc-note,
.gym-fc-video { flex: 1 1 100%; }
.gym-fc-note textarea,
.gym-fc-ex input { width: 100%; }

/* The "prices shown for Iran, going by your phone number" line. Sits between
   the market switch and the price grid, so it reads as a caption on the prices
   rather than another instruction — quieter than the help text above it,
   because it is an admission rather than a question. */
.request-guess {
  margin-block: .35rem 1.1rem;
  font-size: .85rem;
}

/*
 * A CLOSED <details> HIDES ITS CONTENTS. Say so explicitly.
 *
 * The browser hides a closed details' content through the `::details-content`
 * slot, and a child with its own `display` can still end up with a laid-out
 * box: the closed report form measured 58px while the grid inside it reported
 * 755px and dragged the page 291px past the bottom of the screen. The
 * conversation looked short because the page around it was long.
 *
 * This codebase has now hit the same seam three times — `display: contents` on
 * a details broke collapsing outright, and a child's `display` has twice
 * outranked the UA's hiding. The rule is cheap and it ends the category.
 */
.chat-report-form:not([open]) .chat-report-fields,
.chat-history:not([open]) .chat-history-scroll { display: none; }

/*
 * A FINISHED SET, struck through.
 *
 * A client mid-workout is looking for one thing: which row is next. Reading
 * that off the presence of a small "saved" pip means scanning for something
 * tiny; a line across the whole row answers it from arm's length, which is the
 * distance a phone sits at on a bench.
 *
 * The numbers stay legible — this dims and marks, it does not hide. What was
 * lifted is the record, and a client checking last set's weight before the
 * next one should not have to undo anything to read it.
 */
.gym-set-form.is-done,
.gym-ss-body.is-done { position: relative; opacity: .62; }

/*
 * A TICK, NOT A LINE. The first cut drew a strikethrough across the whole row,
 * and the owner was right that it read as deletion — a struck-out set looks
 * like a set that should not have happened, when what it is is a set in the
 * bag. The tick says finished the way the rest of the app already says it
 * (the chat receipts use the same mark), sitting in the row's end padding on
 * the row's own centre line.
 */
.gym-set-form.is-done::after,
.gym-ss-body.is-done::after {
  content: "✓";
  position: absolute;
  inset-inline-end: 2px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  pointer-events: none;
}

/* Touching the row to correct it lifts the dimming out of the way; the tick
   stays, because the set is still logged while it is being edited. */
.gym-set-form.is-done:focus-within,
.gym-ss-body.is-done:focus-within { opacity: 1; }

/*
 * ONE TICK PER ROW. The autosave pip's "saved" state is itself a ✓, so a row
 * that finished showed two — one green beside the fields, one gold at the end.
 * The row-level tick wins and the pip yields, but only its OK state: "saving"
 * and "held on this device" still matter on a finished row, because a queued
 * set is not a saved one and the tick alone would overstate it.
 */
.gym-set-form.is-done .gym-saved.is-ok,
.gym-ss-body.is-done .gym-saved.is-ok { display: none; }

/* ------------------------------------------------ the cancel-request dialog

   A confirm that stays on the page it was asked from. Deliberately plain: the
   page behind it (/request/:id/cancel) is still the mechanism and still the
   no-script answer, so this is the same question in less furniture, not a
   second design.

   The inner box is what gives the backdrop something to be OUTSIDE of — a
   click on the <dialog> itself is a click on the backdrop, which is how it
   closes. */
.confirm {
  width: min(420px, calc(100vw - 32px));
  max-height: calc(100dvh - 32px);
  overflow: auto;
  /* A long confirmation on a small phone scrolls; without this, scrolling past
     its end takes the page underneath with it and the modal drifts over a
     screen that has moved. AGENTS.md: "MUST: overscroll-behavior: contain in
     modals/drawers." Found running the guidelines skill over the photo-delete
     dialog, which is the second user of this rule. */
  overscroll-behavior: contain;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
}
.confirm::backdrop { background: rgb(0 0 0 / 0.55); }

.confirm-body { padding: 20px; }
.confirm-title { margin: 0 0 10px; font-size: 17px; }
.confirm-text { margin: 0 0 18px; font-size: 14px; line-height: 1.65; color: var(--secondary); }

/* The two answers side by side, wrapping to a column on a narrow phone rather
   than shrinking to two buttons neither of which can be read. The forms have
   to stretch or the button inside one sits at a different width to the other. */
.confirm-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.confirm-actions form { margin: 0; }
.confirm-actions .btn { width: 100%; }
.confirm-actions > * { flex: 1 1 150px; }

/* ---------------------------------------------------- the client's questions
 *
 * /messages — a record, not a feed. The visual job is to make one exchange
 * read as one thing, and to make it obvious at a glance which turns are the
 * coach's, because that is what the client came for.
 *
 * No fold and no cap: the whole reason this page exists is that the coach's
 * answers were living in a rolling fifty-row window and being flushed.
 */
.msg-threads { list-style: none; padding: 0; margin: 0; }

.msg-thread + .msg-thread {
  margin-block-start: 20px;
  padding-block-start: 20px;
  border-block-start: 1px solid var(--border);
}

/* The question is the heading of its own thread — it is what they scan for. */
.msg-question { margin: 0; }

.msg-meta {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-block: 6px 0;
}

.msg-waiting { margin-block-start: 10px; }

.msg-turns {
  list-style: none; padding: 0;
  margin-block: 12px 0;
  display: flex; flex-direction: column; gap: 10px;
}

/*
 * The coach's turns carry the accent edge and the client's do not. A border on
 * the INLINE START rather than a background: it survives both themes without a
 * second colour to keep in sync, and it reads correctly in RTL for free, which
 * a left-hand border would not.
 */
.msg-turn {
  padding-inline-start: 12px;
  border-inline-start: 3px solid var(--border);
}

.msg-turn-coach { border-inline-start-color: var(--accent); }

.msg-who {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
  margin: 0 0 2px; font-weight: 600;
}

/* The prose itself. pre-wrap because a coach writes in paragraphs and the
 * newlines they typed are part of what they said. */
.msg-body { margin: 0; white-space: pre-wrap; }

.msg-followup { margin-block-start: 12px; }
.msg-form { margin-block-start: 10px; }
.msg-form textarea { inline-size: 100%; }
.msg-ask-again { margin-block-start: 24px; }

/*
 * The notifications page, in two groups.
 *
 * The owner's complaint was that a coach's paragraph and "your payment was
 * received" sat in one undifferentiated strip. The heading does most of the
 * work; the spacing is what stops the two groups reading as one list with a
 * word dropped into it.
 */
.notify-group + .notify-group {
  margin-block-start: 28px;
  padding-block-start: 20px;
  border-block-start: 1px solid var(--border);
}

.notify-group > .h3 { margin-block: 0 12px; }

/* The way out of a feed that forgets. Quiet: it is a door, not an action. */
.notify-durable { margin-block-start: 12px; }

/*
 * A client's name in a pane heading, as a way to reach their record.
 *
 * Underlined only on hover: the heading is a heading first. It has to be
 * discoverable without shouting, because every pane has one and a row of
 * permanently underlined titles turns the dashboard into a link farm.
 */
.team-client-link { color: inherit; text-decoration: none; }
.team-client-link:hover,
.team-client-link:focus-visible { text-decoration: underline; }

/*
 * A contact thread on the coach's side.
 *
 * The client's turns carry no accent edge and the coach's do, matching
 * /messages on the client side so the same conversation reads the same way
 * from both ends.
 */
.team-turn + .team-turn { margin-block-start: 12px; }
.team-turn-who { font-weight: 600; margin-block-end: 4px; }

/* ================================================== the block report (2026-09-28)
 *
 * One simple document per training block — the owner's brief, which replaced
 * the progress report (§19.1 timeline, §20.7 report, §14 ceremony) and every
 * rule that drew it. One partial, _block-report.eta, on two screens: the
 * client's /program/:id/report and the coach's client pane (All Programs,
 * `view=programs&report=<id>`).
 *
 * ONE COLUMN, PHONE FIRST. Designed at 390px; wider screens only cap the
 * measure. Every control a thumb aims at is 44px tall. The accent is the
 * brand gold, and it carries meaning only where words already do (the ★ is
 * always next to "All-time best"; a rise is always a signed number).
 */

.br-page { max-inline-size: 560px; margin-inline: auto; padding-block: 24px 48px; }

.br { display: grid; gap: 14px; min-width: 0; }

.br-head { display: grid; gap: 4px; }
.br-kind {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent-dim);
}
.br-title { margin: 0; font-size: 26px; letter-spacing: -.02em; line-height: 1.2; text-wrap: balance; }
.br-period { margin: 0; color: var(--secondary); font-size: 14px; }

/* Each section is a card, bordered like the rest of the client side. */
.br-section {
  min-width: 0;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  scroll-margin-top: 80px;
}
.br-section > p { margin: 0; }
.br-section > p + p, .br-section > p + .block-row, .br-section > .block-row + p { margin-block-start: 10px; }
.br-section-title { margin: 0 0 12px; font-size: 16px; font-weight: 700; }

/* "Not logged" / "No progress": a sentence with its label in bold. Not red —
   the app does not know why, and the coach's message is where the why goes. */
.br-flag {
  margin: 0 0 12px !important;
  padding: 10px 12px;
  border: 1px solid var(--accent-border);
  border-radius: 9px;
  background: var(--accent-bg);
  font-size: 14px;
  line-height: 1.5;
}
.br-flag strong { color: var(--accent); }

/* THE FOUR SQUARES (owner, 2026-09-30): "a square container, with 4 smaller
   squares inside: two on the top row and two on the bottom." A two-column
   grid of square tiles in a card; the card is square because its tiles are,
   and on a wide screen it stops at 560px (the client page's own measure) so
   the coach's wider pane does not blow the squares up. The grid runs in the
   PAGE's direction: on /fa the first gain is top-right. Fewer lifts is fewer
   tiles — nothing here draws an empty square.

   aspect-ratio is a floor, not a cage: a tile whose words do not fit (a long
   Farsi lift name at 390px) grows taller rather than clipping them. */
.br-glance {
  max-inline-size: 560px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg);
}
.br-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.br-tile {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  aspect-ratio: 1;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
.br-tile > p { margin: 0; min-width: 0; }

/* The kind, in WORDS; the colour only repeats them. Gain green, "also up"
   the same green, steady neutral, dip amber (--warn, never red), pinned the
   brand gold the pins have always worn. */
.br-tile-kind { display: flex; }
.br-tile-kind > span {
  padding: 2px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--secondary);
}
.br-tile.is-gain .br-tile-kind > span,
.br-tile.is-alsoUp .br-tile-kind > span { border-color: var(--success-border); background: var(--success-bg); color: var(--success); }
.br-tile.is-dip .br-tile-kind > span { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn); }
.br-tile.is-pinned .br-tile-kind > span { border-color: var(--accent-border); background: var(--accent-bg); color: var(--accent); }

.br-tile-name {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Start → end: the two sets, each kept whole on its line, so a narrow tile
   breaks at the arrow and never inside "67.5 kg × 8". */
.br-tile-figs {
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
}
.br-tile-figs bdi { white-space: nowrap; }
.br-tile-from, .br-tile-to { display: block; }
.br-tile-from { font-size: 12.5px; font-weight: 600; color: var(--secondary); }
.br-tile-change { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--secondary); }
.br-tile-change.is-up { color: var(--success); }
.br-tile-change.is-down { color: var(--warn); }

/* The sparkline takes whatever the square has left, and never less than a
   thumb's width of line. The SVG stretches to it (preserveAspectRatio none);
   non-scaling-stroke keeps the line 2px however it is stretched. */
.br-spark { position: relative; flex: 1 1 28px; min-block-size: 28px; margin-block-start: 2px; }
.br-spark svg { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; overflow: visible; }
.br-spark-line {
  fill: none;
  stroke: var(--secondary);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.br-tile.is-gain .br-spark-line, .br-tile.is-alsoUp .br-spark-line { stroke: var(--success); }
.br-tile.is-dip .br-spark-line { stroke: var(--warn); }
.br-tile.is-pinned .br-spark-line { stroke: var(--accent); }

@media (min-width: 30rem) {
  .br-glance { padding: 10px; }
  .br-tiles { gap: 10px; }
  .br-tile { padding: 14px; gap: 6px; }
  .br-tile-name { font-size: 15px; }
  .br-tile-figs { font-size: 17px; }
  .br-tile-change { font-size: 14px; }
}

/* ONE square (a single pinned lift, or a block with one lift to pick from)
   is one column's worth of card, not a card with an empty half beside it. */
.br-glance:has(> .br-tiles > .br-tile:only-child) { max-inline-size: min(calc(50% + 5px), 285px); }
.br-tiles:has(> .br-tile:only-child) { grid-template-columns: minmax(0, 1fr); }

/* The pinned squares and the two lists each have a small heading. All of it
   keeps the squares' measure: on the coach's 900px pane a list row, or an
   opened lift's chart, stretched across it was the "too big" the owner
   asked to be rid of. */
.br-group { margin-block-start: 18px; max-inline-size: 560px; }
.br-group-title { margin: 0 0 8px; font-size: 14px; font-weight: 700; color: var(--secondary); }

/* "ALSO IMPROVED": name at the start, change at the end, one row each. */
.br-also-list { display: grid; margin: 0; padding: 0; list-style: none; }
.br-also-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
  padding-block: 8px;
  border-block-end: 1px solid var(--border);
  font-size: 14.5px;
}
.br-also-name { min-width: 0; overflow-wrap: anywhere; }
.br-also-list .br-change { flex: none; }

/* "Show N more" / "Show fewer": one summary, two labels, the state picks.
   display:none takes the hidden one out of the accessible name too. */
.br-more > summary,
.br-all > summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  cursor: pointer;
  color: var(--accent);
  font-size: 14px;
  font-weight: 600;
  touch-action: manipulation;
}
.br-more > summary:hover, .br-all > summary:hover { color: var(--accent-hover); }
.br-more > summary:focus-visible,
.br-all > summary:focus-visible,
.br-all-item > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.br-more[open] .br-more-closed, .br-more:not([open]) .br-more-open { display: none; }

/* OPEN, "Show fewer" sits at the FOOT of what it revealed rather than in the
   middle of the list where "Show N more" was (QA, 2026-10-01). A <summary>
   is always first inside its <details>, so it is moved by position, not in
   the DOM: one control, its aria-expanded still the browser's own, focus and
   reading order unchanged (the revealed rows are not focusable, so Tab meets
   nothing between it and what follows), and still no script. Positioned
   rather than `order` on a flex <details>, which older Safari and Chrome
   ignore. The reserved foot is the summary's own 44px min-height. */
.br-more[open] { position: relative; padding-block-end: 44px; }
.br-more[open] > summary { position: absolute; inset-inline: 0; inset-block-end: 0; }

/* "ALL N LIFTS": the dropdown. Each row opens to the lift's full chart. */
.br-all {
  max-inline-size: 560px;
  margin-block-start: 14px;
  padding-block-start: 4px;
  border-block-start: 1px solid var(--border);
}
.br-lifts { display: grid; margin: 0; padding: 0; list-style: none; }
.br-lift { min-width: 0; border-block-end: 1px solid var(--border); }
.br-all-item > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 12px;
  min-height: 44px;
  padding-block: 6px;
  cursor: pointer;
  font-size: 14.5px;
  touch-action: manipulation;
}
.br-all-item > summary:hover .br-all-name { color: var(--accent-hover); }
.br-all-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  min-width: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.br-all-name bdi { min-width: 0; }
.br-all-body { padding-block: 2px 14px; }
.br-star, .br-pinned {
  display: inline-flex;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
}
.br-star { background: var(--accent-bg); border: 1px solid var(--accent-border); color: var(--accent); }
.br-pinned { border: 1px solid var(--border-strong); color: var(--secondary); }

.br-headline {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.br-change { color: var(--secondary); font-weight: 600; font-variant-numeric: tabular-nums; }
.br-change.is-up { color: var(--success); }
.br-change.is-down { color: var(--warn); }
.br-prev { margin: 8px 0 0; font-size: 13.5px; color: var(--secondary); font-variant-numeric: tabular-nums; }

/* The full chart: never the only carrier — the figures are the line above
   it. No text inside the SVG, so nothing in it scales with its width. */
.br-chart { margin-block-start: 8px; }
.br-chart svg { display: block; inline-size: 100%; block-size: auto; }
.br-guide { stroke: var(--grid-line, var(--border)); stroke-width: 1; stroke-dasharray: 3 4; }
.br-line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.br-dot { fill: var(--accent); }

/* The coach's pins, inside each opened lift: Pin, Remove, or both. */
.br-pin-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 10px; }
.br-pin-actions form { margin: 0; }
.br .btn { min-height: 44px; margin: 0; }

.br-days { font-size: 15px; font-variant-numeric: tabular-nums; }
.br-note { font-size: 13px; color: var(--secondary); }

.br-message-body { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; font-size: 15px; }
.br-message { background: var(--accent-wash); }
.br-signature { margin-block-start: 8px !important; font-size: 13px; color: var(--secondary); }
.br-message-form { display: grid; gap: 8px; margin-block-start: 12px; }
.br-message-form label { font-size: 14px; font-weight: 600; }
.br-message-form textarea { font-size: 16px; min-height: 8rem; }
.br-message-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

.br-cup-line > * { min-width: 0; }
.br-cup-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

.br-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent);
  text-underline-offset: 3px;
}
.br-link:hover { color: var(--accent-hover); }
.br-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.br-card {
  display: block;
  inline-size: 100%;
  max-inline-size: 270px;
  block-size: auto;
  margin-block: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
}

.br-back { display: flex; flex-wrap: wrap; gap: 4px 20px; margin-block-start: 16px; }

/* Inside the dashboard the report sits under the client's name, so its title
   steps down a size with its heading level. */
.team-pane .br-title { font-size: 20px; }

/* THE REPORT BUTTON on a row of the coach's All Programs (2026-09-30) —
   it replaced the pane's Report sub-tab and its row of block chips. The same
   44px box as the client's own Report button on /programs: `.btn-small` is
   38px. inline-flex with a gap, so the label and the flag are two items that
   cannot run together ("ReportReview due"). Wraps inside itself rather than
   pushing the row past a 390px card in Farsi. */
.team-prog-report {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 8px;
  min-block-size: 44px;
}
/* THE ROW THE TWO SIT IN (2026-10-01). `.team-actions-row` is not a flex box
   anywhere — its `align-items` has never applied — so Open and Report were
   two inline boxes 3.4px apart (one space of 13px type), at two heights
   (37.6 and 44) on a baseline. A flex row with an 8px gap, both at the 44px
   floor, wrapping as a pair when a Farsi label and its flag need the line.
   Scoped to this card: the other rows of that class each have a measured
   layout of their own. */
.team-prog-card > .team-actions-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.team-prog-card > .team-actions-row > .btn { min-block-size: 44px; }
/* This stylesheet has no global `a:focus-visible`; each component states its
   own ring, and this one is the pane's gold. */
.team-prog-report:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* THE FLAG: a dot AND a word, never the dot alone (AGENTS.md: colour is
   never the only carrier). Accent, not red — the app does not know why the
   block went the way it did, and the report's own callout is not red either. */
.team-prog-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 8px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.5;
}
.team-prog-dot {
  flex: none;
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* THE WAY BACK from an opened report to the list it was opened from. A real
   link at the 44px floor; the arrow is drawn here so it points the reading
   direction in both languages — the team-back idiom. */
.team-report-back { margin: 0 0 8px; }
.team-report-back-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-block-size: 44px;
  color: var(--secondary);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
}
.team-report-back-link:hover { color: var(--text); }
/* The arrow is decoration — the link's words are its name — so it carries an
   empty alternative text where the browser understands one (the plain
   declaration above each is the fallback where it does not). */
.team-report-back-link::before { content: "\2190"; content: "\2190" / ""; font-size: 14px; line-height: 1; }
[dir="rtl"] .team-report-back-link::before { content: "\2192"; content: "\2192" / ""; }
.team-report-back-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* The Report button on /programs is a REAL 44px target. `.btn.btn-small` is
   13px type in 10px of padding — 38px. inline-flex so the label sits in the
   middle of the taller box; the row centres its items so the 38px buttons
   beside it keep their own height rather than stretching to this one. */
.prog-actions { align-items: center; }
.prog-actions > .prog-report {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 44px;
}

/* The three links that reach the report from other screens. Quiet, because
   each sits under a heading that is already the subject of its own page. */
.rec-to-progress, .chat-to-progress { margin: 8px 0 0; font-size: 13px; }
.chat-to-progress { margin-block: 14px 0; }

/* ON PAPER. The app is dark-only and browsers do not print backgrounds by
   default, so a printed report would be pale text on white. The same token
   flip the progress report's print layer used (audit F47), scoped to this one
   document; the chrome and the forms are hidden. */
@media print {
  .page-block-report {
    color-scheme: light;
    --bg: #fff;
    --panel: #fff;
    --surface: #fff;
    --surface-2: #fff;
    --text: #111;
    --secondary: #333;
    --muted: #555;
    --border: #ccc;
    --border-strong: #bbb;
    --gym-pip: #ddd;
    --accent-wash: #fdf6e6;
    --accent-bg: #fdf6e6;
    /* The improvement green on white: #1f6f3a is 6.2:1. */
    --success: #1f6f3a;
    --success-bg: #fff;
    --success-border: #b9d8c2;
    /* A dipped square's amber on white: #7a5200 is 6.9:1. */
    --warn: #7a5200;
    --warn-bg: #fff;
    --warn-border: #e2cfa6;
    color: var(--text);
    background: var(--bg);
  }
  /* On paper the squares keep their colours (a gain is green, a dip amber),
     and the "All lifts" dropdown — a control, closed — is left off. */
  .page-block-report .br-all { display: none; }
  .page-block-report .br-tile,
  .page-block-report .br-spark-line,
  .page-block-report .br-line,
  .page-block-report .br-dot,
  .page-block-report .block-seg {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .page-block-report .br-section { break-inside: avoid; }
  body:has(.page-block-report) .tnav,
  body:has(.page-block-report) .tnav-spacer,
  body:has(.page-block-report) .site-header,
  body:has(.page-block-report) .site-footer,
  body:has(.page-block-report) .install-hint,
  .page-block-report .br-share,
  .page-block-report .br-back { display: none; }
}

/* ---- the articles section (§21, §22; rebuilt to the approved mockup
        2026-08-28) ------------------------------------------------------ */

/* MOBILE FIRST, and that is not a slogan here: everything down to the
   `min-width` blocks at the bottom of this section IS the phone layout, and
   desktop is the handful of rules that add a second column. The mockup's
   phone artboards are the design; the desktop ones add a rail to it.

   Every inline offset is a logical property, so the Farsi side is this same
   stylesheet under `dir="rtl"` — the accent rail moves to the right edge, the
   chevrons and the contents icon mirror, and no rule is written twice. */

.page-guides, .page-guide { max-width: 1180px; padding-block: 26px 44px; }

/* ---- the index -------------------------------------------------------- */

.guides-head h1 { margin: 0; font-size: 30px; line-height: 1.15; letter-spacing: -.025em; text-wrap: balance; }
.guides-head p {
  margin: 10px 0 0;
  max-width: 58ch;
  font-size: 16px;
  line-height: 1.6;
  color: var(--secondary);
  text-wrap: pretty;
}

/* Category filter chips, as real links. The current one is FILLED rather
   than outlined: at chip size an outline is easy to miss, and the whole job
   of this row is saying which slice you are looking at. */
.guides-cats {
  margin-block-start: 22px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.guides-cat {
  display: inline-flex;
  align-items: center;
  /* 40px tall, which clears the 24px minimum by enough to be comfortable on a
     phone without becoming a button. */
  min-height: 40px;
  padding: 0 16px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--nav-link);
  font-size: 13.5px;
  text-decoration: none;
  touch-action: manipulation;
}
.guides-cat:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }
.guides-cat.is-current {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.guides-cat.is-current:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* A VISIBLE FOCUS RING ON EVERY LINK THIS SECTION ADDS. There is no global
   `a:focus-visible` rule in this stylesheet — each component states its own —
   so a new component that does not is a component you can tab into and lose
   yourself in. Grouped rather than repeated per rule: it is one decision. */
.guides-cat:focus-visible,
.article-lead:focus-visible,
.article-card:focus-visible,
.article-eyebrow:focus-visible,
.article-toc-item a:focus-visible,
.article-tags a:focus-visible,
.article-related a:focus-visible,
.guide-back a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.guides-empty { margin-block-start: 26px; color: var(--muted); font-size: 15px; }

/* The note a signed-in coach gets when the list is carrying drafts. Nobody
   else ever sees it, because nobody else is ever sent one. */
.guides-draftnote {
  margin: 20px 0 0;
  padding: 10px 14px;
  border: 1px solid var(--accent-border);
  border-radius: 8px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 13px;
}

/* The eyebrow over a card's title: the category it is filed in. */
.article-card-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-dim);
}

/* A card's cover, or the frame where one would be. The aspect ratio is fixed
   so a row of cards lines up whatever shape the pictures are, and the image
   fills it — its own width/height attributes are still in the markup, which
   is what reserves the box before the file lands. */
.article-card-cover {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--panel);
  color: var(--bar-prev);
  border-block-end: 1px solid var(--border);
}
.article-card-cover img { width: 100%; height: 100%; object-fit: cover; }
.article-card-cover svg { width: 34px; height: 34px; }

/* THE LEAD CARD. One column on a phone — the mockup's two-up is a desktop
   shape, and squeezing it onto 390px gives two columns of nothing. */
.article-lead {
  display: grid;
  gap: 0;
  margin-block-start: 26px;
  border: 1px solid var(--accent-border);
  border-radius: var(--radius-lg);
  background: var(--panel);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  touch-action: manipulation;
}
.article-lead:hover { border-color: var(--accent); }
/* On a PHONE the lead card leads with its picture, like every card under it —
   text-then-picture is the desktop two-up read as one column, and it makes the
   first card in the list the one whose rhythm is different from all the rest. */
.article-lead .article-card-cover { aspect-ratio: 16 / 9; border-block-end: 0; order: -1; }
.article-lead-text { padding: 22px; display: flex; flex-direction: column; gap: 10px; }
.article-lead-title {
  margin: 0;
  font-size: 24px;
  line-height: 1.22;
  letter-spacing: -.02em;
  font-weight: 700;
  color: var(--text);
  /* `balance` rather than `pretty`: AGENTS.md names it for a heading, where
     the job is to even the lines rather than to rescue the last word. The
     prose under it keeps `pretty`, which is the right one for a paragraph. */
  text-wrap: balance;
  /* A title is written by a coach and can be any length: it wraps rather than
     overflowing (AGENTS.md, "resilient to user-generated content"). */
  overflow-wrap: anywhere;
}
.article-lead-desc {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: var(--secondary);
  text-wrap: pretty;
}

/* THE GRID. One column on a phone, two from 640px, three from 900px. */
.article-cards {
  margin-block-start: 20px;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: 18px;
}
.article-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  touch-action: manipulation;
}
.article-card:hover { border-color: var(--border-strong); }
.article-card:hover .article-card-title { color: var(--accent); }
.article-card-text { padding: 18px; display: flex; flex-direction: column; gap: 9px; }
.article-card-title {
  margin: 0;
  font-size: 18px;
  line-height: 1.3;
  letter-spacing: -.015em;
  font-weight: 700;
  color: var(--text);
  text-wrap: balance;
  overflow-wrap: anywhere;
}
.article-card-desc { margin: 0; font-size: 14px; line-height: 1.6; color: var(--secondary); }
/* Author · date · reading time. `tabular-nums` because a column of cards puts
   these dates directly under one another. */
.article-card-meta {
  font-size: 12.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A draft, as only a signed-in coach ever sees it. Dimmed AND labelled — the
   label is what says which state this is, because colour alone is not a cue
   (AGENTS.md). */
.article-card.is-draft, .article-lead.is-draft { opacity: .62; }
.article-card.is-draft:hover, .article-lead.is-draft:hover { opacity: 1; }

/* ---- one article ------------------------------------------------------ */

.article { max-width: 68ch; margin-inline: auto; }

.article-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* A 44px target around a 12px label: the hit area is expanded rather than
     the type, which is the AGENTS.md rule for a small control. */
  min-height: 44px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-dim);
  text-decoration: none;
  touch-action: manipulation;
}
.article-eyebrow:hover { color: var(--accent); }
/* The chevron points back the way the reader came, which is the other way
   round in Farsi. One rule, driven by the document's direction. */
[dir="rtl"] .article-eyebrow-chev { transform: scaleX(-1); }

.article-title {
  margin: 8px 0 0;
  font-size: 28px;
  line-height: 1.22;
  letter-spacing: -.025em;
  font-weight: 800;
  color: var(--text);
  text-wrap: balance;
  overflow-wrap: anywhere;
}
.article-standfirst {
  margin: 12px 0 0;
  font-size: 16px;
  line-height: 1.6;
  color: var(--secondary);
  text-wrap: pretty;
}

.article-byline { display: flex; align-items: center; gap: 10px; margin-block-start: 20px; }
/* The initials, not a photograph: coaches have no avatar image here, and a
   grey circle that never resolves is furniture. */
.article-avatar {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  font-size: 12px;
  font-weight: 700;
  color: var(--secondary);
}
.article-byline-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.article-author { font-size: 13px; font-weight: 600; color: var(--text); }
.article-meta { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

.article-cover { margin: 22px 0 0; }
.article-cover img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.article-cover figcaption {
  margin-block-start: 8px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

/* ONE COLUMN on a phone. The grid is where the desktop rail comes from, and
   it is a single column until there is room for two. */
.article-grid { display: grid; grid-template-columns: 1fr; margin-block-start: 24px; }
/* min-width: 0 so a wide child — a table, a code fence — scrolls inside its
   own box instead of stretching this column and pushing the page sideways.
   A grid item's default `min-width: auto` is what lets that happen. */
.article-column { min-width: 0; }

/* ---- the contents ----------------------------------------------------- */

/* THE DISCLOSURE IS THE PHONE'S; the list is its SIBLING, never its child.
   A closed <details> hides its contents through
   `::details-content { content-visibility: hidden }`, which no author
   `display` rule overrides — nesting the list inside is what once made the
   desktop header render empty on every screen in this app. */
.article-toc-toggle {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}
.article-toc-toggle summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  /* 52px, comfortably over the 44px phone minimum. */
  min-height: 52px;
  padding-inline: 16px;
  cursor: pointer;
  list-style: none;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  touch-action: manipulation;
}
.article-toc-toggle summary::-webkit-details-marker { display: none; }
.article-toc-toggle summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.article-toc-summary { display: flex; align-items: center; gap: 10px; min-width: 0; }
.article-toc-icon { color: var(--accent); flex: 0 0 auto; }
[dir="rtl"] .article-toc-icon { transform: scaleX(-1); }
.article-toc-count { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
.article-toc-chev { color: var(--secondary); transition: transform .15s ease; }
.article-toc-toggle[open] .article-toc-chev { transform: rotate(180deg); }
.article-toc-toggle[open] { border-color: var(--accent-border); }

/* Hidden until the disclosure is opened — on a PHONE. The desktop block at
   the foot of this section turns the disclosure off and the list on. */
.article-toc { display: none; }
.article-toc-toggle[open] ~ .article-toc {
  display: block;
  margin-block-start: -1px;
  padding-block: 6px;
  border: 1px solid var(--accent-border);
  border-start-start-radius: 0;
  border-start-end-radius: 0;
  border-end-start-radius: var(--radius);
  border-end-end-radius: var(--radius);
  background: var(--panel);
}
/* The rail's own heading is desktop furniture; on a phone the disclosure's
   summary already says "In this article". */
.article-toc-head { display: none; }
.article-toc-list { list-style: none; margin: 0; padding: 0; }
.article-toc-item a {
  display: flex;
  align-items: center;
  /* 48px rows, as the mockup specifies. */
  min-height: 48px;
  padding-inline: 16px;
  /* The accent rail. `border-inline-start` puts it on the LEFT in English and
     the RIGHT in Farsi with no second rule — which is the mockup's note about
     Farsi being mirrored rather than flipped. */
  border-inline-start: 3px solid transparent;
  font-size: 15px;
  line-height: 1.35;
  color: var(--nav-link);
  text-decoration: none;
  touch-action: manipulation;
}
.article-toc-item.is-sub a { padding-inline-start: 30px; font-size: 14px; color: var(--secondary); }
.article-toc-item a:hover { color: var(--text); }
/* Where you are. articletoc.js adds this; without script nothing is marked
   and every link still works, which is the whole contract. */
.article-toc-item a.is-current {
  border-inline-start-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: 600;
}
.article-toc-hint {
  margin: 6px 16px 10px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

/* ---- the reading column ----------------------------------------------- */

/* 16px/1.75, a DELIBERATE step up from the app's 15px UI size. This is the
   one long-form reading surface on the site and it is read in one sitting
   rather than scanned. `.article-body` overrides `.legal`, which the body
   also carries for everything the two share. */
.article-body { max-width: 68ch; margin-block-start: 26px; }
.article-body p, .article-body li { font-size: 16px; line-height: 1.75; color: var(--nav-link); }
.article-body p { margin: 0 0 18px; }
.article-body h2 {
  margin: 34px 0 12px;
  font-size: 20px;
  line-height: 1.35;
  letter-spacing: -.01em;
  font-weight: 700;
  color: var(--text);
}
.article-body h3 { margin: 26px 0 10px; font-size: 17px; line-height: 1.4; color: var(--text); }
.article-body strong { color: var(--text); font-weight: 600; }
/* A body link has to be visible as a link in a wall of prose, and underlined
   rather than colour-only — colour alone is not a cue (AGENTS.md). */
.article-body a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.article-body a:hover { color: var(--accent-hover); }
/* A deep link or a find-in-page lands on a heading; without this the browser
   parks it flush against the top of the viewport. Generous, because the
   contents rail is what sends people here. */
.article-body :is(h2, h3, h4) { scroll-margin-block-start: 24px; }
/* FARSI READS WITH MORE AIR. Persian ascenders and descenders need it, and
   2.0 is the value the mockup's Farsi artboards are drawn at. */
[dir="rtl"] .article-body p,
[dir="rtl"] .article-body li { line-height: 2; }
[dir="rtl"] .article-body h2 { line-height: 1.5; }

.article-tags { margin-block-start: 34px; padding-block-start: 24px; border-block-start: 1px solid var(--border); }
.article-tags ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.article-tags a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 13px;
  font-size: 12px;
  color: var(--nav-link);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  text-decoration: none;
  touch-action: manipulation;
}
.article-tags a:hover { color: var(--text); border-color: var(--border-strong); }

/* ---- the CTA and share ------------------------------------------------ */

/* The partial is drawn twice and exactly ONE copy is ever displayed —
   `display: none` takes the other out of the accessibility tree as well as
   the layout, so a screen reader meets one. On a phone the foot copy shows;
   the desktop block below swaps them. See _article-aside.eta. */
.article-rail-extra { display: none; }
.article-foot-extra { display: block; margin-block-start: 30px; }

.article-cta {
  padding: 18px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.article-cta-title { margin: 0 0 6px; font-size: 15px; font-weight: 700; line-height: 1.35; color: var(--text); }
.article-cta-body { margin: 0 0 14px; font-size: 13px; line-height: 1.6; color: var(--secondary); }
.article-cta-go { display: block; text-align: center; }

.article-share { display: flex; align-items: center; gap: 12px; margin-block-start: 20px; }
.article-share-label { margin: 0; font-size: 12px; color: var(--muted); }
.article-share-links { list-style: none; margin: 0; padding: 0; display: flex; gap: 8px; }
/* 40px: an icon-only control needs a real target, and each one carries an
   aria-label because the icon is the only thing in it. */
.article-share-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--secondary);
  touch-action: manipulation;
}
.article-share-link:hover { color: var(--accent); border-color: var(--accent-border); }
.article-share-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- read next, and the way back -------------------------------------- */

/* OUTSIDE the article grid, deliberately: the grid's height is what stops the
   sticky rail, so anything in here would let the rail follow the reader past
   the end of the article. */
.article-related {
  max-width: 68ch;
  margin: 44px auto 0;
  padding-block-start: 22px;
  border-block-start: 1px solid var(--border);
}
.article-related h2 {
  margin: 0 0 14px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-dim);
}
.article-related ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 10px; }
.article-related a {
  display: block;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  touch-action: manipulation;
}
.article-related a:hover { border-color: var(--border-strong); }
.article-related-title {
  display: block;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
  overflow-wrap: anywhere;
}
.article-related a:hover .article-related-title { color: var(--accent); }
.article-related-meta { display: block; margin-block-start: 4px; font-size: 12px; color: var(--muted); }

.guide-back { max-width: 68ch; margin: 30px auto 0; }
.guide-back a { color: var(--accent); font-size: 14px; }

/* The draft banner is for a signed-in coach only — the route 404s everyone
   else — so it borrows the accent treatment: loud enough that a screenshot of
   a draft cannot be mistaken for a published page. */
.draft-banner {
  margin: 0 0 18px;
  padding: 10px 14px;
  border: 1px solid var(--accent-border);
  border-radius: 8px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
}

/* ---- and now the wider screens ---------------------------------------- */

@media (min-width: 640px) {
  .article-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .guides-head h1 { font-size: 36px; }
}

@media (min-width: 900px) {
  .page-guides, .page-guide { padding-block: 40px 60px; }
  .guides-head h1 { font-size: 40px; letter-spacing: -.03em; }
  .article-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }

  /* The lead card gets its two-up back: text beside the picture. */
  .article-lead { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: center; padding: 26px; gap: 34px; }
  .article-lead-text { padding: 0; }
  .article-lead-title { font-size: 30px; }
  /* Two columns again, and the picture goes back to the side the mockup puts
     it on — after the text in reading order, whichever way the page runs. */
  .article-lead .article-card-cover { order: 0; border-radius: var(--radius); border: 1px solid var(--border); }

  /* THE TWO-COLUMN ARTICLE: a 260px contents rail and a reading column, 64px
     apart, exactly as the mockup specifies.

     The block is capped at the width the two columns actually need — the rail,
     the gap, and a reading column with room for its 68ch of prose — and then
     CENTRED. Left at the page's full width it sat hard against the left edge
     with a third of the screen empty beside it, which reads as a layout that
     failed rather than one that chose a measure. */
  .article { max-width: 984px; margin-inline: auto; }
  .article-head, .article-cover { max-width: 68ch; margin-inline: 0; }
  .article-title { font-size: 46px; line-height: 1.12; letter-spacing: -.03em; max-width: 20ch; }
  .article-standfirst { font-size: 18px; max-width: 62ch; }
  [dir="rtl"] .article-title { font-size: 38px; line-height: 1.35; letter-spacing: normal; }

  .article-grid {
    grid-template-columns: 260px minmax(0, 1fr);
    gap: 64px;
    /* `align-items: start` is what makes the rail a sticky box inside its own
       grid area rather than a stretched one with nowhere to travel. */
    align-items: start;
    margin-block-start: 34px;
  }

  /* THE RAIL STOPS AT THE END OF THE ARTICLE. A sticky element cannot leave
     its containing block, and its containing block here is the grid AREA —
     whose height is the row's, which is the reading column's. So the rail
     travels down beside the prose and stops dead at the last paragraph
     instead of following the reader into the footer, which is the detail the
     mockup calls out as the one most sites get wrong. "Read next" and the way
     back are outside this grid for the same reason. */
  .article-rail {
    position: sticky;
    top: 32px;
    /* A contents list longer than the viewport scrolls inside the rail rather
       than being cut off by it. */
    max-height: calc(100vh - 64px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The disclosure is a phone control; on desktop the list is simply there. */
  .article-toc-toggle { display: none; }
  .article-toc-hint { display: none; }
  .article-toc,
  .article-toc-toggle[open] ~ .article-toc {
    display: block;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }
  .article-toc-head {
    display: block;
    margin: 0 0 14px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--accent-dim);
  }
  /* The hairline the active mark sits against. The item's own 2px border
     overlaps it by a negative margin, so the mark replaces the line rather
     than sitting beside it. */
  .article-toc-list { border-inline-start: 1px solid var(--border); }
  .article-toc-item a {
    display: block;
    min-height: 0;
    padding: 9px 0;
    padding-inline-start: 15px;
    margin-inline-start: -1px;
    border-inline-start: 2px solid transparent;
    font-size: 14px;
    line-height: 1.45;
    color: var(--secondary);
    background: none;
  }
  .article-toc-item.is-sub a { padding-inline-start: 26px; font-size: 13px; }
  .article-toc-item a.is-current { background: none; }
  [dir="rtl"] .article-toc-item a { line-height: 1.75; }

  /* The CTA and share move into the rail, and the foot copy goes away. */
  .article-rail-extra { display: block; margin-block-start: 26px; }
  .article-foot-extra { display: none; }
  .article-share { margin-block-start: 20px; }

  .article-body { margin-block-start: 0; }
  .article-body h2 { font-size: 22px; margin: 40px 0 14px; letter-spacing: -.015em; }

  /* Read next and the way back line up with the READING COLUMN, not with the
     page — they belong to the article, and centring them under a two-column
     layout would leave them floating between the two. The same 984px block as
     the article above, with the rail's width as a start padding: that is what
     makes them start where the prose starts. */
  .article-related, .guide-back {
    max-width: 984px;
    margin-inline: auto;
    padding-inline-start: calc(260px + 64px);
  }
  /* The rule above "Read next" moves onto the heading, so it starts where the
     PROSE starts. On the element it spanned the rail too, which made it read
     as a divider belonging to the page rather than to the article. */
  .article-related { border-block-start: 0; padding-block-start: 0; }
  .article-related h2 {
    padding-block-start: 22px;
    border-block-start: 1px solid var(--border);
  }
}

@media (min-width: 1180px) {
  .article-cover img { max-height: 420px; object-fit: cover; }
}

/* The rail's own scroll and the chevron's turn are motion; neither is
   essential, and someone who has asked for less of it gets none. */
@media (prefers-reduced-motion: reduce) {
  .article-toc-chev { transition: none; }
  .article-rail { scroll-behavior: auto; }
}

/* ------------------------------------------------ deload weeks (both panes)

   ONE RULE SET, because there is one partial: _team-deload.eta is drawn by the
   generate pane and by the review pane, and giving each its own class is how
   two surfaces of one fact start to look like two facts. See migration 064. */

/* The card itself carries `.team-card`; this is only the hook the checker
   asks every class to have, and the one thing the card needs of its own —
   its own margin, so it does not weld to the tab strip or the run state. */
.team-deload { margin-block-start: 14px; }

.team-deload-weeks {
  margin-block-start: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
}
/* The whole label is the target, not the 16px box inside it — 44px is the
   floor a finger needs and a checkbox is a quarter of it. */
.team-deload-week {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding-inline-end: 6px;
  font-size: 13.5px;
  cursor: pointer;
}
.team-deload-week input { width: 18px; height: 18px; margin: 0; flex: none; }

/* ------------------------------------------------------------------- pause

   ONE SET OF RULES for the client's account page and the coach's client pane.
   The two draw the same rows and mean the same thing (backlog §14 slice two),
   and giving each its own classes is how two views of one record start to look
   like two records — the mistake the records page and its coach-side pane were
   built specifically to avoid. Only the FORM above the list differs, because
   the client's is a page form and the coach's is a dashboard row. */

.pause-form { margin-block-start: 14px; }
.pause-dates { display: flex; flex-wrap: wrap; gap: 12px; }
/* Each date takes half the row and wraps to full width on a phone rather than
   squeezing a date picker to something a thumb cannot hit. */
.pause-dates .field { flex: 1 1 12rem; min-width: 0; margin: 0; }

.pause-list {
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.pause-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
}
.pause-row form { margin: 0; }
/* The remove control is a real 44px box. `.btn-small` is ~32px on the client
   side and ~36px on the coach's, and this one sits at the end of a row a thumb
   scrolls past — the same finding, and the same fix, as the photo delete
   control (audit F26). */
.pause-row .btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin: 0;
  touch-action: manipulation;
}
/* Why a row has no remove button, rather than a control that simply is not
   there. See lib/consistency.js on the undo window. */
.pause-locked { flex: 1 1 12rem; min-width: 0; text-align: end; }
.pause-row-body {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 10px;
  align-items: baseline;
  min-width: 0;
}
/* Two dates side by side, and the list is read as a column of them — so the
   digits line up rather than jittering row to row. */
.pause-when { font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.pause-who { flex-basis: 100%; }
/* The reason is somebody's own words — it wraps and it breaks, rather than
   holding the row wider than the card. */
.pause-reason {
  flex-basis: 100%;
  font-size: 13px;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* The coach's row-shaped form. `.team-inline-form` already lays the row out;
   these give each labelled control its own column so the label sits over its
   input instead of beside it. */
.team-pause { margin-block-start: 14px; }
.team-pause-field { display: flex; flex-direction: column; gap: 4px; }
.team-pause-field input { margin: 0; }
.team-pause-reason { flex: 1 1 12rem; min-width: 0; }

/* ------------------------------------------------------ this block's strip

   THE SAME VISUAL LANGUAGE AS `.gym-week-bar`, deliberately (backlog §14 slice
   two). This strip extends the week bar across the whole block rather than
   replacing it, so it borrows the bar's box and the pip's shape instead of
   inventing a second vocabulary for the same idea one card lower down.

   The segments are FIXED-SIZE flex items — no computed widths, so there is
   nothing here for the CSP to drop (an inline width is what left the intake
   progress bar at 100% for every client), and no motion, because the app is
   deliberately near-motionless and there is nothing to reduce. */

/* ---------------------------------------------- the disclosure and the aside
 *
 * TOO MUCH ON THE LOBBY (owner, 2026-08-29). The strip sat open between the
 * week bar and the day list, so the screen someone opens standing at a rack
 * asked them to read a block-long strip before they could start. It is now a
 * closed <details> on a phone and a real aside from 60rem up.
 *
 * THE STRIP IS THE DISCLOSURE'S SIBLING, NEVER ITS CHILD. A closed <details>
 * hides its contents through `::details-content { content-visibility: hidden }`
 * and no author `display` rule overrides that — which is what once rendered
 * the desktop header empty on every screen in this app (see _header.eta). As a
 * sibling the strip is ours to show: the disclosure drives it while it is on
 * screen, and the aside block at the foot of this section turns the disclosure
 * off and the strip on.
 */
.block-toggle {
  margin-block-start: 10px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
.block-toggle > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* 52px, comfortably over the 44px phone minimum — and this is the one
     control on the lobby that a wet thumb reaches for out of curiosity. */
  min-height: 52px;
  padding: 8px 18px;
  cursor: pointer;
  list-style: none;
  touch-action: manipulation;
}
.block-toggle > summary::-webkit-details-marker { display: none; }
.block-toggle > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The label over the count, in the strip's own two type sizes: the closed
   state has to say something true, and "11 of 14 sessions this block" is the
   whole reason to open it. */
.block-toggle-k { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.block-toggle-n { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
/* Drawn as a chevron and rotated, like the article contents. It points DOWN
   closed in both languages — down is not a direction the bidi algorithm has an
   opinion about, which is why this one needs no RTL rule. */
.block-toggle-chev { flex: 0 0 auto; color: var(--secondary); transition: transform .15s ease; }
.block-toggle[open] .block-toggle-chev { transform: rotate(180deg); }
/* The rotation is the only motion on this screen and it is not the message —
   the chevron's DIRECTION is, and that lands either way. */
@media (prefers-reduced-motion: reduce) { .block-toggle-chev { transition: none; } }
.block-toggle[open] { border-color: var(--accent-border); }

/* Closed is the default and the point: a client can start training without
   reading any of this. */
.block-toggle ~ .block-strip { display: none; }
.block-toggle[open] ~ .block-strip {
  display: block;
  /* The two boxes join into one while it is open. */
  margin-block-start: -1px;
  border-color: var(--accent-border);
  border-start-start-radius: 0;
  border-start-end-radius: 0;
}
/* The strip's own head repeats what the summary already says. It is the
   ASIDE's head — display:none rather than a visual trick, so a screen reader
   on a phone is never read the same fraction twice. */
.block-toggle ~ .block-strip .block-head { display: none; }

.block-strip {
  margin-block-start: 10px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
.block-empty { margin-block-start: 10px; font-size: 13px; }

.block-head {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  align-items: baseline;
  justify-content: space-between;
}
.block-title {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
/* "11 of 14" is a comparison of two numbers, and it changes under the reader
   as they log sessions. Tabular figures keep it from shifting. */
.block-count { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* One row of weeks, wrapping. A 24-week block is six rows on a phone rather
   than a strip that scrolls sideways out of the card. */
.block-row {
  margin-block-start: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 5px 10px;
}
/* A week's own segments sit tight together; the larger gap between weeks is
   what makes the grouping readable without a label on every one. */
.block-week { display: flex; gap: 3px; }
.block-seg {
  width: 10px;
  height: 8px;
  border-radius: 3px;
  background: var(--gym-pip);
}
.block-seg.is-done { background: var(--accent); }

/* A PRESCRIBED DELOAD WEEK, drawn light and counted in neither fraction. It is
   still drawn: a week that simply vanished would leave the row looking like
   the block was shorter than it is. Outlined rather than filled, so "the coach
   asked for less here" reads at a glance and never reads as a gap. */
.block-week.is-light .block-seg {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--gym-pip);
}
.block-week.is-light .block-seg.is-done {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--accent);
}

/*
 * Both state margin-block-END as well as start, and the end is why: these are
 * <p>, so the BROWSER's own `margin: 1em 0` is what a rule naming only the
 * start leaves standing. Adjacent margins collapse to the larger, so the strip
 * drew the 13.5px and 12px the UA sheet asked for and never once drew the 6px
 * this file does — the deload note and the paused note sat 12px apart instead
 * of 6, and the line above them 13.5px from the note under it instead of 6.
 * Measured on the gym lobby's aside at 1440, 2026-08-29.
 */
.block-line {
  margin-block: 12px 0;
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
}
.block-note { margin-block: 6px 0; font-size: 12px; color: var(--muted); }

/* ------------------------------------------------------- THE CUP BADGES
 *
 * The owner, 2026-09-01: bronze / silver / gold cups. One mark, drawn by
 * _cup.eta and used on both the client's awards row and the coach's ladder,
 * so a cup cannot become two different objects on two screens.
 *
 * SIZED IN em THROUGHOUT. The badge takes its size from whatever text it sits
 * in — 1.15em of glyph beside the label — so the client's chip and the coach's
 * denser row each get a mark in proportion to their own type. At the size this
 * is actually read at, roughly 20px on a 390px phone, the silhouette is what
 * identifies the object and the word beside it is what names it.
 *
 * COLOUR IS NEVER THE ONLY CARRIER. Every badge carries its name in text, in
 * the reader's own language, in every state. The colour is the third signal
 * after the word and the ladder's order.
 *
 * The fills are CLASSES, not `fill="var(--cup-gold)"` attributes: a
 * presentation attribute is parsed as CSS but is not reliably resolved as a
 * custom property in every engine, which is the same rule the records charts
 * follow. */

.cup-badge {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  padding: .25em .6em;
  border: 1px solid var(--cup-border);
  border-radius: 999px;
  background: var(--cup-bg);
  font-size: 12px;
  line-height: 1.3;
  white-space: nowrap;
}
.cup-name { color: var(--text); font-weight: 600; }

.cup {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
}
.cup-body { fill: var(--cup-metal); }
/* Open strokes, so each handle keeps a hole in it at small sizes rather than
   filling in to a blob. */
.cup-handle {
  fill: none;
  stroke: var(--cup-metal);
  stroke-width: 1.7;
  stroke-linecap: round;
}

/* The three metals. One custom property set per cup, read by the four rules
   above, so a metal is named in exactly one place per cup. */
.cup-bronze { --cup-metal: var(--cup-bronze); --cup-bg: var(--cup-bronze-bg); --cup-border: var(--cup-bronze-border); }
.cup-silver { --cup-metal: var(--cup-silver); --cup-bg: var(--cup-silver-bg); --cup-border: var(--cup-silver-border); }
.cup-gold   { --cup-metal: var(--cup-gold);   --cup-bg: var(--cup-gold-bg);   --cup-border: var(--cup-gold-border); }

/* ------------------------------------- THIS BLOCK'S CHALLENGES (v2)
 *
 * The owner's decisions of 2026-09-28 — lib/challenges.js. Two surfaces:
 *
 *   .chl   the client's panel, in the gym lobby and the program viewer:
 *          a heading with the block's cup, one line per challenge with its
 *          progress, and a thin bar. Small, because the lobby is busy.
 *   .bch   the coach's card, in the builder and on the client pane.
 *
 * The bars are sized by [data-pct] (the 5% steps near .rec-bar), never an
 * inline style — the CSP. No transition on the width: an animated layout
 * property is what the guidelines forbid.
 *
 * NOTHING HERE DRAWS A FAILURE. A met line gets weight and a tick; an unmet
 * one is its progress in the ordinary ink. There is no red. */

.chl {
  margin-block-start: 14px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
}
.chl-head {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 12px;
  align-items: center;
  justify-content: space-between;
}
.chl-title { margin: 0; font-size: 15px; font-weight: 600; }
.chl-nocup { font-size: 12px; color: var(--muted); }
.chl-sub {
  margin-block: 4px 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--secondary);
  font-variant-numeric: tabular-nums;
}
.chl-list { margin: 10px 0 0; padding: 0; list-style: none; display: grid; gap: 10px; }
.chl-item { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
.chl-mark { flex: none; inline-size: 1.4em; text-align: center; line-height: 1.5; color: var(--muted); }
.chl-item.is-met .chl-mark { color: var(--accent); font-weight: 700; }
.chl-body { flex: 1 1 auto; min-width: 0; display: grid; gap: 3px; }
.chl-name { font-size: 13.5px; font-weight: 600; overflow-wrap: anywhere; }
.chl-target { font-weight: 400; color: var(--secondary); font-variant-numeric: tabular-nums; }
.chl-progress { font-size: 12px; color: var(--secondary); font-variant-numeric: tabular-nums; }
.chl-item.is-met .chl-progress strong { color: var(--text); }
.chl-bar {
  display: block;
  margin-block-start: 3px;
  height: 5px;
  border-radius: 3px;
  background: var(--gym-pip, var(--border));
  overflow: hidden;
}
.chl-fill {
  display: block;
  height: 100%;
  inline-size: var(--pct, 0%);
  border-radius: 3px;
  background: var(--accent);
}
.chl-earlier { margin: 10px 0 0; font-size: 12px; line-height: 1.6; color: var(--secondary); }
/* The cup and what it is: "So far this block" while it runs, "Won this
   block" once it has ended. */
.chl-standing { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; font-size: 12px; color: var(--secondary); }

/* IN PROGRESS — a challenge judged at the block's end, while it runs. The
   words say it; the bar agrees by being dashed rather than solid, and the
   model never sizes it full before the end. */
.chl-pending {
  padding: 0 6px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text);
  font-size: 11px;
  white-space: nowrap;
}
.chl-fill.is-pending {
  background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 10px);
}

/* FOLDED, in the gym lobby (owner, 2026-09-28). A closed <details> whose
   summary carries the standing, in the same box language as the block
   strip's disclosure beside it: 52px summary, a chevron pointing down. */
.chl-fold { padding: 0; }
.chl-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 52px;
  padding: 8px 16px;
  cursor: pointer;
  list-style: none;
  touch-action: manipulation;
}
.chl-summary::-webkit-details-marker { display: none; }
.chl-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 14px; }
.chl-summary-k {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  min-width: 0;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.chl-summary-title { font-weight: 600; }
.chl-summary-met { color: var(--secondary); }
.chl-chev { flex: 0 0 auto; color: var(--secondary); transition: transform .15s ease; }
.chl-fold[open] .chl-chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .chl-chev { transition: none; } }
.chl-fold[open] { border-color: var(--accent-border); }
.chl-panel { padding: 0 16px 14px; }
.chl-panel > .chl-standing { display: flex; font-size: 13px; color: var(--text); font-weight: 600; }
/* Inside the program document the panel sits in the body's own column, last,
   under the foot buttons (owner, 2026-09-28). */
.program-body > .chl { margin-block: 28px 0; }

/* The coach's card. */
.bch { margin-block-start: 14px; scroll-margin-block-start: 24px; }
.bch-title { margin: 0; font-weight: 500; }
.bch-score { display: inline-flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; }
.bch-count { font-size: 12px; color: var(--secondary); font-variant-numeric: tabular-nums; }
.bch-block { margin-block-start: 6px; }
.bch-row { margin-block-start: 8px; }
.bch-on { display: inline-flex; align-items: center; gap: 8px; min-height: var(--control-h); min-width: 9rem; cursor: pointer; }
.bch-on input { margin: 0; }
.bch-num { display: inline-flex; align-items: center; gap: 6px; }
.bch .bch-in { width: 5rem; margin: 0; text-align: center; font-variant-numeric: tabular-nums; }
.bch-unit { font-size: 12.5px; color: var(--secondary); }
.bch-progress { font-size: 12.5px; color: var(--secondary); font-variant-numeric: tabular-nums; }
.bch-progress.is-met { color: var(--text); font-weight: 600; }
.bch-err { flex-basis: 100%; }
.bch-sub {
  margin: 16px 0 6px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--secondary);
}
.bch-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.bch-lift {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 12px;
  align-items: center;
  justify-content: space-between;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
}
.bch-lift-text { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; min-width: 0; }
.bch-lift-name { font-weight: 600; overflow-wrap: anywhere; }
.bch-lift-target { font-variant-numeric: tabular-nums; }
.bch-lift-best { font-size: 12px; color: var(--secondary); font-variant-numeric: tabular-nums; }
.bch-lift.is-met .bch-lift-best { color: var(--text); font-weight: 600; }
.bch-remove { margin: 0; }
.bch-none { margin: 0; }
.bch-carry-note { font-size: 12px; }
.bch-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.bch .bch-add .bch-in { width: 6rem; }

/*
 * NOTHING IN THIS CARD IS WIDER THAN THE CARD (the owner, 2026-09-28).
 *
 * A native select is as wide as its LONGEST option, and every option is an
 * exercise name plus its measure: "Single-Arm Dumbbell Rear-Foot-Elevated
 * Bulgarian Split Squat — weight × reps" drew the Exercise box 621px wide in
 * a 320px card at 390, and `.team-inline-form select`'s 9rem floor held it
 * there. The box takes the row it is given and ends in an ellipsis; the whole
 * name is in the opened list. Every item of the card's rows may shrink, and
 * every text in them may break.
 */
.bch-field-movement { flex: 1 1 18rem; max-width: min(100%, 32rem); }
.bch .bch-field-movement select { width: 100%; min-width: 0; max-width: 100%; }
.bch-row > * { min-width: 0; }
.bch-on span, .bch-progress, .bch-lift-best, .bch-carry-note { overflow-wrap: anywhere; }
.bch-lift-text { flex: 1 1 12rem; }
.bch-lift > * { max-width: 100%; }
@media (max-width: 560px) {
  /* A lift's words a line of their own, its button or note under them; a
     block goal's progress under its box, never read as the box's unit. */
  .bch-lift-text, .bch-progress { flex-basis: 100%; }
  /* The select a line of its own; the boxes its measure takes share the next,
     half each; the button the whole line under them — the assign and pause
     cards' shape, so a phone never gets a button stranded at a line's start. */
  .bch-field-movement { flex-basis: 100%; max-width: 100%; }
  .bch-add .bch-field:not(.bch-field-movement) { flex: 1 1 0; }
  .bch .bch-add .bch-in { width: 100%; }
  .bch-add > .btn { flex: 1 1 100%; }
}

/*
 * The client pane's target form shows only the boxes the chosen exercise
 * takes (weight and reps, reps, or seconds), keyed off the selected option's
 * data-shape. Cosmetic in the sense of the :has() note near the top: without
 * :has() all three show, and the server reads only the ones that apply.
 */
.bch-add:has(option[data-shape="reps"]:checked) .bch-field-weight,
.bch-add:has(option[data-shape="reps"]:checked) .bch-field-seconds,
.bch-add:has(option[data-shape="time"]:checked) .bch-field-weight,
.bch-add:has(option[data-shape="time"]:checked) .bch-field-reps,
.bch-add:has(option[data-shape="load"]:checked) .bch-field-seconds { display: none; }
@media (pointer: coarse), (max-width: 560px) {
  .bch-remove .btn { min-height: 44px; }
}

/* ------------------------------------- the builder's 🎯 on a row
 *
 * A fourth button in the ORDER column, so the header and every row still ask
 * the same of that auto track (see THE HEADINGS ROW IS A SEPARATE GRID): the
 * floor grows by one 36px button and its gap on a table that draws it. */
.team-table.bld.has-targets .bld-acts { min-width: 164px; }
.bld-act.is-target { position: relative; font-size: 15px; padding: 0 8px; }
/*
 * A 44px HIT AREA ON A 36px BUTTON (2026-10-01). At desk width every
 * `.bld-act` is 36px, and the 🎯 measured 36.7 × 36 — under the 44px floor
 * the phone card already gives it. A bigger BOX would widen the ORDER track
 * that the headings row is measured against (THE HEADINGS ROW IS A SEPARATE
 * GRID), so the box stays and the target grows round it: a pseudo-element
 * centred on the button, 44px each way, laid out by nothing. 3.65px each side
 * fits in the movers' 4px gap and 4px top and bottom in the row's 10px of
 * padding, so it takes no tap from ↑ or from a field. Where the button is
 * already 44 (the card, a coarse pointer) the `min()` makes it the button.
 */
.bld-act.is-target::after {
  content: "";
  position: absolute;
  inset-block: min(0px, calc((100% - 44px) / 2));
  inset-inline: min(0px, calc((100% - 44px) / 2));
}
.bld-act.is-target.is-set {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}
/* The editor opens as a row of its own under the exercise it is about. */
.bld-target {
  padding: 12px 14px;
  border-block-end: 1px solid var(--border);
  background: var(--panel);
}
.bld-target-head { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
.bld-target-row { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; min-width: 0; }
.bld-target-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; max-width: 100%; font-size: 12px; color: var(--secondary); }
.bld-target-field input,
.bld-target-field select { margin: 0; font-size: 16px; min-height: 38px; }
.bld-target-field input { inline-size: 6rem; text-align: center; font-variant-numeric: tabular-nums; }
/* A superset part's name can be longer than the editor is wide. */
.bld-target-field select { min-width: 0; max-width: 100%; text-overflow: ellipsis; }
.bld-target-note { margin: 8px 0 0; font-size: 12px; color: var(--secondary); overflow-wrap: anywhere; }
.bld-target-msg { margin: 8px 0 0; font-size: 12.5px; }
.bld-target-msg.is-error { color: var(--danger); }
/* The buttons: a row of their own at the foot of the editor (review.js). */
.bld-target-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 12px; }
.bld-target-actions .btn { margin: 0; }
@media (pointer: coarse), (max-width: 560px) {
  .bld-target-field input,
  .bld-target-field select { min-height: 44px; }
  .bld-target-actions .btn { min-height: 44px; }
}
/* ON A PHONE the fields stack, each the editor's full width, and the buttons
   share one full-width row under them — Save first, where the thumb ends. */
@media (max-width: 560px) {
  .bld-target-row { flex-direction: column; align-items: stretch; }
  .bld-target-field input,
  .bld-target-field select { inline-size: 100%; }
  .bld-target-actions .btn { flex: 1 1 0; min-width: max-content; }
}

/* ------------------------------------------- the lobby's second column
 *
 * "Side-bar" was the owner's word for OUT OF THE MAIN FLOW. On a phone that
 * can only mean the disclosure above; here there is room for the thing he
 * actually described, so from 60rem the lobby is two columns and the strip is
 * drawn open beside it with no disclosure at all.
 *
 * The main column keeps the measure it had — 614px at 1440 against the 608 it
 * had before, and 598 at the breakpoint itself — so the page grows to the side
 * of it rather than the resume card and the day list stretching to fill a desk
 * monitor. Below this width every rule here is inert and the lobby is exactly
 * the single column it has always been.
 */
@media (min-width: 60rem) {
  .page-gym-days { max-width: 61rem; }

  .gym-lobby {
    display: grid;
    grid-template-columns: minmax(0, 40rem) 16rem;
    column-gap: 26px;
    align-items: start;
  }
  .gym-lobby-main { grid-column: 1; grid-row: 1; }
  .gym-lobby-rest { grid-column: 1; grid-row: 2; }
  /* Beside both of them, and sticky so it stays with a long day list without
     following it off the bottom of a short one. */
  .gym-lobby-side {
    grid-column: 2;
    grid-row: 1 / span 2;
    position: sticky;
    inset-block-start: 18px;
  }

  /* No handle, because there is nothing to open: the strip is simply here. */
  .block-toggle { display: none; }
  .block-toggle ~ .block-strip { display: block; }
  .block-toggle ~ .block-strip .block-head { display: flex; }
  .gym-lobby-side .block-strip { margin-block-start: 0; }
}

/* ------------------------------------------- the generation run, made legible
 *
 * Two surfaces added 2026-09-01, both on the review pane: what Claude said it
 * did, and what kind of run the coach is about to start.
 */

/* Claude's summary of a run, beside the instructions it ran with. A disclosure
   because a high-effort summary is long and the run being reviewed is the one
   worth reading — `.team-run .team-reply-note` already caps the body at 22rem
   and scrolls it, so a talkative run cannot push the next one off screen. */
/* The chip in the head links here, so the card must not land under the sticky
   pane heading when it does. */
#validation { scroll-margin-block-start: 24px; }

.team-reasoning { margin-block-start: 16px; }
.team-reasoning > summary {
  cursor: pointer;
  padding: 7px 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--secondary);
  /* The row is 24px+ tall with this padding; the marker sits inside it. */
  min-height: 24px;
}
.team-reasoning > summary:hover,
.team-reasoning > summary:focus-visible { color: var(--text); }

/* Amend or rebuild, chosen before the run starts.
 *
 * Each option is a label wrapping its own radio, so the words are part of the
 * hit target and there is no dead zone beside the control. */
.team-runkind {
  margin-block-start: 12px;
  padding: 0;
  border: 0;
}
.team-runkind > legend {
  padding: 0;
  margin-block-end: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--secondary);
}
.team-runkind-opt {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  /* 44px of target on a phone, and the padding is what makes it, not a height
     that would clip a two-line help sentence. */
  padding: 9px 10px;
  border: 1px solid var(--border);
  border-radius: 9px;
  /*
   * A label that WRAPS its control is a row, not a caption, so it does not
   * want the bare `label` rule's 1rem of lead-in — that spaces a caption off
   * the field BELOW it, and there is no field below this one. Zeroed here
   * rather than as a global `label:has(> input)` exemption: that would reach
   * every wrapping label in the app, including ones whose spacing is
   * deliberate.
   */
  margin-block: 0 8px;
  cursor: pointer;
}
.team-runkind-opt:hover { border-color: var(--accent-border); }
/* Grouped on the label, but NOT a second ring: the radio inside is a real
   control and already gets the app's focus ring from the global input rule.
   Two outlines around one thing reads as a rendering fault. */
.team-runkind-opt:focus-within { border-color: var(--accent); }
.team-runkind-opt input { margin: 3px 0 0; flex: none; }
/* Flex children need a min-width of 0 or a long help sentence refuses to wrap. */
.team-runkind-opt > span:last-child { min-width: 0; display: block; }
.team-runkind-name { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
.team-runkind-help {
  display: block;
  margin-block-start: 3px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--secondary);
}


/* "Write a program by hand", folded, at the head of the Generate queue
   (2026-09-04). Folded because it is the uncommon way to start a program and
   the queue under it is the common one — an always-open form here would push
   the work down the page to serve the exception. */
.team-byhand { margin: 0 14px 10px; }

/*
 * AND IT HAS TO LOOK LIKE A CONTROL.
 *
 * It did not. `display: flex` on a <summary> suppresses the disclosure marker
 * in Chrome, and the rest of this rule was 13px of `--secondary` grey with no
 * border, no underline and no button chrome — so it rendered as a caption
 * sitting under the queue heading. The owner looked for it on a desktop and
 * reported that it "simply doesn't exist" (2026-09-06); it was on screen, in
 * the viewport, 245x44, the whole time.
 *
 * So the marker is drawn back by hand as a `+` — a triangle would say "there
 * is more text below", and what is below is a form — and the row gets the
 * border and the foreground colour of the secondary buttons it sits among. It
 * stays a disclosure rather than becoming a button because pressing it reveals
 * a form rather than doing anything, and because a <summary> is the one
 * control that works with the script off.
 */
.team-byhand > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  padding-inline: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
}
/* The marker `display: flex` took away. `content` rather than a real character
   in the template: it is punctuation belonging to this presentation, and a
   screen reader already announces the summary's own expanded state. */
.team-byhand > summary::before {
  content: '+';
  color: var(--accent);
  font-size: 16px;
  line-height: 1;
}
.team-byhand[open] > summary::before { content: '\2212'; }
.team-byhand > summary:hover { border-color: var(--accent); }
.team-byhand > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.team-byhand[open] > summary { margin-block-end: 8px; }
/* The form is 245px in the queue column at 1440 (274 less its own margins),
   and the select at its intrinsic width plus the Start button was 250px in
   English and 308 in Farsi — so the button sat under the select in both, on
   the one screen where the row had a whole column to itself. The select now
   grows from the 9rem floor every row select has to what the button leaves
   (166px / 154px at 1440). The BASIS is the floor, not `auto`: a wrapping
   row breaks lines on the items' bases before it shrinks anything, so an
   `auto` basis (the intrinsic 171/217) still put the button under. A client's
   name the select cannot fit is clipped by the browser's own ellipsis and
   read whole in the opened list. */
.team-byhand select { flex: 1 1 9rem; max-width: 100%; }

/* The same door on the request PANE, next to Generate — not folded, because
   here the coach is already standing on the request it answers and it is one
   of the two ways to answer it. A form rather than a row inside the Generate
   form: forms do not nest, so it needs its own top margin to sit where a
   sibling actions row would have. */
.team-byhand-row { margin-block-start: 14px; }
.team-byhand-row .field-help { margin-block-start: 6px; }
