/* Showup design system.
   --------------------------------------------------------------------------
   Ported from the Showup console handoff. Three rules carry the identity and
   everything below follows from them:

   1. Soft geometry on a near-black ground (owner decision 2026-08-03, chosen
      against two squarer alternatives seen side by side). Cards, controls and
      sheets round on a small radius scale (--r/--r-s/--r-pill) and sit with
      real depth (--shadow). This deliberately FORKS this app's look from the
      FORMWORK system BFT Scheduler uses — the two products still share type,
      voice and spacing, but not geometry. The brandmark and the Book tab tile
      stay square: they are the logo, not furniture.
   2. Depth is one shadow token, tone bars are inset. Separation comes from
      surface-on-ground contrast plus --shadow; drawn borders are reserved for
      affordances (inputs, buttons, tinted alerts). A status edge is an inset
      bar so it clips with the corner radius instead of tracing it.
   3. Two families and a hard split of duty. Archivo carries prose, names,
      headings and metric numerals; JetBrains Mono carries every label,
      timestamp, price, status, table cell and button — always uppercase. If a
      string is a value, it is Archivo. If it is a label about a value, it is
      mono.

   js/lib/theme.js resolves the user's preference (system/light/dark) to a
   concrete data-theme on <html>, so this file only needs one dark block rather
   than duplicating tokens across a prefers-color-scheme query and an attribute
   selector. */

/* Both families are variable fonts, which is why there are four files and not
   the seven a static build would need: one file per family per subset spans the
   whole weight axis, and the four weights this design uses are interpolated from
   it. Declaring the range rather than a single weight is what enables that — with
   `font-weight:400` the browser would synthesise 500/600/700 by smearing the
   400 outlines, which is the fake-bold look that makes uppercase mono labels
   muddy at 9.5px.

   swap, not block: an offline cold start should show the layout in a system font
   immediately rather than hold a blank screen. The fallback stacks in --sans and
   --mono are metric-adjacent enough that the reflow is a change of texture, not
   of line count.

   Subsets are split so the common case stays small. latin is 34K and covers
   everything the UI itself renders; latin-ext is another 32K and exists only for
   accented client names, so unicode-range keeps it unfetched until such a name
   is actually on screen. Chinese names fall through both to the system CJK face,
   as they already did on the CDN — neither family has CJK coverage. */
@font-face{
  font-family:'Archivo';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('fonts/archivo-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Archivo';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('fonts/archivo-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:100 800;
  font-display:swap;
  src:url('fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:100 800;
  font-display:swap;
  src:url('fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root{
  color-scheme:light;
  /* The page ground is a real grey, not an off-white. At #f1f2f3 it sat 1.12:1
     from the white cards — a difference you could measure and not see, so every
     card floated with no edge and the screen read as one undivided sheet. At
     #e3e6ea the same card is 1.25:1 lighter than the ground and the border is
     confirming a boundary the eye already found rather than drawing it alone.
     Everything else in this block follows from that one move: the ground got
     darker, so every grey printed on it had to get darker too. */
  /* THE GROUND IS NEAR-WHITE, NOT MID-GREY (owner, 2026-08-25: "how to optimize
     the light screen. is it better if it was all white?").
     Measured before changing it: the page was #dae0e7 and the hero card sitting
     on it was #eaeef2 — 1.14:1, a difference you can compute and cannot see. So
     the two brightest surfaces on the screen were the TOP BAR and the TAB BAR,
     both pure white, and the content between them was a grey slab. That is the
     wrong way round, and it is what makes the theme read as dull rather than
     bright: the chrome was lit and the work was not.
     All-white is not the answer either — this app leans on cards, and with no
     ground there is nothing for a card to be raised FROM. The answer is the one
     every legible light interface uses: a near-white ground, white cards, and a
     hairline doing the boundary work (see --edge). Lightening the ground only
     RAISES every text contrast measured below, so nothing here gets tighter.

     #f3f5f8 SPECIFICALLY (owner, 2026-08-26: "scheduler has the screen
     whiter"). It is the value the sibling scheduler uses, and the two products
     share a design system — so this is not a taste adjustment, it is the two
     apps agreeing on their ground. Its note there says the same thing this one
     does from the other side: "the design's --surface as the page ground: white
     panels need something to sit on". */
  --bg:#f3f5f8;
  /* Chrome — the brand bar and the tab bar. In light they are white, so the
     screen is bookended by the same surface the cards are made of and the grey
     ground is only ever the middle. It also gives the two smallest labels in
     the app — the tab words and the mode chip — a white backing to clear 4.5:1
     against, which they cannot do on the ground at this weight. Dark keeps
     chrome on the page colour: there the panels sit lighter than the page, so
     inverting the chrome would put the brightest band at the edges. */
  --chrome:#ffffff;
  --surface:#ffffff;
  /* The pressed/inset tint. Against a white card it now has to be visible in
     its own right — it used to borrow the difference from a near-white page. */
  --surface-2:#eaeef2;
  /* RAISED, in both themes — which --surface-2 is NOT. In dark it is lighter
     than the page and lifts a card off it; in light it is the pressed/inset
     tint and sits DOWN. Painting a hero with it therefore worked in dark and
     quietly failed in light, which is exactly what had happened. One token, one
     meaning: the surface a card is raised to. */
  --raised:#ffffff;
  /* SUNKEN — the mirror of --raised, and the half of the pair light mode
     actually needs. In dark you can say "you type here" by going lighter; in
     light you are already at white, so the only direction left is down. Named
     by MEANING like --raised, because --surface-2 is one step DARKER in light
     and one step LIGHTER in dark: the same trap that made a card sink here and
     lift there. */
  --sunken:#e9edf2;
  --inset:inset 0 1px 2px rgba(15,20,25,.07);
  --soft:#ccd3da;
  --ink:#111417;
  --ink-invert:#ffffff;
  /* Three greys, each set by the darkest ground it is ever printed on rather
     than by how it looks on white. --muted 9.2:1 white / 7.3:1 ground,
     --faint 6.0 / 4.8, --fainter 5.7 / 4.5. The last two sit close together
     because 4.5:1 on a #e3e6ea ground is a hard floor at around #5e6770 and
     there is no room below it — a fourth step would only be a fourth step that
     could not be read. */
  --muted:#3a424a;
  --faint:#4e5760;
  --fainter:#59626b;
  /* Three border weights now, not two. --line divides inside a panel,
     --line-strong bounds it, and --btn-bd outlines a control. The third one
     matters because a button whose only edge is its border has to clear 3:1
     to be found at all, while a card's border is confirming an edge the
     surface change already made. Dark collapses the first two.

     --line WAS #dde2e7, which is 1.2:1 on white — invisible (owner, 2026-08-16:
     "in the light mode, i cannot see any hairlines"). That was defensible while
     every hairline lived INSIDE a card: the surface change had already drawn
     the boundary and the line was only confirming it. Today and the queue have
     no cards, so the hairline IS the structure — the only thing separating the
     hero from the day, or one session from the next — and a divider carrying
     that much has to be seen. A token edit, not a per-screen override, because
     the change of meaning applies everywhere the app has gone cardless. */
  --line:#c4cdd6;
  --line-strong:#aeb7c0;
  --btn-bd:#828b95;
  /* THE CARD'S OWN EDGE, and the single biggest thing this theme was missing.
     A white card on a light grey ground is 1.33:1 — a difference you can
     measure and barely see — so the card was relying on a wide soft shadow that
     reads as a glow rather than as a boundary. A hairline ring plus a tight
     contact shadow is how every legible light interface separates a surface
     from its page. Transparent in dark, where the surface is already lighter
     than the ground and an edge would only add noise. */
  --edge:rgba(17,24,33,.11);
  /* Green, like dark's lime, because green means "selected / in view" in both
     themes — the accent used to be blue here. Dark enough (0.47) to clear
     4.5:1 on an almost-white page, where the lime itself would vanish. */
  --accent:oklch(0.55 0.11 170);
  --accent-ink:#ffffff;
  --neutral-mark:#8b939c;
  /* Filled emphasis — selected segment, active day. Inverting to --ink is the
     design's own idiom for "this one is chosen". */
  --solid:var(--accent);
  --on-solid:var(--accent-ink);
  --toast-bg:#16191c;
  --toast-ink:#ffffff;
  /* There is no separate "success" hue: positive movement is the accent. */
  --ok:var(--accent);
  /* Both dropped a step in lightness for the same reason the accent did. A
     status word is the only coloured thing on its row, so if it is the one
     string that cannot be read the row has no status at all. */
  --warn:oklch(0.53 0.145 62);
  /* One orange in this theme too — see the note in the dark block. */
  --bad:var(--warn);
  /* Tinted backings are derived rather than picked, so they track the accent
     and stay flat against whichever surface they land on. */
  --ok-bg:color-mix(in oklab,var(--ok) 12%,var(--surface));
  --warn-bg:color-mix(in oklab,var(--warn) 12%,var(--surface));
  --bad-bg:color-mix(in oklab,var(--bad) 12%,var(--surface));
  --scrim:rgba(22,25,28,.42);
  --r:0;
  --maxw:520px;
  --nav-h:78px;
  /* One height for every frozen head. Home's carries the most (month bar plus
     week strip) and sets the number; the others stand to it rather than each
     sizing to its own contents, so the chrome under the brand bar does not
     change depth as you move between tabs. */
  --freeze-h:121px;
  /* One height for every client card — the roster, Home's attention feed and
     Pay's unpaid list are the same object on three screens. */
  --card-h:59px;
  /* The two SOURCE-OF-TRUTH cards — a client on the roster, a session on the
     calendar — stand taller than the cards that merely summarise them. Home's
     attention feed and Pay's list are views onto these records, not the records
     themselves, and the height is what says so. */
  --card-h-lg:84px;
  /* Geometry scale for the 2026-08-03 fork: --r for surfaces (cards, sheets),
     --r-s for controls (inputs, buttons, icon tiles), --r-pill for status
     chips and inline actions. The brandmark and Book tile stay square. */
  --r:16px;
  --r-s:12px;
  --r-pill:999px;
  /* One pill height for the whole app: two of them plus a 6px gap fill a
     56px card rail exactly, which is where the number comes from. Anything
     that wants to look like a rail pill uses this rather than guessing. */
  --pill-h:25px;
  /* Depth: one downward shadow for resting surfaces, one upward for the
     sheet. Light needs far less ink than dark to read as lift. */
  /* TWO SHADOWS, not one. The wide ambient alone made a card look lit rather
     than lifted; the tight 1px contact shadow is the part the eye reads as an
     edge sitting on a surface. */
  --shadow:0 1px 2px rgba(15,20,25,.10),0 6px 18px rgba(15,20,25,.07);
  --shadow-up:0 -2px 4px rgba(15,20,25,.08),0 -14px 34px rgba(15,20,25,.12);
  /* Card gradient: lit faintly from above, the Bevel read. Light barely
     moves (white to a breath of grey); dark travels further. --hi is the
     1px top edge-light that sells the curvature — invisible in light. */
  /* LIGHT GETS DEPTH TOO (owner TODO, actioned 2026-08-07). Dark travelled a
     real gradient and carried a 6% edge light — the whole point of the Bevel
     fork — while light ran four values of near-white and set --hi to fully
     transparent. The result was two different apps: one with modelled cards
     and one with flat rectangles, and only the dark one looked designed.
     A longer travel, because white has less room to fall than near-black does
     and a 4-value ramp reads as no ramp at all. */
  /* THE FOOT OF A CARD MUST STAY ABOVE THE GROUND IT SITS ON. This ended on
     #eef1f5, which is now exactly --bg — so every card faded into the page at
     its bottom edge and the boundary died halfway down. Ends one step above the
     ground instead: still a gradient, still the same direction of travel, but a
     card is a card for its whole height. */
  --card-grad:linear-gradient(180deg,#ffffff 0%,#fafbfc 100%);
  /* The edge light is WHITE ON WHITE in dark and has to be the opposite here:
     what lifts a pale card off a pale ground is the top edge being brighter
     than the paper around it, which on white means a hairline of pure white
     over a card that is no longer pure white. */
  --hi:rgba(255,255,255,.9);
  /* Frosted glass for the floating bar island and the + circle. */
  --glass:rgba(255,255,255,.82);
  --glass-hi:rgba(15,20,25,.10);
  /* The grouping wash behind a banded region (Progress's body-data run).
     FLAT, not a fade (owner, 2026-08-16: "proerly wrap the coloar the same
     inteh background.. now it fades out") — a wash that evaporates halfway
     down leaves the last card sitting on bare page, and the grouping claim
     with it. One tint, edge to edge. */
  --band-grad:rgba(15,20,25,.06);
  /* Motion vocabulary. One curve, two beats: --dur-1 acknowledges a touch,
     --dur-2 moves furniture (sheets, screens). Anything longer than .25s on a
     phone reads as lag, not luxury. The reduced-motion guard below the type
     rules disables every animation and transition wholesale. */
  --ease:cubic-bezier(.2,.8,.2,1);
  --dur-1:.14s;
  --dur-2:.22s;
  /* Archivo and JetBrains Mono come from Google Fonts (see index.html) with a
     system fallback, so a cold offline start still renders in the right sizes
     and weights. Self-host both before this goes to real users. */
  --sans:Archivo,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
  --display:var(--sans);
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}

:root[data-theme=dark]{
  color-scheme:dark;
  /* Near-black, not slate (2026-08-03). The old #3a4046 ground was a mid-grey
     — exactly the band OLED panels, True Tone and auto-brightness shift most,
     which is why the app read "a shade lighter" on the phone than on the
     desk. Near-black renders stably on OLED, and the accents glow against it
     instead of merely contrasting. */
  --bg:#141619;
  /* Same value as --bg: dark's chrome has always been the page colour, and
     this token exists so light can differ without light needing an override
     rule for every bar. */
  --chrome:#141619;
  --surface:#1f2226;
  --surface-2:#25282d;
  /* Dark keeps its extra step: the hero lifts further than an ordinary card. */
  --raised:#25282d;
  /* NOT SIMPLY "DOWN" HERE, and the honest version matters. A boxed field sits
     on a card (--surface #1f2226) in some places and on a sheet (--bg #141619)
     in others, and dark is already near the floor — there is no value that is
     darker than both. #191c20 is darker than the card and lighter than the
     page, so it separates from either; which DIRECTION it separates in changes
     with the container, and in dark that is the convention rather than a
     compromise. Light has room below white, so there it is genuinely sunken. */
  --sunken:#191c20;
  --inset:inset 0 1px 2px rgba(0,0,0,.28);
  --soft:#33373d;
  --ink:#f6f7f8;
  --ink-invert:#141619;
  --muted:#d5dade;
  --faint:#c3c9ce;
  /* Was #a4abb2, which came to 3.47:1 on a panel — so the weekday letters in
     the day strip and the studio name under every session were the two things
     on the screen you could not quite read. Dark hits the same floor light
     does: below --faint there is only about one step left before a grey stops
     being text. */
  /* THE MOCK'S VALUE, not a brighter guess (2026-08-18). Ours was #bcc3ca where
     the design says #a8afb6 — a step hot, on the tier that carries the most
     text on any screen. It went unnoticed while the body ran 12% small; once
     the type came up to the kit there was simply more of it, and the owner read
     the result as "the white is a bit too sharp".
     The other three tiers already match the design exactly: ink #f6f7f8,
     muted #d5dade, faint #c3c9ce. This was the one that did not. */
  --fainter:#a8afb6;
  --line:#33373d;
  --line-strong:#3d4248;
  /* No edge in dark: the surface already sits lighter than the ground, so a
     ring would be a second boundary drawn over one the eye has already found. */
  --edge:transparent;
  --btn-bd:#4a5057;
  --accent:oklch(0.86 0.15 170);
  --accent-ink:#141619;
  --neutral-mark:#8b939b;
  --toast-bg:#f6f7f8;
  --toast-ink:#141619;
  --shadow:0 8px 24px rgba(0,0,0,.35);
  --shadow-up:0 -12px 32px rgba(0,0,0,.45);
  --card-grad:linear-gradient(180deg,#262a30 0%,#1b1e22 100%);
  --hi:rgba(255,255,255,.06);
  --glass:rgba(31,34,38,.72);
  --glass-hi:rgba(255,255,255,.09);
  --band-grad:rgba(255,255,255,.05);
  /* Both lifted for the same reason light's were lowered: a status word that
     cannot be read leaves the row with no status. --bad moved furthest, from
     0.72 to 0.82, because a red dark enough to look like a warning on paper is
     the one hue that goes illegible fastest on a dark panel. */
  /* Two colours carry meaning in this app and no others (owner's rule):
     ACCENT green says "selected / in view", ORANGE says "needs attention".
     There is no pink and no red — what used to be --bad is now the deeper,
     hotter end of the same orange, so severity still reads as severity
     without introducing a third hue to learn. */
  --warn:oklch(0.83 0.15 70);
  /* ONE orange, not two: a slightly hotter "bad" beside "warn" read as an
     inconsistency rather than a severity, so attention has a single colour. */
  --bad:var(--warn);
  --scrim:rgba(0,0,0,.55);
}

*,*::before,*::after{box-sizing:border-box}
/* SCROLL DOWN, NEVER SIDEWAYS (owner, 2026-08-17: "fix the pages on mobile so
   it won't jiggle left right up down, only scroll").
   The drift comes from children that are WIDER than the viewport — a long mono
   meta line, a wide sheet, a chip row — and the browser then lets the whole
   page slide to reveal them. Locking overflow-x here stops the slide; the
   min-width:0 below stops it being created, because a flex child defaults to
   min-width:auto and refuses to shrink below its content.
   overscroll-behavior-y kills the rubber-band bounce at the ends, which is the
   up-down half of the jiggle. */
/* CLIP, NOT HIDDEN. overflow-x:hidden computes overflow-y to auto, which turns
   html and body into scroll containers — and a position:sticky element sticks
   to its nearest scrolling ancestor, so the top bar started sticking to a box
   that never scrolls and rode away with the content. The page also stopped
   scrolling at the document level entirely.
   overflow:clip clips without establishing a scroll container, which is the
   whole reason it exists. Sticky survives. */
html{overflow-x:clip}
body{overflow-x:clip;overscroll-behavior-y:none}
/* The actual CAUSE, not just the symptom: a flex or grid child defaults to
   min-width:auto, so a long unbreakable string makes its row wider than the
   screen and pushes the page out. Every row in this app is a flex line with a
   text column, and these are those columns. */
.tb-row-b,.cl-row-b,.bk-body,.q-row .tb-row-b,.pg-m{min-width:0}
.tb-row-t,.tb-row-m,.cl-row-t,.cl-row-m,.bk-name,.bk-meta{min-width:0;overflow-wrap:anywhere}
/* Anything that genuinely has to be wide scrolls INSIDE itself rather than
   taking the page with it. */
.pg-trend,.mh,.logset{max-width:100%}
html,body{margin:0;padding:0}
/* No rubber-band: the iOS bounce drags the whole page — sticky header
   included — which reads as the bar "not being locked". Nothing here needs
   pull-to-refresh; updates announce themselves with a reload bar instead. */
html,body{overscroll-behavior-y:none}
html{-webkit-text-size-adjust:100%}
body{
  font-family:var(--sans);
  color:var(--ink);
  background:var(--bg);
  font-size:var(--ds-body);
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}
h1,h2,h3,.big-amount,.stat .n{font-family:var(--display);letter-spacing:-.02em}
/* Times, money, counts and measurements sit in columns and change in place,
   so they need fixed-width figures or the layout jitters. Archivo carries the
   metric numerals, and its tabular set is what keeps them aligned. */
button{font:inherit;color:inherit;background:none;border:none;padding:0;margin:0;cursor:pointer}
input,textarea,select{font:inherit;color:inherit}
a{color:var(--accent);text-decoration:none}
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ---------- mono label roles ----------
   The three sizes the handoff uses, named by where they sit rather than by
   size, so a rename of the scale does not mean hunting down call sites. */
.eyebrow{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--faint)}
.eyebrow-sm{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.meta{font:400 var(--ds-label)/1.5 var(--mono);letter-spacing:.04em;color:var(--faint)}

/* ---------- layout shell ---------- */
#app{min-height:100vh}
.shell{
  max-width:var(--maxw);
  margin:0 auto;
  min-height:100vh;
  background:var(--bg);
  display:flex;
  flex-direction:column;
  position:relative;
}
/* Opaque, not glass. A blurred bar is a soft edge, and this system does not
   have soft edges — the 1px rule underneath does the separating, and it is the
   only thing that does: the bar sits on the page background rather than on a
   lighter panel of its own, so nothing competes with the title. */
/* FROZEN, AND HIGHER (owner, 2026-08-18). sticky only pins while an ancestor
   is the scroll container; the moment a view puts its own scroller in between,
   the bar rides up with the content and the coach loses the wordmark, the
   avatar and the way out. fixed pins it to the viewport whatever any view does
   underneath. The shell reserves the height via --top-h, which app.js already
   publishes on every render and on resize.
   The top padding drops from 13px to 6px above the safe area — the inset
   already clears the notch, and 13px on top of it was a band of nothing. */
.topbar{
  position:sticky;top:0;z-index:20;
  background:var(--chrome);
  border-bottom:1px solid var(--line-strong);
  /* Bottom > top: the top pad sits under the status bar and the brand row has
     its own slack, while the day line ends flush at its baseline and needs the
     hairline held off it. */
  /* 16px, MATCHING .main (owner, 2026-08-28: "the logo banner contains useful
     info and functions, should we make that a bit wider?"). It was 18 while the
     content below it was 16 — so the one bar on the screen carrying the day's
     facts was NARROWER than the list it sits over, and its left half was
     clipping. Aligning the two also lines the wordmark up with the first row
     under it, which is what a coach reads as "the same column". */
  padding:calc(env(safe-area-inset-top) + 8px) 16px 13px;
  display:flex;align-items:center;justify-content:space-between;gap:12px;
}
/* A bar carrying a second row stacks instead: the brand row keeps the old
   layout, and whatever rides under it gets the full width. app.js measures
   the bar's real height into --top-h every render, so the taller bar moves
   the sticky layers below it without anything here knowing the number. */
.topbar-2{display:block}
/* THE ROW IS ALWAYS A ROW. This rule was scoped to .topbar-2, so every screen
   WITHOUT a second line — Clients, Calendar, Library, Money — put the wordmark
   and the coach's avatar in a column, one under the other. The bar is flex, so
   its single .topbar-row child stretched and its own children stacked. A
   layout rule that only holds in the case it was written for is not a layout
   rule. */
.topbar-row{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%}
/* The client's own row, under the brand band. It carries the name, the standing
   line and the status pill — and no controls, which is the point: the way back
   lives in the band above rather than in line with the person's name. */
.cl-head-row{margin-top:10px;align-items:flex-start}
.cl-head-row .cl-state{margin-top:2px}
/* The title takes the slack so anything else in the bar — a back arrow before
   it, a button after it — sits against its own edge rather than being spaced
   out around the middle. */
.tb-title{flex:1;min-width:0}
/* The screen's name, riding beside the wordmark after a middle dot. Mono and
   quieter than the brand so the pair reads as "product · where you are". */
/* Small and quiet beside the wordmark — the brand is the loud half, the
   screen name is a caption on it. Same 12px on all four pages. */
.brand-page{display:flex;align-items:center;gap:7px;color:var(--faint);
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
/* Back to sentence case. The five destinations now carry their title in
   .pagehead below the brand bar, and this rule is left serving the screens you
   arrive at from somewhere specific — a client, settings — which should be set
   the same way as the pageheads they were opened from. */
.topbar h1{font-size:var(--ds-figure);font-weight:700;line-height:1.15;letter-spacing:-.02em;margin:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ONE LINE. "+852 9123 4567 · 5 SESSIONS LEFT" broke after SESSIONS at phone
   width and pushed the tab row down with it. The line is three short facts and
   a phone number; it shrinks to fit rather than wrapping, because a name and
   its standing are read as one object. */
.topbar .sub{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.05em;text-transform:uppercase;color:var(--faint);margin-top:5px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media (max-width:420px){.topbar .sub{font-size:var(--ds-micro);letter-spacing:.03em}}
.topbar .sub-over{margin:0 0 6px}
.main{flex:1;padding:4px 16px calc(var(--nav-h) + env(safe-area-inset-bottom) + 34px)}

/* ---------- page head ----------
   Large, sentence case, Archivo. It is the one line of display type on the
   screen and it is a phrase a coach would say — "Schedule", "Open the day" —
   so it is set as language rather than as a caption. The tab bar underneath
   stays in caps: those are five fixed labels on a control, not sentences. */
.pagehead{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  padding:13px 0 11px}
/* Set in the tab bar's own type, one size up. The 28px version was a magazine
   headline for a word the coach already knows — they pressed the tab, they know
   which screen they are on. Small caps names the screen and gives the height
   back to the content. */
.pagehead h1{font:600 var(--ds-strong)/1.2 var(--sans);letter-spacing:.09em;text-transform:uppercase;
  margin:0;color:var(--ink);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pagehead-r{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--faint);flex:none}

/* ---------- week bar and day strip ----------
   Seven cells, Monday to Sunday, and no scrolling. The old strip ran thirty
   days sideways, which meant the answer to "what does this week look like"
   depended on where you had last dragged it. A fixed week is a shape you can
   learn; the arrows move it a week at a time and the label says which. */
/* Flex, not a 3-column grid: the Today button appears only when the selected
   day is not today, so the bar has to hold three children or four. */
.cal-nav{display:flex;gap:8px;margin-bottom:8px}
.cal-nav .cal-nav-mid{flex:1;min-width:0}
/* A word, not a chevron — the other three controls are arrows and a month, and
   a fourth arrow would have to mean something the first three do not. */
.cal-today{flex:none;display:flex;align-items:center;justify-content:center;min-width:58px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--accent);border-color:var(--accent)}
/* On today it IS the call-off control, so it wears the colour of what it does.
   Same slot, same size — only the meaning changes, which is why the min-width
   above is fixed: the other three controls must not shuffle when it swaps. */
.cal-today.is-danger{color:var(--warn);border-color:var(--warn)}

/* AUG 2026 · WK 34 ON ONE ROW (owner). At 16px with .1em tracking the month,
   the year and the week ran past the pill on a 375px screen and wrapped to
   three lines, which pushed the whole strip down. Smaller, tighter, and told
   not to wrap — the numbers are still the largest thing in the bar. */
.cal-nav .cal-nav-mid{}
/* 13px, not 9px. This is the one bar in the app that gets pressed dozens of
   times a day — the week you are looking at is changed from here — and at 9px
   the whole thing stood 35px tall, so all three of its controls were under a
   thumb's worth. The pseudo-element trick used elsewhere does not apply: these
   three sit edge to edge, so overlapping targets would steal each other's
   taps. The bar just gets to be the size it is used at. */
/* Three separate pills rather than one segmented bar (owner's reference):
   each control is its own rounded surface, which is why they can now carry
   their own hit area without stealing a neighbour's tap. */
.cal-nav > *{padding:6px 13px;color:var(--muted);border:1px solid var(--line);
  border-radius:var(--r-pill);background:var(--surface);background-image:var(--card-grad);
  display:flex;align-items:center;justify-content:center}
/* The Book screen puts .icon-btn in this bar, and .icon-btn is a fixed 32px box.
   32px minus the 28px of padding above left 4px of content, which flattened the
   chevron to a 2px sliver. The bar sizes its own children; anything sitting in
   it gives up its own dimensions. */
.cal-nav > .icon-btn{width:auto;height:auto}
.cal-nav svg{flex:none}
/* The month is set at the same size as the day numerals it labels, so the
   two rows read as one instrument — a skinnier bar carrying bigger type. */
.cal-nav .cal-nav-mid{
  gap:7px;font:500 var(--ds-strong)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink);white-space:nowrap;
}
/* The ISO week, riding quietly beside the month it qualifies */
.cal-wk{font-size:var(--ds-small);color:var(--faint);letter-spacing:.1em}

/* Seven separate cards with air between them, not a fused strip: a day is a
   thing you tap, and the gaps are what make that legible at a glance. */
.wk-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
.wk-strip.hidden{display:none}
/* Softly rounded like every other surface in this app — the day cells were
   the only near-square things in the system, and the brandmark is meant to
   be the one intentional square. */
.wk-d{
  /* Half-pixel borders: on a phone's 2–3× screen this is a true hairline,
     and every cell carries the same width so the selected one cannot shift
     its neighbours by half a pixel. */
  border:.5px solid var(--line);
  background:var(--surface);padding:9px 2px 8px;
  display:flex;flex-direction:column;align-items:center;gap:5px;}
.wk-d-w{font:500 var(--ds-micro)/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--fainter)}
/* SCALED UNDER THE MONTH BAR (owner, 2026-08-18: "reduce to match the new
   month bar"). The bar came down to ~14px and the day numbers stayed at 16,
   so the strip was shouting over the thing that labels it. Everything on this
   screen now sits at or below the bar: the date strip reads first, the day
   heading second, the rows third. */
.wk-d-n{font:600 var(--ds-small)/1 var(--mono);letter-spacing:.02em;color:var(--ink);font-variant-numeric:tabular-nums}
/* The day's booked count, under the numeral — transparent at zero so all
   seven cells keep the same height. */
.wk-d-ct{flex:none;font:500 var(--ds-label)/1 var(--mono);color:var(--faint)}
.wk-d-ct.blank{color:transparent}
/* The weekday label never takes colour — it is the same word on every cell
   and colouring it just doubles a signal the numeral already carries. */
.wk-d.today .wk-d-n{color:var(--accent)}
/* A general holiday, in the red every wall calendar in Hong Kong prints them
   in — and a mark in the corner, so it does not rest on colour alone. Today
   wins the numeral if the two land on the same day: which day it is now is the
   more urgent fact, and the corner mark still says holiday. */
/* A public holiday colours its numeral and nothing else — the corner dot was
   a second mark for the same fact, and the day itself already names the
   holiday in the line under the strip. */
.wk-d.hol .wk-d-n{color:var(--warn)}
.wk-d.today.hol .wk-d-n{color:var(--accent)}
/* Selected is a lime RING, not an inverted block (owner's reference): the
   ground stays dark so the seven cells read as one row, and the accent marks
   which one is being read rather than repainting it. */
/* A hairline ring, and nothing else: the border alone says which day is
   being read. The numeral stays ink — green on a date means ONE thing in
   this app, and that is today. */
/* THE SELECTED DAY IS WHITE, NOT GREEN (owner, 2026-08-17: "should it just be
   white, no need for green border?").
   Right — accent means "act on this", and the day you are already looking at
   is not a thing to act on, it is where you are. Spending the accent on it
   competed with UP NEXT, OFFER and BOOK, which are the rows that do want the
   tap. A plain bright edge says "here" without asking for anything. */
/* FILLED WHITE (owner, 2026-08-18: "can we make the selected date a white
   filled selection?"). The ring alone was a hairline change on a strip of
   seven identical cells — findable, but you had to look for it. A filled cell
   is the one thing on the screen you cannot miss, and it costs no colour the
   app has spent elsewhere: --ink is the page's own text, so the strip inverts
   rather than lighting up. Everything inside it flips to the ground colour,
   including the day count and the today/holiday numerals — a green numeral on
   a white cell would be unreadable and would say "act on this" about the day
   you are already on. */
/* ONE MARK FOR "CHOSEN", AND IT IS THE ACCENT (owner, 2026-08-26: "review the
   consistency in how a selected item looks.. roster gets an underline, calendar
   is a highlighted cell").
   Measured across the app first: the client tabs underline in the accent, the
   month cell rings in the accent, the filter segment borders in the accent —
   and this one alone inverted to plain ink, so the day a coach is reading was
   the only selected thing in the app that did not use the colour selection is
   made of. The SHAPE still varies with the control, which is right — a fill on
   a cell, a rule under a tab, a ring on a date — but the colour saying "this
   one" is now the same everywhere. */
.wk-d.on{border-color:var(--accent);background:var(--accent);background-image:none}
.wk-d.on .wk-d-n,.wk-d.on .wk-d-w,.wk-d.on .wk-d-ct{color:var(--on-solid)}
.wk-d.on .wk-d-ct.blank{color:transparent}
.wk-d.on.today .wk-d-n,.wk-d.on.hol .wk-d-n{color:var(--on-solid)}
/* Today, when it is NOT the day being read, keeps its accent numeral. When it
   is, the fill says where you are and the accent is not needed twice. */
/* :not(.on) — a closed day that is also the selected day still has to be read.
   The dim was being applied on top of the inverted fill, so the one cell the
   coach had deliberately tapped ended up the least legible thing in the strip:
   a 60%-opacity grey printed on near-black. Closed-and-selected keeps the
   inversion and drops one step to --soft instead, which reads as quieter
   without becoming unreadable. */
.wk-d.off:not(.on) .wk-d-n{color:var(--fainter);opacity:.6}

/* The line between the week and the day: which day, what hours it runs, and how
   much of it is sold. Everything a coach would otherwise count off the cards. */
.dayline{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--faint);padding:12px 1px 10px}
/* THE DAY STARTS HERE (owner, 2026-08-17: "a THICKER line above Today, below
   waitlist.. and increase the space above today").
   The waitlist and the day are two different subjects — one is people with no
   hour, the other is the hour-by-hour — and they were separated by the same
   hairline that separates two rows INSIDE a list. So the eye read straight
   through the boundary and the waitlist looked like the day's first band.
   A heavier rule and real air above it: this is the one place on the screen
   where the reader is meant to change what they are thinking about. The rule
   is on the DAY heading rather than under the waitlist, so it appears whether
   or not anyone is waiting. */
.cal-day-sep{margin-top:26px;padding-top:0;border-top:2px solid var(--line-strong)}
.cal-day-sep.dayline{padding-top:16px}
/* The day's own name earns its size — it is the anchor for everything under
   it, and at 10.5px mono it was quieter than the row times it introduces. */
.cal-day-sep .grow{font:600 var(--ds-label)/1.3 var(--mono);letter-spacing:.09em;color:var(--muted)}
.cal-day-sep .grow b{color:var(--ink)}
.dayline b{font-weight:500;color:var(--muted)}

/* ---------- list controls ----------
   Search and its filters are one apparatus for narrowing a list, so they sit in
   one block with a rule under it. Without the rule the filter row floated
   between the header and the first card and looked like it belonged to neither. */
/* THE FREEZE RULE: the controls that frame a list — the week you are in, the
   filters you set, the figure you came for — stay put while the list itself
   scrolls under them. They stick directly below the brand bar, whose measured
   height arrives as --top-h from app.js. The background is opaque and the
   padding matches .main so cards pass cleanly underneath. */
/* Pulled up a hair so it OVERLAPS the brand bar instead of butting against it.
   Both are sticky, and the bar's real height is fractional (safe-area inset +
   padding); rounding --top-h to a whole pixel left a sub-pixel seam that
   shimmered as rows scrolled through it. An overlap has no seam to shimmer. */
.freeze{position:sticky;top:calc(var(--top-h,56px) - 1px);z-index:15;
  /* SAME GROUND AS THE BRAND BAR. This is what stops the bump. Two sticky
     layers in different colours meet at a moving boundary, and the browser
     resolves that boundary to a whole pixel a frame at a time — so the seam
     between --chrome and --bg walked up and down by a pixel as you scrolled.
     Painting both slabs the same colour leaves no seam to walk. (will-change:
     transform used to live here and made it worse, not better: promoting a
     sticky element to its own layer hands its offset to a compositor that
     updates a frame behind the scroll.) */
  background:var(--chrome);
  /* Every frozen head stands the same height whatever it carries, so moving
     between tabs does not resize the chrome under the brand bar. */
  min-height:var(--freeze-h);
  display:flex;flex-direction:column;justify-content:center;
  margin:0 -16px 14px;padding:6px 16px 10px;
  /* every frozen head is closed by one hairline, so the list visibly passes
     UNDER it rather than merging into it */
  border-bottom:1px solid var(--line-strong)}
/* One slab, one closing edge. With the frozen head now painted in --chrome it
   is continuous with the bar above it, so the bar's own rule would be a line
   drawn through the middle of a single surface. The head's rule closes the
   whole thing. */
.shell:has(.freeze) .topbar{border-bottom-color:transparent}

/* The band device does NOT belong here, and the two are solving opposite
   problems. A band is a soft-edged wash that says "these things belong
   together" — it deliberately dies to nothing so it never closes. A frozen
   head needs the exact opposite: a hard, unambiguous edge, because live
   content is sliding underneath it and the eye has to know precisely where the
   pinned layer stops. Swap the rule for a fade and the topmost scrolling row
   bleeds through the gradient, which is the one thing the rule exists to stop.
   What the band DOES have to offer is depth, so it lends that: a short fall of
   shadow below the line, so the list reads as passing beneath a raised bar
   rather than being cut off by a drawn one. The line stays; it just no longer
   has to do the job alone. */
.freeze::after{
  content:'';position:absolute;left:0;right:0;top:100%;height:10px;
  pointer-events:none;
  background:linear-gradient(180deg,rgba(0,0,0,.22),transparent);
}

.list-controls{padding:10px 0 14px;border-bottom:1px solid var(--line-strong);margin-bottom:14px}
.list-controls.freeze{padding:8px 16px 10px}
.list-controls.freeze .seg{margin-top:8px}
/* The library's two framing controls, stacked in one frozen block — the same
   tight rule the clients filters sit above. */
.lib-head{display:flex;flex-direction:column;gap:8px;padding-bottom:10px}
.lib-head .mb-2{margin-bottom:0}

/* Pay's frozen head is the client header's two-figure strip, reused. It spent
   several iterations as bespoke pay-* classes — an owed phrase, a collected
   grid, a heading over it — and every one of them was a worse version of a
   component this app already had. All that is left here is the wrapper. */
.pay-head{display:block;padding-top:10px;padding-bottom:12px}
/* The qualifier under a stat's figure: money gone stale, or the year to date.
   Never the warn colour — the figure above it already carries that, and two
   coloured lines make the smaller one shout over the total it qualifies. */
/* ONE LINE, ALWAYS. This sits inside a .freeze, and every frozen head in the
   app is the same height by contract — a qualifier that wraps on a big enough
   figure silently makes Pay's head taller than every other screen's. */
.stat-sub{font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  color:var(--faint);margin-top:6px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.list-controls .seg{margin-top:10px}

/* ---------- the right-hand end of a list row ----------
   A value with a word under it. The booking sheet, the payments list and the
   roster were three different shapes for the same job — a person on the left,
   a figure on the right — so a coach had to learn the list again on each tab.
   One class, and the count, the amount and the status all land in the same
   column at the same size. */
.row-end{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:5px;text-align:right}
.row-val{font:500 var(--ds-small)/1 var(--mono);letter-spacing:.02em;
  font-variant-numeric:tabular-nums;color:var(--ink)}
.row-lab{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap}
/* Tone as a word, not as a rule down the side. The status is the only thing on
   the row that changes colour, so nothing else has to be read to find it. */
/* GOOD NEWS IS AS LOUD AS BAD NEWS (owner TODO, actioned 2026-08-07). Every
   warning state in this app has a colour and every healthy one was grey, so a
   screen could only ever tell a coach what was wrong — "Active" and "Renew" sat
   in the same ink and only one of them was doing any work. The same word was
   also green as a session flag and grey as a pill, so one status had two
   appearances depending on which surface it landed on. */
.t-ok{color:var(--accent)}
.t-warn{color:var(--warn)}
.t-bad{color:var(--bad)}

/* ---------- brand bar ----------
   The wordmark is mono and letterspaced because it is the one place the app
   says its own name, and the mono face is the half of the type system that
   does labels. Set in Archivo it would read as a heading — as if SHOWUP were
   the title of the screen rather than the name over the door. */
.brand{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
/* The back chevron standing in for the mark: same footprint, so the word after
   it does not shift between a screen that has one and a screen that does not. */
.brand > .icon-btn{flex:none;width:30px;height:30px;margin-left:-6px}
/* THE MARK (owner, 2026-08-12). Was a plain lime square standing in for a
   logo; it is the sign-in stamp now. As a background-image rather than inline
   SVG so one file serves every surface — coach bar, portal, favicon — and a
   redraw is one file, not four copies to keep in step. Slightly larger than
   the square was: the stamp carries its own padding in its viewBox, so at
   17px the tick was tiny. */
/* The mark alone, leading a titled bar — see topbar(). Slightly smaller than
   the branded row's, because there it carries the word and here it sits beside
   a heading that is already doing the naming. */
.tb-mark-only{width:19px;height:19px;margin-right:11px}
.brand-mark{width:22px;height:22px;flex:none;
  background:url('icons/mark.svg') center/contain no-repeat}
.brand-word{font:700 var(--ds-strong)/1 var(--mono);letter-spacing:.15em;text-transform:uppercase;color:var(--ink);flex:none}
.topbar-r{flex:none;}
/* A statement, not a control: no hover, no pointer, and the muted ink every
   other read-only label in the app uses. */
/* One corner control: the face above, the role beneath it — the same
   icon-over-label anatomy the tab bar uses, so the fifth destination reads
   like the other four. */
.me-btn{display:flex;flex-direction:column;align-items:center;gap:4px;
  background:none;border:0;padding:0;flex:none}
/* Round, like .avatar — the two marks in the system that stand for a person
   rather than for a box. */
/* OUTLINED, where a client monogram is FILLED. The coach is the one person in
   this app who is not a client, and a filled circle of initials drawn from the
   client palette said the opposite — it could even come out the same colour as
   someone on the roster. A ring is a different class of object at a glance,
   costs no colour, and keeps the corner reading as chrome rather than data. */
/* BIGGER (owner). It is the only way into settings and the only thing in the
   bar that is the coach themselves, and at 30px it was the smallest tap target
   on the screen. */
/* Quieter than the avatar beside it: leaving is a real action but it is not
   the one a coach reaches for twenty times a day, and two equally loud circles
   in the corner make neither of them obvious. */
.exit-btn{width:34px;height:34px;border-radius:50%;flex:none;background:none;
  border:1px solid transparent;color:var(--faint);
  display:flex;align-items:center;justify-content:center}
.exit-btn:active{color:var(--ink)}
.topbar-r{display:flex;align-items:center;gap:6px}

.me{
  width:36px;height:36px;border-radius:50%;flex:none;
  background:none;border:1px solid var(--line-strong);color:var(--ink);
  display:flex;align-items:center;justify-content:center;
  /* A single letter has the room two never had, so it takes it — and with no
     second glyph to space against, the tracking goes back to zero. */
  font:700 var(--ds-small)/1 var(--mono);letter-spacing:0;
}
.me-btn:active .me{background:var(--surface-2)}
/* A segmented control whose question is not live yet — the repeat cadence on
   Block time, dead until the box beside it is ticked. Dimmed rather than
   hidden: appearing on tick would move the button under the coach's thumb. */
.seg.is-off{opacity:.4;pointer-events:none}
/* With no name saved there are no initials to show, so it falls back to the
   generic figure and takes the neutral fill rather than a colour derived from
   an empty string. */
.me svg{width:17px;height:17px;stroke-width:1.9;color:var(--ink)}

/* The date the brand bar no longer carries. It sits on the content rather than
   in the header because it belongs to what is below it — this is the day the
   run of day is for. */

/* ---------- bottom nav ----------
   Copied from Bevel's bar (owner call, from a screenshot of the real thing):
   a frosted pill island floats over the content carrying the four places —
   icon over a sentence-case label, the active one on a soft pill highlight —
   and the + is its own frosted circle to the RIGHT. The wrapper itself is
   transparent and lets touches through; only the island and the circle are
   real surfaces. Content scrolls behind the glass, which is the whole point
   of the blur. */
.tabbar{
  position:fixed;left:0;right:0;bottom:0;z-index:30;
  max-width:var(--maxw);margin:0 auto;
  display:flex;align-items:center;justify-content:center;gap:10px;
  /* LOWER (owner, 2026-08-18: "move the bar a bit lower on mobile, there is a
     bit of room"). The island was floating 4px above the safe area on top of
     whatever the phone reserves for its own home indicator, which on a device
     with no indicator at all left a visible band of nothing under it. Sitting
     it down onto the safe area gives the list back the height it was wasting
     and puts the bar where a thumb already rests. */
  padding:0 14px calc(env(safe-area-inset-bottom) * 0.5);}
/* LOCKED, not floating (owner, 2026-08-16: "please lock the bottom bar in
   place", with a mockup showing a solid band). The wrapper is transparent so
   touches pass through it, which meant rows scrolled visibly BETWEEN the
   island and the +, and the bar read as drifting over the content. It sits on
   its own ground now, fenced from the list by a hairline — the same device
   the top bar uses, and the reason both of them stay put while the middle of
   the screen moves. The island keeps its own surface INSIDE that band. */
/* Lower on the glass (owner: "move the bar down more"). The caption under the
   + was holding the whole bar up off the bottom edge; without it the island
   can sit where the thumb already is. */
.tabbar{
  background:var(--chrome);
  border-top:1px solid var(--line);
  padding-top:8px;
}
/* The wrapper is the ground now, so it has to take its own touches back —
   only the gaps between the controls stay transparent to them. */
.tabbar{pointer-events:auto}
.tab-island,.tab-fab{
  pointer-events:auto;
  background:var(--glass);
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  box-shadow:var(--shadow);
}
.tab-island{flex:1;max-width:340px;display:flex;border-radius:var(--r-pill);padding:5px}
/* AIR BETWEEN THE ICON AND ITS WORD (owner, 2026-08-18). At 3px they read as
   one smudged object; the label is there to teach the icon and it can only do
   that if the eye registers them as two things. */
.tab{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;
  padding:7px 4px;border-radius:var(--r-pill);
  color:var(--faint);font:600 var(--ds-small)/1 var(--sans);letter-spacing:.01em;
}
.tab.active{background:var(--glass-hi);color:var(--ink)}
.tab-ico{position:relative;display:flex}
/* Weight rather than colour. Green here means selected / in view / on track,
   and a permanently lime circle would be the one green thing on screen not
   describing a state — it would out-shout the UP NEXT tag, which is the only
   accent on Home that earns it. A ring and a heavier stroke say "control"
   just as clearly, and stay honest about a button whose job changes with the
   screen. */
.tab-fab{flex:none;justify-content:center;color:var(--ink);}
/* The glyph carries the emphasis: a 2.4 stroke against the tab icons' 1.8. */
.tab-fab svg{stroke-width:2.4}
.tab-fab:active{}
/* Icon tile inside the create sheet's rows. */
/* Pinned to the mark's top-right rather than nudged there by margins. On an
   11px dot the badge has to sit clear of the dot itself, or the count and the
   mark merge into one smudge at arm's length. */
.tab .badge{
  position:absolute;top:-5px;left:13px;
  /* --ink-invert, not #fff. --bad is a dark red on a light page and a light red
     on a dark one, so a hardcoded white label was legible in exactly one of the
     two themes. The inverted ink follows the fill. */
  background:var(--bad);color:var(--ink-invert);font:700 var(--ds-label)/1 var(--mono);
  min-width:15px;height:15px;padding:0 4px;border-radius:var(--r-pill);
  display:flex;align-items:center;justify-content:center;
}
.tab.centre .badge{top:-6px;left:auto;right:-7px}

/* ---------- desktop breakpoint ---------- */
/* THE RAIL, not a second app (owner, 2026-08-16: "should we also build a web
   version?" -> "desktop i mean"). The old rule was one layout at every width
   — a coach on a laptop got the same phone-width column, centred in a mostly
   empty page, because a second set of screens kept in step with the first
   was worse than no desktop mode at all. That still holds: nothing here
   forks the markup or duplicates a view's logic.

   What changes at 900px is the one thing that is actually wrong on a big
   screen — a bar built for a thumb, reached with a mouse instead — and the
   column gets the width back that a phone never had to give up. The five
   destinations move from the bottom bar into a left rail, in the same DOM
   order, same click handlers, same badge logic; only the CSS asks them to
   stack a different way. What each screen DOES with the freed width past
   that is a per-screen decision, made screen by screen, starting with Book.
*/
@media (min-width:900px){
  :root{ --nav-h:0px; --maxw:760px; --rail-w:88px; }
  .shell{ max-width:var(--maxw); margin-left:var(--rail-w); margin-right:auto; }
  .tabbar{
    left:0; right:auto; top:0; bottom:0;
    width:var(--rail-w); max-width:var(--rail-w);
    flex-direction:column; justify-content:flex-start; align-items:center;
    gap:18px; padding:calc(env(safe-area-inset-top) + 22px) 0 22px;
    background:var(--chrome); border-right:1px solid var(--line-strong);
    border-radius:0; box-shadow:none;
    -webkit-backdrop-filter:none; backdrop-filter:none;
  }
  .tab-island{
    flex:1; max-width:none; flex-direction:column; gap:6px;
    background:none; box-shadow:none; border-radius:0; padding:0;
    -webkit-backdrop-filter:none; backdrop-filter:none;
  }
  .tab{ flex:none; width:64px; padding:9px 4px 8px }
  /* Bottom-anchored floats (toast, install nudge) lose the bar they were
     floating above — --nav-h:0 already clears the space under them, this
     clears the rail beside them. */
  .install-hint,.toast-wrap{ left:calc(var(--rail-w) + 16px) }
}


/* ---------- cards & lists ---------- */
/* Tonal, not drawn (2026-08-03): a card is a lighter surface on the ground,
   and the eye finds the boundary without a stroke — the screen stops being a
   grid of outlined boxes. Borders now mean something: an input you can type
   in, a button you can press, a tinted alert, a tone edge. */
.card{
  background:var(--surface);
  background-image:var(--card-grad);
  border-radius:var(--r);overflow:hidden;
  box-shadow:inset 0 0 0 1px var(--edge),inset 0 1px 0 var(--hi),var(--shadow);
}
.card + .card{margin-top:12px}
.card-pad{padding:14px 16px}
.section-title{
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint);margin:22px 2px 10px;
}
/* A heading that also carries a control — "N SNOOZED" opening the parked
   cards. Same face and size as the heading it sits in, so it reads as part of
   the label rather than a button dropped beside it; underlined because a word
   that does something has to look different from the words that do not. */
.section-title:has(.sec-link){display:flex;align-items:baseline;justify-content:space-between;gap:12px}
/* The note was a second bare span inside a block, so it ran straight into the
   count it qualifies: "MONEY · 1OLDEST AND LARGEST FIRST". A space alone was
   not the fix — it inherited the heading's mono face, uppercase and .16em
   tracking, so a sentence of ordinary English was set as though it were a
   label. Three of them at three different lengths broke across lines at three
   different points, which is the "jumbled" of the jumbled list.
   Always its own line, and set as prose: sans, sentence case, no tracking.
   Every section now has the same two-storey header — WHAT · HOW MANY, then one
   quiet line saying why they are in this order. */
.section-title:has(.sec-note){display:flex;flex-direction:column;gap:5px}
.sec-link{font:inherit;letter-spacing:inherit;text-transform:inherit;
  color:var(--muted);background:none;border:0;padding:0;
  text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--line-strong)}
.sec-link:active{color:var(--ink)}
/* ---------- programme blocks ----------
   A block is a phase of the same programme — Phase 1, then Phase 2 — so its
   heading has to outrank the day headings underneath it without becoming a
   second card. A rule across the top and the accent on the number does it:
   the eye finds the phase boundaries first and the days inside them second. */
.blk-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;
  border-top:1px solid var(--line-strong);margin:22px 0 0;padding:10px 2px 0}
.blk-head:first-child{margin-top:0}
.blk-name{flex:1;min-width:0;text-align:left;border:0;background:none;padding:0;
  display:flex;flex-direction:column;gap:3px;color:var(--ink)}
.blk-t{font:600 var(--ds-strong)/1.2 var(--sans);letter-spacing:-.01em;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.blk-note{font:400 var(--ds-small)/1.4 var(--sans);color:var(--fainter);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.blk-count{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--fainter);flex:none}
/* The days inside a block. Same mono label as .section-title, one notch quieter
   and much closer in, so it reads as belonging to the phase above it. */
.day-title{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--fainter);margin:14px 2px 8px}
.blk-add{margin-top:10px}
/* The programme's phases, on the library row. Between the name and the tally
   because it is part of what the programme is, not a count of it. */
.row-blocks{font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint);margin-top:2px}

/* ---------- collapsible groups ----------
   Nineteen rows of settings on a 390px phone is four screens of scrolling to
   reach the one you want. Closed, each group is a single line that already
   states its answer — "6 days · 146 starts / week" — so most visits end at the
   header without opening anything. The row list underneath is unchanged; it is
   only whether it is on screen that moves. */
.grp{
  width:100%;display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  background:none;text-align:left;margin-top:18px;margin-bottom:4px;
  padding:0 0 9px;border-bottom:1px solid var(--line);
}
.grp-t{font:600 var(--ds-label)/1 var(--mono);letter-spacing:.13em;text-transform:uppercase;
  color:var(--faint);flex:none}
/* THE SUMMARY IS A VALUE, SO IT IS ARCHIVO — and until the duplicate below was
   removed it was mono uppercase, identical to the label beside it. Ten rows of
   two competing small-caps strings with nothing to tell them apart is what made
   this page read as busy: not its length (it is under one screen) but the
   absence of any hierarchy inside each row. "Grace Lu" and "Never backed up"
   are things the app is telling the coach; the word to their left is a label
   about them. Two voices, one row. */
.grp-s{font:400 var(--ds-small)/1.3 var(--sans);letter-spacing:0;text-transform:none;
  color:var(--ink-2);flex:1;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
.grp-c{flex:none;color:var(--fainter);display:flex;align-self:center;
  transition:transform .14s ease}
/* Open, the summary is redundant with the rows now showing it, and the rule
   under the header would draw a second line right above the card's own top
   border. Both step out of the way. */
.grp.on{border-bottom-color:transparent}
.grp.on .grp-t{color:var(--ink)}
.grp.on .grp-s{opacity:0}
.grp.on .grp-c{transform:rotate(180deg);color:var(--muted)}
.grp-body{padding-bottom:8px}
/* Same line, no control: used for the library's pattern headings while a search
   is running, where everything matching is already showing and there is nothing
   left to open. The count stays visible because it is the one thing a search
   result wants to tell you. */
.grp-lbl{border-bottom-color:transparent}
.grp-lbl .grp-t{color:var(--ink)}

/* The .grp-go line that used to send you to the library lived here. The library
   has its own tab now, so a second door on the settings screen would only be a
   way of arriving somewhere you were already one tap from. */

/* ---------- booking, on a phone ---------- */
/* The week as seven touch targets. Each carries its own booked count, so the
   coach picks the quiet day from the strip rather than by opening three of
   them. */
/* The Book screen uses the Home calendar's own strip classes (.wk-strip /
   .wk-d) — there is one week-strip in this app, not two that drift. Its only
   local need is the gap under it before the slot list. */
.wk-strip{margin-bottom:2px}
/* Band headings are rules across the list, not cards — they say where you are
   in the day and then get out of the way. */
/* SECTIONS YOU CAN SEE (owner, 2026-08-10: "Scheduling can use some lines to
   separate the sections... can look like just a jumbled list"). The band label
   was a small line of text floating in the same gap as everything else, so a
   morning, a midday and an evening ran together into one column of thirty
   near-identical rows and there was no telling, mid-scroll, which one you were
   in.

   A rule above it draws the seam. Sticky keeps the answer on screen: the whole
   problem is that the heading scrolls away and the rows below it look like the
   rows above. It sits on the page ground rather than a translucent one — rows
   passing UNDER a see-through header is exactly the mush being fixed. */
.bk-band{display:flex;align-items:baseline;justify-content:space-between;
  /* Under BOTH pinned layers — the bar and the frozen week strip — not behind
     them. Both heights move with the safe-area inset and the user's text size,
     so neither can be named here; app.js measures the pair on every render and
     on resize as --top-h and --frozen-h. z-index below .freeze (15) so the strip always wins the overlap
     as a band passes under it. */
  position:sticky;top:calc(var(--top-h, 56px) + var(--frozen-h, 0px) - 1px);
  z-index:2;background:var(--bg);padding:9px 2px 8px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--ink);}
/* One section per band, so each heading is pinned inside its OWN block and is
   carried off the top by its own rows rather than stacking with the next. */
.bk-sect{display:flex;flex-direction:column;gap:10px}
/* No rule and no gap on the first band — a line under the day strip is a seam
   between two things that are already separate. */
.bk-sect:first-child > .bk-band{margin-top:4px;border-top:0}
.bk-band .dim{letter-spacing:.06em;color:var(--fainter)}
/* Rounded rows on the calendar's own 10px rhythm, not a fused grid of squares.
   This board was the last squared surface left in the app (an earlier owner
   call, since overtaken by the no-squares rule) and beside the rounded cards
   on every other screen it read as a different product. */
.bk-list{display:flex;flex-direction:column;gap:10px}

/* THE BOOK BOARD, using the width instead of wasting it (owner, 2026-08-16:
   desktop pass). A day's open slots are a chronological list on a phone
   because there is nowhere else to put them; on a desktop column they were
   the exact same single file of pill buttons with a whole extra column of
   empty page beside them. Booked sessions and buffer rows stay full-width
   cards — they carry a name and a reason — and only the open half-hour
   buttons, which carry a time and nothing else, fall into a grid. CSS
   grid's own auto-flow does the sorting: a full-span item ends its row and
   the next slot starts a fresh one, so a booked session or a buffer still
   breaks the grid exactly where it broke the list. AFTER .bk-sect's base
   rule on purpose — same specificity, later wins, and this has to win when
   the media query is active. */
@media (min-width:900px){
  .bk-sect{ display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px }
  .bk-sect > .bk-band,
  .bk-sect > .bk-sess,
  .bk-sect > .bk-r.buffer,
  .bk-sect > .bk-r.gbusy{ grid-column:1/-1 }
}
/* A booked session sits in the board as the full Home card, unmodified — same
   radius, gradient, tone bar and shadow as the same session on the calendar. */
.bk-sess{padding:0}
.bk-r{display:flex;align-items:center;justify-content:space-between;gap:12px;
  width:100%;text-align:left;padding:13px 14px;
  background:var(--surface);background-image:var(--card-grad);
  border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow)}
/* DEPTH, NOT COLOUR, separates a booking from an empty hour. Once the accent
   edges came off, every row on the board sat on the same surface at the same
   height and the eye had nothing to catch. A booked session is CONTENT: it
   keeps the raised surface, the gradient and the shadow every card in this app
   has. An open slot is the ABSENCE of content, so it drops to the page ground
   with a hairline round it — present, tappable, and visibly a hole in the day
   rather than a thing in it. No new colour, no new token: this is the same
   surface/ground relationship the rest of the sheet already runs on. */
.bk-r.open{background:var(--bg);background-image:none;box-shadow:none;padding:11px 14px}
.bk-r.open:active{}
.bk-t{font:500 var(--ds-small)/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--muted)}
.bk-s{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
.bk-r.open:active{background:var(--surface-2)}
/* OPEN is the default state of a board that is mostly open — colouring it made
   the whole list green and left nothing for the accent to mean. Green is for
   what you have selected; an untouched free slot is just a row. */
.bk-r.open .bk-s{color:var(--faint)}
/* Booked is the only row that names a person, so it is the only one that gets
   the accent mark and the sans face. */
/* Inset, not a left border: on a rounded box a border traces the corner arc as
   a tapering wedge, and it shunts the row's content 2px out of column. */
.bk-r.booked{box-shadow:inset 3px 0 0 var(--accent),var(--shadow)}
.bk-r.booked .bk-t{color:var(--ink)}
.bk-r.booked .bk-s{font:600 var(--ds-small)/1.2 var(--sans);letter-spacing:-.005em;
  text-transform:none;color:var(--ink)}
.bk-r.buffer{background:var(--bg)}
.bk-r.buffer .bk-t,.bk-r.buffer .bk-s{opacity:.45}
/* A Google event holding the row: quieter than a booking, firmer than buffer */
.bk-r.gbusy{background:var(--bg);box-shadow:inset 3px 0 0 var(--line-strong),var(--shadow)}
.bk-r.gbusy .bk-t,.bk-r.gbusy .bk-s{opacity:.6}
.bk-r.gbusy .bk-s{font-size:var(--ds-small)}
/* The tick only exists once a row is chosen; before that it would be a column
   of empty boxes asking to be ticked. */
.bk-tick{display:none;color:var(--accent);flex:none}
.row.on .bk-tick{display:block}
.row.on{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)}
.row.dimmed{opacity:.5}

/* ---------- working hours ---------- */
/* A rota, not a form. Seven rows of the same four things, so the eye runs down
   the "first" column and reads the week rather than reading seven paragraphs. */
.hrs{display:grid;grid-template-columns:1fr auto auto 46px;gap:1px;
  background:var(--line);border:1px solid var(--line-strong);
  border-radius:var(--r-s);overflow:hidden}
.hrs-h{background:var(--surface-2);padding:8px 12px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.hrs-h.r{text-align:right}
.hrs-d{background:var(--surface);display:flex;align-items:center;gap:9px;padding:9px 12px;
  text-align:left;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
/* The dot is the switch. A day is open or it is not, and that is the whole
   control — no toggle chrome for a binary that already has a row to live in. */
.hrs-d i{width:8px;height:8px;flex:none;border:1px solid var(--btn-bd)}
.hrs-d.on{color:var(--ink)}
.hrs-d.on i{background:var(--accent);border-color:var(--accent)}
.hrs-c{background:var(--surface);padding:5px 8px;display:flex;align-items:center}
.hrs-c.off,.hrs-n.off{opacity:.38}
.hrs-n{background:var(--surface);padding:9px 12px;text-align:right;
  font:500 var(--ds-label)/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--muted)}
.step{gap:2px;}
.step-b{width:22px;height:24px;flex:none;border:1px solid var(--line);border-radius:8px;color:var(--faint);
  font:400 var(--ds-small)/1 var(--mono)}
.step-b:hover:not(:disabled){color:var(--ink);border-color:var(--btn-bd)}
.step-b:disabled{opacity:.3}
.step-v{min-width:52px;text-align:center;font:500 var(--ds-label)/1 var(--mono);
  font-variant-numeric:tabular-nums;color:var(--ink)}
.hrs-r{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:5px 0}
.hrs-r + .hrs-r{border-top:1px solid var(--line)}
.hrs-r > span{font:400 var(--ds-small)/1.3 var(--sans);color:var(--muted)}
.hrs-cfg{padding:6px 14px}
.hrs-sum{margin-top:22px;background:var(--surface);border:1px solid var(--line-strong);padding:14px 16px}
.hrs-sum-v{font:700 var(--ds-title)/1.2 var(--sans);letter-spacing:-.015em;margin-top:9px}
.hrs-sum-s{font:400 var(--ds-label)/1.6 var(--mono);letter-spacing:.02em;color:var(--faint);margin-top:7px}

.list{display:flex;flex-direction:column}
.row{
  display:flex;align-items:center;gap:12px;
  padding:13px 15px;text-align:left;width:100%;
}
.row + .row{border-top:1px solid var(--line)}
.row:active{background:var(--surface-2)}
/* Generic, not row-scoped: session-item cards and tappable cards use the same
   middle-column-takes-the-space anatomy. flex:1 is inert outside a flex parent. */
.grow{flex:1;min-width:0}
/* Declared block because inside a button these have to be spans — a button
   takes phrasing content only, and half the rows in the app are buttons. */
.row-title,.row-sub{display:block}
.row-title{font:600 var(--tb-row)/1.3 var(--sans);letter-spacing:-.005em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* THE SUBTITLE IS THE SENTENCE, NOT THE LABEL (owner, 2026-08-27: "how can we
   make font sizes bigger, surface what is useful"). The note this replaces
   claimed row subtitles are "almost always meta — a prescription, a date, a
   count". Measured, they are not: "Put it in your bio — enquiries come here",
   "Adds only what is missing by name", "Replaces everything with demo". Those
   are sentences, and setting a sentence in tracked 11px uppercase mono is the
   hardest-to-read combination the app has — caps remove word shape, the size
   removes stroke detail, and the tracking pulls what is left apart. Both of
   the ones on Settings were also TRUNCATING at the row width, so the coach got
   two thirds of a sentence in the least legible face available.

   Archivo, sentence case, and the design system's own rule is restored:
   Archivo speaks, Mono labels and measures. The genuinely mono cases — a dose,
   a date, a count — override locally; .shelf-flat already does. */
.row-sub{font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint);
  margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chev{color:var(--fainter);flex-shrink:0}
.chev svg{width:18px;height:18px}

/* avatar — one of the two round things in the system */
.avatar{
  width:40px;height:40px;border-radius:50%;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font:600 var(--ds-body)/1 var(--sans);color:#fff;letter-spacing:-.02em;
}

/* ---------- pills / badges ----------
   Outlined, not filled. The handoff's only status chip is a 1px accent outline
   with matching text, and filling them would put five competing blocks of
   colour on a screen whose whole point is restraint. */
.pill{
  display:inline-flex;align-items:center;gap:5px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  padding:5px 8px;border:1px solid currentColor;border-radius:var(--r-pill);white-space:nowrap;flex:none;
}
.pill-ok{color:var(--accent)}
.pill-warn{color:var(--warn)}
.pill-bad{color:var(--bad)}
.pill-neutral{color:var(--faint)}
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}

/* ---------- attention feed ----------
   Borrows .session-item wholesale — see the note down there. Only the action
   button is new, and it is placed in the flag slot rather than on a row of its
   own, so an attention card is exactly as tall as a booked one.

   The padding moves off the card and onto the body button so the tap target
   reaches the card's own edges. Compound selector, not order, keeps it ahead of
   .session-item's own padding. */
/* Attention cards share the roster's compact two-line anatomy; the reason
   line wears the tone (not just the 3px edge) and stays a mono label —
   uppercase, one line, ellipsized. */
.attn-reason{text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  display:block;max-width:100%}
/* THE CLIENT-CARD COLOUR RULE (owner, 2026-08-05): on a client card the
   ONLY coloured text is an amount owed. Everything else — the reason line,
   the status word, the credit fraction — is neutral, and the card's left
   EDGE is what carries the accent (orange = needs attention, per the app's
   two-colour rule). Colouring a whole sentence spends the signal on words
   that never vary, and it was doing exactly that here. Session cards in the
   calendar are the one exception, where the status word carries colour
   because those cards have no edge at all. */

/* ---------- stats ----------
   One card, not two. Two bordered boxes side by side read as two separate
   things a coach has to consider; the day's numbers are one glance, so they
   share a card and are divided by a single hairline. Label above value —
   the label is the question, and you read it before the answer. */
.stat-grid{
  display:grid;grid-template-columns:1fr 1fr;
  background:var(--surface);
  background-image:var(--card-grad);
  border-radius:var(--r);overflow:hidden;
  box-shadow:inset 0 0 0 1px var(--edge),inset 0 1px 0 var(--hi),var(--shadow);
}
.stat{padding:12px 14px 14px}
.stat + .stat{border-left:1px solid var(--line)}
.stat .l{font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.stat .n{font-size:var(--ds-name);font-weight:700;line-height:1.1;letter-spacing:-.02em;margin-top:7px}
.stat .n-warn{color:var(--warn)}
.stat .n-bad{color:var(--bad)}

/* ---------- client header ----------
   The same two-figure strip as Today, sunk into a card that also holds the
   waiver line and the contact row. One card rather than a stat block stacked on
   a detail block, because they are one answer: this is where the person stands
   and this is how you reach them.

   The strip loses its own border and keeps only the hairline between the two
   figures — the card is already drawing the box, and a border inside a border
   at 1px each reads as a smudge rather than as two edges. */
.cli-head-foot{padding:12px 14px 14px;border-top:1px solid var(--line-strong)}
/* Mono, because it is a label about a state rather than a value in its own
   right, and small enough that the two buttons under it stay the loudest thing
   in the foot. */
.cli-waiver{font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.cli-waiver.miss{color:var(--warn)}
/* Roster rail: the card keeps two-thirds; the right third stacks two
   labelled pills that say the ask (Waitlist → wanted day/time, Request →
   the requested slot). Off is near-silent so quiet rows stay quiet. */
/* THE ROSTER CARD (owner, 2026-08-05): full width, with its two asks along the
   bottom INSIDE it. The rail version gave a third of every row to two pills
   and squeezed the name and the status into the remaining two-thirds — on a
   roster, the name is the thing being read. Home and Pay keep the rail: there
   the pill is an ACTION on the card (Nudge, Payment), which is a different
   relationship from a label that states the card's own standing. */
/* All three classes are named on purpose. .session-item.si-compact sets
   `align-items:center` at the same specificity AND later in the file, so it won
   every earlier attempt at this rule — the pill row kept collapsing to its text
   width and centring, which is what truncated WAITLIST to "WAITLI…". */
/* 56px — the same height as the client cards on Home and Pay. The budget is
   tight and deliberate: 5 + title 16 + gap 4 + pill 25 + 6. The pill keeps its
   --pill-h; the padding is what gives way, because a shared card height is
   worth more than four pixels of air. */
.session-item.si-compact.client-card{display:flex;flex-direction:column;align-items:stretch;
  justify-content:center;gap:4px;padding:5px 14px 6px 15px;min-height:var(--card-h)}
/* The navigating half: name on the left, where they stand on the right. Reset
   to a bare row because .session-item's own button styling now belongs to the
   card wrapping it, not to this. */
.cc-main{display:flex;align-items:baseline;justify-content:space-between;gap:12px;width:100%;
  text-align:left;background:none;border:0;padding:0;color:inherit}
.cc-main .row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The count and the status word share the first line rather than stacking —
   stacked they cost the card a whole row for two short strings. */
/* Name and count on one baseline. The name gives way first when the row is
   tight — the count is four characters and a word, and truncating THAT would
   leave a fraction with no unit. */
.cc-name{display:flex;align-items:baseline;gap:8px;min-width:0}
/* The count and the word that says which way it runs, set as one quiet
   string. Quieter than the status word opposite: it qualifies the person, it
   is not a state of its own. */
.cc-rem{font:400 var(--ds-label)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  color:var(--faint);flex:none;white-space:nowrap}
/* First line of an attention card: who, then what the card wants from you —
   the same top-right corner the calendar cards put UP NEXT and CHECK-IN in. */
.cc-top{display:flex;align-items:center;justify-content:space-between;gap:10px}
.cc-top .cc-main{flex:1;min-width:0}
/* Second line: when they are next in, then the two asks. */
.cc-foot{display:flex;align-items:center;justify-content:space-between;gap:10px}
.cc-foot .row-sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The pills size to their words here, not to the card: they share this line
   with the next-session label, and stretching them would push it out. */
.cc-pills{display:flex;gap:8px;flex:none}
.cc-pills .cr-pill{flex:none;height:var(--pill-h);padding:0 10px}

/* A row with no rail beside it takes the whole width — Pay's unpaid list,
   where the card IS the action and there is nothing to sit next to. The height
   is stated rather than left to the content: without a pill row its second
   line is plain text, which came out 3px shorter than the same card on Home
   and Clients. Three pixels is invisible alone and obvious in a column. */
/* Every pill centres its contents — waitlist and request included. */
.cr-pill{flex:1;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:6px;
  padding:0 10px;min-width:0;border:1px solid var(--line-strong);border-radius:var(--r-pill);
  background:none;color:var(--faint);opacity:.4;text-align:center}
.cr-pill svg{flex:none}
.cr-pill-w{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.cr-pill.on{opacity:1;color:var(--accent);border-color:var(--accent)}
.cr-pill.req.on{opacity:1;color:var(--warn);border-color:var(--warn)}
.cr-pill:disabled{opacity:.2}
/* Action pills (Home attention, Pay): the EXACT roster pill geometry, never
   resized. They take no tone — the card's edge already says "attention", and
   an orange label beside an orange edge is the same word twice.
   A lone pill sits centred against its card; the moment a second pill joins
   it — a waitlist ask, a booking request — the pair stacks from the top. */
/* Fixed to the shared pill height, whether it stands alone or has a sibling.
   Never stretches to the card. Text centred — a lone pill beside a card
   reads as a button, and a button's label sits in the middle of it. */
/* Same 86 x --pill-h box as the UP NEXT / CHECK-IN tags on the calendar
   cards above — see .up-next-tag. */
.cr-pill.act{opacity:1;color:var(--muted);flex:0 0 auto;
  min-width:86px;height:var(--pill-h);
  justify-content:center;text-align:center}
.cr-pill.act .cr-pill-w{white-space:normal;line-height:1.3;text-align:center}

/* An empty day: a dashed card the size of the session card that would have
   been there, so the day holds its shape either way. Mono uppercase, like
   every other label in this app that states a condition rather than a name. */
/* THE EMPTY-STATE RULE (owner, 2026-08-05): anywhere a list, a day or a
   filter has nothing in it, say so in a DASHED BAR — never a filled card. A
   filled card is a thing; an absence must not look like a thing. Mono
   uppercase in --faint, like every other label that states a condition
   rather than a name. `.between` when the bar also carries an action pill,
   `.col` when it carries a sentence and a button beneath it. */
/* The same height as every client card (--card-h) — an absence keeps the
   row's height and changes only its treatment: dashed instead of filled, no
   accent edge. */
.empty-bar,.day-empty{display:flex;align-items:center;justify-content:center;gap:0;
  min-height:var(--card-h);padding:9px 12px;background:none;
  border:1px dashed var(--line-strong);border-radius:var(--r);
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--faint);text-align:center}
.empty-bar.col{flex-direction:column;padding:16px 14px;line-height:1.5;min-height:0;}

/* Two figures on one quiet line, replacing the display boxes Today and Pay
   used to open with — a glance-line, not a fifth of the screen. */

/* the contact card's QR sits on white by necessity, not by style */
.bcard-qr{display:flex;justify-content:center;margin-top:12px;padding:10px;
  background:#fff;border-radius:var(--r-s)}
.bcard-qr canvas{display:block;max-width:200px;width:100%;height:auto;image-rendering:pixelated}

/* Waitlist sheet: seven weekday chips, up to two lit */

/* Package card: facts in a right-hand pill column, pencil on the name line */
/* One width for the whole column, whatever the words are. PAID is four letters
   and "10 REMAINING" is twelve, so two pills stacked on the same right edge
   used to step in and out like a ragged margin. `stretch` makes every pill in
   the column as wide as the widest, and the text centres inside it — the same
   rule the action pills on the roster follow. The min-width holds the shape
   when the only pill present is a short one. */
.pill-col{display:flex;flex-direction:column;gap:4px;align-items:stretch;flex:none}
.pill-col .pill{justify-content:center;min-width:104px}
.pkg-edit{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;
  margin-left:6px;vertical-align:middle;border:1px solid var(--btn-bd);border-radius:8px;
  background:none;color:var(--muted)}

/* Status chips beside the client name: read-only facts, right-aligned,
   wrapping when a client has several things true at once. */
.tb-chips{margin-left:auto;display:flex;flex-wrap:wrap;gap:4px;justify-content:flex-end;
  max-width:46%;align-self:center}

/* ---------- buttons ----------
   Every button label is a mono CTA in the handoff, uppercase in both themes. */
/* A BUTTON SPEAKS (owner, 2026-08-27, reading the measured review back:
   "caps are load-bearing on eight different things"). Nav labels, section
   headers, row meta, chips, stat captions, buttons, the day bar and the tab row
   were all uppercase mono — and when everything is emphasised nothing is, so
   the eye has no rung to grab.
   Caps keep the two places they are genuinely labels: a section header names a
   group, a status chip names a state. A button names an ACTION, which is speech
   — "Start session", not START SESSION — and Archivo says it at a size that
   reads one-handed. This is also what makes a primary read as an offer rather
   than signage, which is the difference the owner was naming between a tool and
   a chore. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  padding:13px 16px;
  font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.006em;
  border:1px solid transparent;border-radius:var(--r-s);transition:opacity .12s ease;
}
.btn:active{opacity:.75}
.btn svg{width:16px;height:16px;stroke-width:2}
/* One lime per view. btn-primary is the single accent action a screen or
   sheet is for; everything else is soft (bordered, ink) or ghost (bordered,
   muted). A former .btn-accent twin of this rule let two limes share a sheet
   unnoticed — it is gone, and the discipline is the class system itself. */
.btn-primary{background:var(--accent);color:var(--accent-ink)}
.btn-ghost{background:transparent;border-color:var(--btn-bd);color:var(--muted)}
/* --btn-bd, not --line-strong: this button has no fill, so the border is the
   whole control and has to clear 3:1 on its own. A card border does not — the
   surface change either side of it already says where the card stops. */
.btn-soft{background:transparent;border-color:var(--btn-bd);color:var(--ink)}
.btn-danger{background:transparent;border-color:var(--bad);color:var(--bad)}
/* Outlined accent: an action the card is recommending, not one it is insisting
   on. Filled is reserved for the single primary action of a whole screen. */
.btn-block{width:100%}
/* The add bar, wherever a list can be added to. One rule so Booking, Clients
   and the Library cannot drift apart, and one margin so the first card of the
   list always sits the same distance under it. */
.btn-sm{padding:9px 12px;font-size:var(--ds-label)}
.btn-lg{padding:15px 18px;font-size:var(--ds-small)}
.btn:disabled{opacity:.4;pointer-events:none}
.btn-row{display:flex;gap:9px}
/* min-width:0 so five icon actions share a phone's width without the row
   overflowing; the side padding drops because these are square-ish targets,
   not word-bearing buttons. */
.btn-row .btn{flex:1;min-width:0;padding-left:10px;padding-right:10px}

/* ---------- hit areas ----------
   The density here is deliberate: a 32px icon button and a 32px segment are the
   right size on the page and the wrong size under a thumb. Inflating them to
   44px would buy the target and spend the layout — five filter chips at 44px
   are a toolbar, not a filter.
   So the visible box stays where it was drawn and the target grows past it,
   with an absolutely positioned pseudo-element that catches the taps the chip
   is too small to catch itself. Nothing moves; the 44px is simply there.
   The rule is centred on the control, so the overhang is split evenly above
   and below rather than stealing from whatever sits underneath. */
.me,.icon-btn,.seg button,.btn-sm,.pkg-history-toggle,
.cr-pill,.step-b,.pkg-edit,.wl-dow,.sec-link,.chip-tog,.pkg-owe-btn{position:relative}
.me::after,.icon-btn::after,.seg button::after,.btn-sm::after,
.pkg-history-toggle::after,
/* The most-tapped controls in the app were the ones missing from this list:
   Nudge/Payment/Wake are 25px tall, and the waitlist and request toggles,
   the hours steppers and the package edit pencil are all under 30px. The
   visible box stays where it was drawn; only the target grows. */
.cr-pill::after,.step-b::after,.pkg-edit::after,.wl-dow::after,.pkg-owe-btn::after,
.sec-link::after,.chip-tog::after{
  content:'';position:absolute;left:0;right:0;
  top:50%;height:44px;transform:translateY(-50%);
}
/* Square controls are short in both directions, so they take the width too. */
.me::after,.icon-btn::after{left:50%;right:auto;width:44px;transform:translate(-50%,-50%)}

/* ---------- forms ---------- */
.field{margin-bottom:14px}
.field > label{display:block;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint);margin:0 0 7px}
/* A FIELD IS A WELL, NOT A CARD. These were painted with --surface — the same
   white as the sheet they sit on — so a form was a set of outlines on a flat
   plane and the only thing saying "you write here" was a border. Sunk one step
   with a 1px inner shadow, they read as somewhere to put something before a
   word of the label is read. This is the boxed form style only: the inline
   rows inside a sheet override the background further down and STAY
   borderless, because a bordered input inside a ruled row is two containers
   saying the same thing — see .sh-row-v. */
.input,.textarea,.select{
  width:100%;padding:12px 13px;background:var(--sunken);box-shadow:var(--inset);
  border:1px solid var(--line-strong);border-radius:var(--r-s);
  /* 14px, not 16 (owner, 2026-08-16: "the texts filling in the boxes are
     TOO big"). 16px was only ever the iOS anti-zoom floor; the viewport
     meta now carries maximum-scale=1, which stops the focus zoom without
     touching pinch (iOS has ignored the cap for pinch since iOS 10). */
  font-size:var(--ds-body);font-family:var(--sans);color:var(--ink);appearance:none;
}
/* Placeholders were never styled, so they were the browser's #757575 on
   whatever surface the field happened to sit on — 1.75:1 in dark, which is a
   grey on a grey. That would matter less if they were decoration, but half of
   them carry the format the field wants: "+852 …", "12345678-000", "at least 6
   characters". --fainter is the quietest grey in the system that is still
   checked against both grounds. */
.input::placeholder,.textarea::placeholder{color:var(--fainter);opacity:1}
/* The border alone carries the focus state now that there are no shadows
   to glow with. */
/* Focus fills the well white and keeps the accent edge: the field you are in is
   the one that is no longer recessed. */
.input:focus,.textarea:focus,.select:focus{outline:none;border-color:var(--accent);
  background:var(--surface);box-shadow:none}
.textarea{min-height:80px;resize:vertical;line-height:1.45}
.select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%235c646c' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:38px}
/* the chevron colour is baked into the data URI, so dark needs its own copy */
:root[data-theme=dark] .select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23d5dade' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")}
.field-row{display:flex;gap:10px}
.field-row > .field{flex:1}
/* A price-list row: the name across the top with its bin, and the numbers as
   sheet rows under it. The three-abreast number layout below this went with
   them (2026-08-26) — squeezing "Price (HKD)" and "Valid days" into 90px
   columns was the old shape solving a problem the row shape does not have. */
.tpl-row{margin-bottom:10px}
.tpl-head{display:flex;gap:8px;align-items:center;margin-bottom:12px}
.tpl-head .input{flex:1;min-width:0}
.tpl-head .icon-btn{flex:none}
/* ARCHIVO SPEAKS, MONO LABELS AND MEASURES — the design system's own rule, and
   this broke it on every form in the app. A hint is a SENTENCE ("How every
   message and link reaches them.", "Leave blank if not yet signed."), and a
   full line of mono reads as terminal output. It also mattered more in light
   than in dark: grey mono prose next to grey mono prose on a light ground is
   where the eye stops being able to tell a note from a field.
   --ds-small rather than --ds-label, because Archivo at a label size is smaller
   on the page than mono at the same token — the mono face runs wide. */
.hint{font:400 var(--ds-small)/1.5 var(--sans);letter-spacing:0;color:var(--faint);margin:7px 0 0}

/* ---------- lock screen ----------
   The whole page, not a sheet over it, because there is nothing underneath: the
   store has not been decrypted, so anything behind this would be an app with no
   clients in it. No brand bar and no tab bar either — every destination they
   offer is unreachable until the passcode is in.

   Sat a little above centre. Optically centred content wants to be high of the
   true middle, and here it also keeps the field clear of a phone keyboard. */
.lock{min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:24px 20px 15vh}
.lock-box{width:100%;max-width:340px}
.lock-brand{justify-content:center;margin-bottom:18px}
.lock-note{font:400 var(--ds-small)/1.55 var(--sans);color:var(--faint);text-align:center;margin:0 0 18px}
/* Reserved height, so a wrong passcode does not shove the button down the page
   under the thumb that is already moving towards it. */
.lock-err{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.06em;text-transform:uppercase;
  color:var(--bad);min-height:16px;margin-top:9px;text-align:center}
/* 17px of padding on 10.5px of type, which is the only way this control reaches
   44px without getting louder — and it is the one control on the screen that
   must not be hit by accident, so it stays the smallest text and the quietest
   grey while still being a full-sized target. */
.lock-wipe{display:block;width:100%;margin-top:20px;padding:17px 12px;background:none;border:0;
  color:var(--faint);font:400 var(--ds-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase}
/* Segmented controls are outlined cells, and the chosen one inverts to --ink.
   No track behind them — the track was doing a job that a border does better. */
.seg{display:flex;gap:5px}
.seg button{flex:1;padding:10px 8px;border:1px solid var(--line);border-radius:var(--r-s);background:var(--surface);
  color:var(--muted);font:500 var(--ds-small)/1 var(--sans);letter-spacing:0}
/* Selected = a green ring, exactly as a selected day on the calendar. It used
   to be an inverted ink slab, which was a THIRD way of saying "this one" in an
   app whose rule is green for selected/in-view. Same class serves the Clients
   filters, so both tab rows now say it the one way. */
.seg button.on{background:var(--surface-2);border-color:var(--accent);color:var(--ink)}
.check-row{display:flex;align-items:center;gap:10px;padding:10px 0}
/* checkboxes reach here carrying .input, whose appearance:none hides the tick */
.check-row input[type=checkbox]{
  width:19px;height:19px;flex:none;padding:0;border:0;
  background:none;appearance:auto;-webkit-appearance:checkbox;accent-color:var(--accent);
}
.check-row span{font-size:var(--ds-small)}

/* ---------- modal / sheet ---------- */
.scrim{
  position:fixed;inset:0;z-index:50;background:var(--scrim);
  display:flex;align-items:flex-end;justify-content:center;
  animation:fade .18s ease;
}
/* a stacked sheet keeps its parent visible behind it, but only one backdrop dims */
.scrim-under{background:transparent}
/* The exit is the entrance run backwards: scrim fades while the sheet sinks.
   ui.js adds .closing and removes the node 200ms later; pointer-events off so
   a departing scrim can never swallow the tap that dismissed it. */
.scrim.closing{animation:fade-out .16s ease forwards;pointer-events:none}
.scrim.closing .sheet{animation:sink var(--dur-2) ease forwards}
@keyframes fade{from{opacity:0}to{opacity:1}}
@keyframes fade-out{to{opacity:0}}
@keyframes rise{from{transform:translateY(24px)}to{transform:translateY(0)}}
@keyframes sink{to{transform:translateY(28px);opacity:0}}
.sheet{
  width:100%;max-width:var(--maxw);
  background:var(--bg);
  border-radius:20px 20px 0 0;box-shadow:var(--shadow-up);
  max-height:92vh;display:flex;flex-direction:column;
  animation:rise .22s cubic-bezier(.2,.8,.2,1);
  padding-bottom:env(safe-area-inset-bottom);
}
.sheet-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:16px 18px 12px;position:sticky;top:0;
}
.sheet-head h2{font-size:var(--ds-figure);font-weight:700;margin:0;letter-spacing:-.02em}
/* The title as a door to the person — see openSheet(). It has to look like the
   heading it replaces, so it inherits everything and adds only the chevron that
   says it goes somewhere. */
.sheet-title-go{display:inline-flex;align-items:center;gap:6px;padding:0;border:0;
  background:none;color:inherit;font:inherit;letter-spacing:inherit;text-align:left}
.sheet-title-go svg{color:var(--faint);flex:none}
.sheet-sub{padding:0 18px 2px;font:var(--ds-w-medium) var(--ds-micro)/1.5 var(--mono);
  letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
/* The instruction that turns a list of names into a set of controls. Quieter
   than the heading it sits beside — it is read once, on the first class a coach
   ever opens, and never needed again. */
/* Also a sentence — "Tap a name to mark one person" is an instruction, not a
   label. Same rule as .sh-act-s above. */
.sect-hint{font:var(--ds-w-regular) var(--ds-small)/1.35 var(--sans);letter-spacing:0;
  text-transform:none;color:var(--fainter)}
.sheet-grip{width:34px;height:3px;background:var(--line-strong);margin:9px auto 0}
.sheet-body{padding:4px 18px 18px;overflow-y:auto;-webkit-overflow-scrolling:touch}
.sheet-foot{padding:12px 18px calc(16px + env(safe-area-inset-bottom));border-top:1px solid var(--line-strong);background:var(--surface)}

/* ---- THE ACTION-SHEET ROW (owner mocks 29–31, 2026-08-19) ----------------
   Every sheet used field(): a label stacked above its control, which is a FORM.
   The mocks are statements you can change — mono key in a fixed left column,
   value beside it, and what the app already knows on the right.

   A grid, not flex, and for the same reason the runner's set table is one: the
   value column holds an <input> on one row and a plain span on the next, and
   under flex those two land in different places. The key column is the widest
   label in the sheet (WHATSAPP), so every sheet in the app aligns the same. */
.sh-rows{border-left:1px solid var(--line);padding-left:14px;margin-top:6px}
.sh-row{display:grid;grid-template-columns:88px minmax(0,1fr) auto;align-items:center;
  gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
.sh-row:last-child{border-bottom:0}
.sh-row-k{font:var(--ds-w-medium) var(--ds-micro)/1.3 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--fainter)}
/* The control CARRIES this class rather than sitting inside a span with it —
   see sheetRow(). It inherits the row rather than drawing a box inside it: a
   bordered input in a ruled row is two containers saying the same thing, and it
   was the single biggest reason these sheets read as forms. */
.sh-row-v{min-width:0;border:0;background:none;padding:0;width:100%;
  font:var(--ds-w-regular) var(--ds-strong)/1.3 var(--sans);color:var(--ink)}
.sh-row-v::placeholder{color:var(--fainter)}
/* A SELECT NEEDS A HEIGHT. Stripping the box left height:auto on it, which is
   how a <select> is told to render as a LIST — every option stacked on top of
   the row, drawn over the one underneath. It has to be one line with a caret
   the coach can see, and the caret has to be drawn here because appearance:none
   takes the native one away. */
select.sh-row-v{appearance:none;height:26px;padding-right:18px;
  background-image:linear-gradient(45deg,transparent 50%,var(--faint) 50%),
    linear-gradient(135deg,var(--faint) 50%,transparent 50%);
  background-position:calc(100% - 9px) 11px,calc(100% - 4px) 11px;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
/* A textarea in a ruled row is one line until it is written in — the notes
   field was claiming five lines of a sheet nobody had typed into yet. */
textarea.sh-row-v{height:26px;min-height:26px;resize:vertical;overflow:hidden}
textarea.sh-row-v:focus{height:auto;min-height:66px;overflow:auto}
/* The repeat row's own controls, laid out inside the value column so the row
   keeps the same three tracks as every other one. */
.sh-repeat{display:flex;align-items:center;gap:9px}
.sh-repeat input[type=checkbox]{width:19px;height:19px;flex:none;accent-color:var(--accent)}
.sh-repeat .input{width:46px;flex:none;text-align:center;border:1px solid var(--line);
  border-radius:8px;padding:3px 0;height:auto;
  font:var(--ds-w-semi) var(--ds-small)/1.4 var(--sans);color:var(--ink)}
.sh-rep-u{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--faint)}
/* WHAT THE APP ALREADY KNOWS. Never a control — "3 of 5 left", "her usual",
   "1 left now". It answers alongside the question so the coach is confirming
   rather than filling in. Amber when the answer is a shortfall. */
.sh-row-h{justify-self:end;white-space:nowrap;
  font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--faint)}
.sh-row-h.acc{color:var(--accent)}
.sh-row-h.warn{color:var(--warn)}

/* WHAT HAPPENS WHEN YOU PRESS THE BUTTON, said before it is pressed — the one
   thing a confirm button cannot do on its own. */
.sh-note{display:grid;grid-template-columns:88px minmax(0,1fr);gap:12px;
  margin-top:14px;padding:12px 0 12px 14px;border-left:2px solid var(--line-strong)}
.sh-note.warn{border-left-color:var(--warn)}
.sh-note-k{font:var(--ds-w-medium) var(--ds-micro)/1.4 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--fainter)}
.sh-note.warn .sh-note-k{color:var(--warn)}
.sh-note-t{font:var(--ds-w-regular) var(--ds-small)/1.45 var(--sans);color:var(--muted)}

/* Cancel keeps only the width it needs; the one that DOES the thing takes the
   rest, because on a sheet with two buttons only one of them is the reason the
   sheet is open. */
/* ---- the tappable statement (owner session mocks) --------------------- */
.sh-grp{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin:22px 0 8px;font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);
  letter-spacing:.12em;text-transform:uppercase;color:var(--fainter)}
/* The optional right half of a group heading — a count, a state. Quieter than
   the label it sits opposite, because the label is the subject. */
.sh-grp-r{flex:none;color:var(--faint);letter-spacing:.06em}
.sh-acts-list{border-left:1px solid var(--line);padding-left:14px}
.sh-act{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  background:none;border:0;padding:12px 0;border-bottom:1px solid var(--line)}
.sh-act:last-child{border-bottom:0}
.sh-act:disabled{opacity:.45}
/* A STATEMENT, NOT A CONTROL. Same row, no press affordance and no chevron:
   the title steps back to normal weight so the eye stops offering it as a
   choice, and the row is a div so a tap lands nowhere at all. */
.sh-act-fact .sh-act-t{font-weight:var(--ds-w-regular);color:var(--ink-2)}

/* ── the session sheet, read one-handed between clients ───────────────────
   Owner 2026-08-25: "too complicated. 3 sections, 2-3 each" · "a clear view and
   simple buttons so they can do things quickly on the go". Eleven same-sized
   grey rows became one line of facts, three thumb-sized buttons and a door. */

/* The facts. Mono because it is labels and measures — BASE BUILD · WK 3 · 9
   LEFT — never a sentence, so it never reads as terminal output. Not tappable:
   the sheet's own title is already the way to the record. */
.sh-facts{font:var(--ds-w-regular) var(--ds-label)/1.4 var(--mono);
  letter-spacing:.06em;text-transform:uppercase;color:var(--muted);
  padding:2px 0 14px;border-bottom:1px solid var(--line);margin-bottom:14px}

/* Three across at every width. A coach is holding a phone in one hand and a
   client is waiting, so these are targets first and decoration never: 64px of
   height clears the 44px minimum with room for the label under the mark.
   Shared by the session sheet and the Profile tab: one shape learned once. */
.tile-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.cl-body .tile-row,.main .tile-row{margin:16px 0 4px}
.tile-row:has(> :only-child){grid-template-columns:1fr}
.tile-row:has(> :nth-child(2):last-child){grid-template-columns:repeat(2,1fr)}
.tile{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:6px;min-height:64px;padding:10px 6px;
  background:var(--raised);border:1px solid var(--line);border-radius:14px;
  color:var(--ink);cursor:pointer}
.tile span{font:var(--ds-w-semi) var(--ds-small)/1 var(--sans);letter-spacing:.02em}
.tile svg{color:var(--muted)}
.tile:active{background:var(--surface-2);filter:brightness(1.12)}
/* The one that cannot be undone wears the warning, and only when it will
   actually cost the client a credit. */
.tile.bad svg,.tile.bad span{color:var(--bad)}

/* ── the Profile tab ──────────────────────────────────────────────────────
   Owner 2026-08-25: "the view of the page is horrible.. bigger and cleaner,
   with more spacing". The old shape was a two-column grid — a mono key holding
   a third of the width whatever it said — so a three-word value wrapped onto
   three lines beside a half-empty label. The value leads now and its label sits
   under it, which is what lets the value have the whole phone. */

.pf-head{font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--muted);margin:26px 0 10px}
.pf-head:first-child{margin-top:18px}
/* The only heading that is ever coloured, and only when it has something in
   it — the count is in the title, so an empty section cannot shout. */
.pf-head.warn{color:var(--bad)}

.pf-list{display:flex;flex-direction:column;gap:2px}

/* SPACING IS THE WHOLE FIX. 16px of vertical padding per row against the old
   grid's cramped 12, and rows separated by a 2px gap and a surface rather than
   hairlines — a list of six hairline rules reads as a table, and a table is
   what made this page feel like a form to fill in. */
.pf-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  padding:16px 15px;background:var(--surface);border:0;border-radius:12px;
  color:var(--ink)}
button.pf-row{cursor:pointer}
button.pf-row:active{background:var(--surface-2)}
.pf-row-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}
/* The value, at the size the rest of the app reads a row title at. */
.pf-row-t{font:var(--ds-w-semi) var(--ds-strong)/1.35 var(--sans);color:var(--ink);
  overflow-wrap:anywhere}
/* Its label, underneath and quiet — mono, because it is a label about a value.
   Sentence case, not caps: some of these are short sentences. */
.pf-row-k{font:var(--ds-w-regular) var(--ds-label)/1.35 var(--mono);
  letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
/* The sentence under a row that needs doing. Archivo, because it SPEAKS —
   mono set a full line of English and the amber rows read like a console. */
.pf-row-h{font:var(--ds-w-regular) var(--ds-small)/1.4 var(--sans);color:var(--muted)}
.pf-row.warn .pf-row-t{color:var(--bad)}
.pf-row-a{flex:none;display:flex;align-items:center;gap:2px;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--accent)}
.pf-row.warn .pf-row-a{color:var(--bad)}
.pf-row-a svg{color:var(--muted)}

/* The WhatsApp mark used as a field's own label, and the pair of marks on the
   Book button that say what leaves the phone. Both replace wording that was
   longer than the thing it named. */
.wa-label{display:inline-flex;align-items:center;color:var(--muted)}
.go-marks{display:inline-flex;align-items:center;gap:5px;margin-left:8px;opacity:.75}

/* ── ready for more ───────────────────────────────────────────────────────
   The suggestion the coach can take or ignore. Accent-edged rather than
   accent-filled: the loud thing on this screen is still the set in front of
   them, and a suggestion that shouted louder than the work would be wrong. */
.rn-up{display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  margin-top:10px;padding:11px 13px;background:none;border-radius:11px;
  border:1px solid color-mix(in oklab,var(--accent) 42%,transparent);
  color:var(--ink);cursor:pointer}
.rn-up:active{background:color-mix(in oklab,var(--accent) 10%,transparent)}
.rn-up-k{flex:none;font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.12em;
  text-transform:uppercase;color:var(--accent)}
.rn-up-t{flex:1;min-width:0;font:var(--ds-w-semi) var(--ds-small)/1.35 var(--sans);color:var(--ink)}
/* Why the app thinks so, in the quietest voice on the row — a coach who
   disagrees needs to see the evidence, and one who agrees never reads it. */
.rn-up-t i{font:400 var(--ds-label)/1.35 var(--sans);font-style:normal;color:var(--muted)}
.rn-up-a{flex:none;font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--accent)}

/* ── the rest bar in the strength runner ──────────────────────────────────
   Owner 2026-08-26: log sets, reps and rest "with minimal screen taps
   mid-workout". A slim bar under the sets rather than the timed mode's full
   panel — there the clock IS the exercise, here it is the gap between two
   things the coach is already watching, so it must not take the screen.
   Read from a metre away while holding a dumbbell: the figure is the size the
   set rows use, and the fill says how much is gone without being read. */
.rn-rest{position:relative;display:flex;align-items:center;gap:12px;width:100%;
  margin-top:10px;padding:13px 15px;overflow:hidden;
  background:var(--sunken);border:1px solid var(--line);border-radius:12px;
  color:var(--ink);cursor:pointer;text-align:left}
/* The fill sits UNDER the text and is never the thing you read — it is a
   quantity you take in without looking at it. */
.rn-rest-fill{position:absolute;left:0;top:0;bottom:0;
  background:color-mix(in oklab,var(--accent) 16%,transparent);
  transition:width .95s linear}
.rn-rest-k,.rn-rest-v,.rn-rest-a{position:relative}
.rn-rest-k{font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted)}
.rn-rest-v{flex:1;font:600 var(--ds-figure)/1 var(--mono);
  font-variant-numeric:tabular-nums;color:var(--ink)}
.rn-rest-a{font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--muted)}
.rn-rest.is-held .rn-rest-fill{background:color-mix(in oklab,var(--muted) 14%,transparent)}

/* An empty day for a coach who is not new. Says so, and offers the one thing
   that would change it — the onboarding card taught them this months ago and
   has no business reappearing. */
.tb-empty{padding:26px 2px 8px}
.tb-empty-t{font:var(--ds-w-semi) var(--ds-title)/1.3 var(--sans);color:var(--ink-2)}

/* The line that closes the day when the hero is the only thing in it. A
   statement, not a row — there is nothing here to tap, so it must not look
   like the session rows above it. */
.tb-last{padding:16px 2px 4px;font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint)}

/* ── the corner when the day is asking ────────────────────────────────────
   Owner 2026-08-26: on Today the round button carries the client notices, as a
   megaphone with a count, and falls back to + Block time when nothing waits.

   NO BLINKING, and that is a considered no rather than an oversight. A control
   that flashes in a fixed corner until it is cleared is a permanent resident —
   the exact thing this app removed the nightly reminder batch and the low-pack
   nudge to avoid — and it is the pattern people turn notifications off over.
   What it does instead is announce itself ONCE when the number goes up: a
   single pulse for something newly arrived, then stillness. The count is doing
   the work; the pulse only says "that changed while you were looking away". */
.tab-fab-circle.has-count{flex-direction:column;gap:1px}
/* The count is the thing being read from a metre away, not the mark above it,
   so it gets the larger of the two. */
.tab-fab-n{font:700 var(--ds-small)/1 var(--mono);letter-spacing:.02em;
  font-variant-numeric:tabular-nums}
@keyframes fabArrived{
  0%{transform:scale(1)}
  38%{transform:scale(1.13)}
  100%{transform:scale(1)}
}
.tab-fab.arrived .tab-fab-circle{animation:fabArrived .5s cubic-bezier(.2,.8,.2,1)}
/* Somebody who has asked for less motion has asked for less motion. */
@media (prefers-reduced-motion:reduce){
  .tab-fab.arrived .tab-fab-circle{animation:none}
}

/* ── what this client is waiting on ───────────────────────────────────────
   Owner 2026-08-26: a per-client banner under the header, above the tabs, only
   there when there is something. It sits above the tabs because the tabs are
   facts about someone's TRAINING and this is a fact about the person — and
   because these notices used to live inside the Sessions tab, where a coach
   reading Progress could not see that the client had asked for an hour. */
.cl-alerts{display:flex;flex-direction:column;gap:2px;margin:2px 0 14px}
.cl-alert{display:flex;align-items:baseline;gap:12px;width:100%;text-align:left;
  padding:12px 14px;background:var(--raised);border:0;border-radius:11px;
  color:var(--ink);cursor:pointer}
.cl-alert:active{background:var(--sunken)}
.cl-alert-k{flex:none;width:62px;
  font:var(--ds-w-semi) var(--ds-label)/1.3 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}
.cl-alert-t{flex:1;min-width:0;font:var(--ds-w-semi) var(--ds-small)/1.35 var(--sans);
  color:var(--ink)}
/* Amber only where somebody could be harmed or is out of pocket — a booking
   request is a person waiting, not a fault. */
.cl-alert.warn .cl-alert-k,.cl-alert.warn .cl-alert-t{color:var(--bad)}

/* ── the defaults, stated rather than asked ───────────────────────────────
   Owner 2026-08-26: the booking form gave "a headache at how long and wordy it
   is". Nine rows of equal weight, six of them the same answer every time. They
   are stated on one line now — the same "show the state, not a description"
   shape the Settings rows use — so the coach reads what is about to happen
   without opening anything, and taps once when today is different. */
.sh-more-v{flex:1;min-width:0;text-align:left;
  font:400 var(--ds-small)/1.35 var(--sans);color:var(--ink-2)}
.sh-more-a{flex:none;font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--accent)}
.sh-more-body{margin-top:4px}

/* The waitlist ask, entered the way it is said: which days, roughly when.
   Chips rather than a multi-select — "Mon, Wed, Fri" is three taps and reads
   back at a glance, which a list box does neither of. */
/* SEVEN ACROSS, ALWAYS. A grid rather than a wrapping flex: at 375px seven
   chips get 43.8px each, which lost a fight with a 44px min-width and dropped
   SUN onto a row of its own — a week that reads as six days and a straggler.
   The row is still 40px tall, so the target clears the floor in the direction
   that actually matters for a thumb. */
.wl-days{display:grid;grid-template-columns:repeat(7,1fr);gap:5px}
.wl-day{min-height:44px;padding:0 2px;border-radius:10px;
  background:none;border:1px solid var(--line-strong);color:var(--muted);cursor:pointer;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase}
/* Chosen wears the accent, like every other chosen thing in this app. */
.wl-day.on{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}

/* The note a coach wrote at the end of the hour, read the next time they open
   it. Quiet — it is context for the decision above it, not the decision. */
.sh-hournote{display:flex;gap:10px;align-items:baseline;margin:-4px 0 14px;
  padding-bottom:14px;border-bottom:1px solid var(--line);
  font:400 var(--ds-small)/1.45 var(--sans);color:var(--ink-2)}
.sh-hournote-k{flex:none;font:var(--ds-w-medium) var(--ds-label)/1.45 var(--mono);
  letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}

/* ── talking instead of typing ────────────────────────────────────────────
   Owner 2026-08-26. On the two fields a coach writes at length — a note about
   a client, a note on a session — and nowhere else: everything else in this app
   is a tap, and a tap is already faster than a sentence.
   The mic sits INSIDE the field's bottom-right rather than beside it, so the
   row keeps its width and the textarea keeps its full measure. */
.mic-wrap{position:relative;display:block}
.mic-wrap > textarea,.mic-wrap > input{padding-right:46px}
.mic{position:absolute;right:8px;bottom:8px;display:flex;align-items:center;
  justify-content:center;width:32px;height:32px;border-radius:50%;
  background:none;border:1px solid var(--line);color:var(--muted);cursor:pointer}
.mic:active{background:var(--surface-2)}
/* LISTENING SAYS SO. A recogniser that is running and a coach who thinks it is
   not is how a whole sentence gets lost, so the state is unmistakable rather
   than a subtle tint — and it pulses, because this is the one place in the app
   where something is genuinely happening while nobody touches the screen. */
.mic.on{background:var(--bad);border-color:var(--bad);color:var(--ink-invert);
  animation:micLive 1.4s ease-in-out infinite}
@keyframes micLive{0%,100%{opacity:1}50%{opacity:.62}}
@media (prefers-reduced-motion:reduce){.mic.on{animation:none}}

/* ── the price list, at the top of a sale ─────────────────────────────────
   Owner 2026-08-26. Selling was nine fields with the one control that fills
   them all in — the coach's own price list — sitting at the BOTTOM as a select,
   under everything it would have replaced. A coach sells the same three things
   over and over; tapping one of them should be the whole job. */
.tpl-pick{display:flex;flex-direction:column;gap:2px;margin-bottom:4px}
.tpl-opt{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  padding:14px 15px;background:var(--raised);border:0;border-radius:12px;
  color:var(--ink);cursor:pointer}
.tpl-opt:active{background:var(--sunken)}
.tpl-opt-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.tpl-opt-n{font:var(--ds-w-semi) var(--ds-strong)/1.3 var(--sans);color:var(--ink)}
.tpl-opt-s{font:var(--ds-w-regular) var(--ds-label)/1.3 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}
/* The price is the number the coach is checking, so it is the one thing set in
   tabular figures at a size that reads from across a room. */
.tpl-opt-p{flex:none;font:600 var(--ds-strong)/1 var(--sans);
  font-variant-numeric:tabular-nums;color:var(--ink)}
/* The last card in the stack is the same object with nothing priced on it, so
   it reads as one more answer to the same question rather than a different
   kind of control. Dashed, because it is the one that opens further work. */
.tpl-opt-alt{background:transparent;box-shadow:inset 0 0 0 1px var(--line)}
.tpl-opt-alt .tpl-opt-n{color:var(--muted)}

/* ── the Progress tab's findings ──────────────────────────────────────────
   Owner 2026-08-26: the tab "has so many useful information but is starting to
   get very heavy". Four headed sections became four rows, each stating what it
   FOUND and opening the rest on a tap. Deliberately not sub-tabs: a mode switch
   asks a question before any work happens, and it would put a 15% side
   difference behind a tab nobody opens.

   NEGATIVE SPACE IS THE POINT. 15px of padding and a 2px gap rather than
   hairlines, so four rows of dense findings still read as four calm lines. */
/* NOT .pg-card — that name was already the client's PORTAL page card, four
   hundred lines down, and it carries a margin-left:12px so those cards indent
   under one address. Reusing the name put a 12px shove on every finding here:
   the whole list sat proud of the container it lived in and hung 12px past its
   right edge, which is the "not well aligned" the owner could see and I could
   not until I measured the boxes. A collision, not a duplicate rule — and the
   older claim keeps the name. */
.pg-finds{display:flex;flex-direction:column;gap:2px;margin-top:22px}
.pg-find{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  padding:15px;background:var(--raised);border:0;border-radius:12px;
  color:var(--ink);cursor:pointer}
.pg-find:active{background:var(--sunken)}
/* The label is fixed-width so the findings line up in a column of their own —
   a coach scans the RIGHT of this list, not the left. */
.pg-find-k{flex:none;width:74px;
  font:var(--ds-w-medium) var(--ds-label)/1.3 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted)}
/* The finding takes the width the chevron used to hold. See pgCard() for the
   rule: an arrow on every row of a uniformly tappable list marks nothing out. */
.pg-find-f{flex:1;min-width:0;font:var(--ds-w-semi) var(--ds-small)/1.35 var(--sans);
  color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A row that found something wears it, so the eye lands there without reading
   the other three. */
.pg-find.warn .pg-find-f{color:var(--bad)}
.pg-find.warn .pg-find-k{color:var(--bad)}

/* ── training balance ─────────────────────────────────────────────────────
   Sets per movement pattern over six weeks. See lib/balance.js for why this
   counts sets rather than tonnage. The bars are scaled to the biggest pattern,
   not to a target — there is no correct number of sets, and what a coach reads
   here is the SHAPE of the six weeks. */
.bal{display:flex;flex-direction:column;gap:9px;margin-top:4px}
.bal-row{display:grid;grid-template-columns:58px minmax(0,1fr) 30px;align-items:center;gap:10px}
.bal-k{font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}
.bal-track{height:8px;border-radius:999px;background:var(--sunken);overflow:hidden}
.bal-fill{display:block;height:100%;border-radius:999px;background:var(--accent)}
/* A pattern with nothing in it is the finding, so it is drawn as an absence
   rather than as a very short bar the eye rounds up to "some". */
.bal-row.is-none .bal-k,.bal-row.is-none .bal-n{color:var(--bad)}
.bal-row.is-none .bal-track{background:transparent;
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--bad) 34%,transparent)}
.bal-n{font:600 var(--ds-small)/1 var(--mono);font-variant-numeric:tabular-nums;
  color:var(--ink);text-align:right}

.bal-notes{display:flex;flex-direction:column;gap:6px}
.bal-note{display:flex;gap:10px;align-items:baseline;margin-top:10px;
  font:400 var(--ds-small)/1.45 var(--sans);color:var(--muted)}
.bal-note.warn{color:var(--bad)}
.bal-note-k{flex:none;min-width:58px;
  font:var(--ds-w-medium) var(--ds-label)/1.45 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}
.bal-note.warn .bal-note-k{color:var(--bad)}

/* ── left against right ───────────────────────────────────────────────────
   A periodic bilateral test, not a side on every set — see lib/balance.js.
   Two sides, side by side, with the weaker one marked: the comparison IS the
   layout, so the coach reads it without doing arithmetic. */
.sym{display:block;width:100%;text-align:left;padding:15px 16px;margin-top:4px;
  background:var(--raised);border:1px solid var(--line);border-radius:14px;
  color:var(--ink);cursor:pointer}
.sym-ex{font:var(--ds-w-semi) var(--ds-strong)/1.3 var(--sans);color:var(--ink)}
.sym-sides{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:12px 0 4px}
.sym-side{padding:11px 12px;border-radius:11px;background:var(--sunken);
  display:flex;flex-direction:column;gap:5px}
/* The weaker side is the finding. Amber, not red — a side difference is
   something to program against, not an injury. */
.sym-side.is-weak{background:var(--bad-bg);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--bad) 30%,transparent)}
.sym-side-k{font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}
.sym-side.is-weak .sym-side-k{color:var(--bad)}
.sym-side-v{font:600 var(--ds-figure)/1 var(--sans);color:var(--ink);
  font-variant-numeric:tabular-nums}
.sym-side-v i{font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);font-style:normal;
  color:var(--muted);margin-left:4px}
.sym-verdict{margin-top:9px;font:400 var(--ds-small)/1.45 var(--sans);color:var(--muted)}
.sym-verdict.watch,.sym-verdict.flag{color:var(--bad)}
.sym-trend{margin-top:5px;font:400 var(--ds-label)/1.45 var(--sans);color:var(--faint)}

/* The rare acts, behind one door that says how many. */
.sh-more{display:flex;align-items:center;justify-content:space-between;width:100%;
  margin-top:14px;padding:13px 14px;background:none;
  border:1px solid var(--line);border-radius:12px;
  font:var(--ds-w-semi) var(--ds-small)/1 var(--sans);color:var(--muted);cursor:pointer}
.sh-more:active{background:var(--surface-2)}
.sh-act-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.sh-act-t{font:var(--ds-w-semi) var(--ds-strong)/1.25 var(--sans);color:var(--ink)}
/* The consequence, in the app's quietest voice — it is read at the moment of
   the decision and never again, so it must not compete with the decision. */
/* ARCHIVO, NOT MONO — the design system's own rule, read off
   foundation-type.html in Showup Client Progress v2 on 2026-08-20:
   "Two voices only: Archivo speaks, Mono labels and measures. A FULL LINE OF
   MONO READS AS TERMINAL OUTPUT — mono is for labels about values, tabular
   digits, captions."
   These are sentences. "Inside 24 h — the session counts as used" is the app
   speaking to the coach at the moment of a decision, and setting it in
   uppercase mono made the most human line on the sheet read like a log file.
   The KEY beside it stays mono, because that is a label. */
.sh-act-s{font:var(--ds-w-regular) var(--ds-small)/1.45 var(--sans);letter-spacing:0;
  text-transform:none;color:var(--faint)}
.sh-act.bad .sh-act-t{color:var(--bad)}
.sh-act.bad .sh-act-s{color:color-mix(in srgb,var(--bad) 70%,var(--muted))}
.sh-act-h{flex:none;display:flex;gap:4px;align-items:center;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--accent)}
.sh-act-h.mut{color:var(--faint)}
.sh-acts{display:flex;gap:10px}
.sh-cancel{flex:none}
.sh-go{flex:1;font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase}
.icon-btn{width:32px;height:32px;border:1px solid var(--btn-bd);border-radius:var(--r-s);display:flex;align-items:center;justify-content:center;color:var(--muted)}
.icon-btn svg{width:16px;height:16px}

/* ---------- misc ---------- */
.divider{height:1px;background:var(--line);margin:14px 0}
.kv{display:flex;justify-content:space-between;align-items:baseline;padding:9px 0;gap:12px}
.kv + .kv{border-top:1px solid var(--line)}
.kv .k{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
.kv .v{font-weight:600;text-align:right;font-size:var(--ds-small)}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
.mut{color:var(--muted)}
.big-amount{font-size:var(--ds-display);font-weight:700;letter-spacing:-.03em;line-height:1.05}
.center{text-align:center}
/* The one way to hide a thing. Everywhere else in this app that reached for
   `style.display='none'` set it inline, which means the stylesheet cannot see
   it and neither can the duplicate-declaration guard. */
.hide{display:none!important}
.mt-1{margin-top:var(--sp-2)}.mt-2{margin-top:var(--sp-3)}.mt-3{margin-top:var(--sp-4)}

/* Utilities added when the views' static inline styles were folded into this
   sheet, then consolidated onto the system (both 2026-08-03): spacing sits on
   the mt/mb-1/2/3 rhythm (8/14/22), text on the three roles below, weight on
   600. Dynamic styles (progress widths, per-client avatar colours) stay inline
   in the views — they are computed from data and cannot live here. */
.left{text-align:left}
.cap{text-transform:capitalize}
.break-all{word-break:break-all}
.hidden{display:none}
.min0{min-width:0}
.p0{padding:0}
.between{display:flex;justify-content:space-between;align-items:center}
.between.top{align-items:flex-start}
.gap-10{gap:10px}
.ink-bad{color:var(--bad)}
.ink-warn{color:var(--warn)}
.ink-accent{color:var(--accent)}
.fw-600{font-weight:600}
/* Outlined accent: for actions that deserve the accent's voice without a
   filled block — the share sheet's copy/create rows wear this so two link
   sections read as one calm system instead of competing lime slabs. */
.btn-outline{border-color:var(--accent);color:var(--accent)}
/* Anchors wearing .btn are buttons to the eye: no underline, ink stays put. */
a.btn{text-decoration:none}
/* The client-page preview inside a sheet. */
/* 60vh, not 70: head + pinned foot + frame must all fit inside the sheet's
   92vh on a phone, or the close controls are the thing that gets cropped. */
.preview-frame{width:100%;height:60vh;border:0;border-radius:var(--r-s);background:#141619}
/* Text roles (consolidated 2026-08-03 from a px-named fs-* sprinkle set):
   fine = metadata and footnotes, note = secondary/informational text,
   body-sm = compact body copy. Sizes live here and only here. */
.fine{font-size:var(--ds-small)}
.note{font-size:var(--ds-small)}
.body-sm{font-size:var(--ds-body)}
.mb-1{margin-bottom:var(--sp-2)}.mb-2{margin-bottom:var(--sp-3)}.mb-3{margin-bottom:var(--sp-4)}
/* Lead paragraph under a heading — settings intros, the plan sheet's
   assessment note, the report sheet's explainer. One class, one voice
   (unified 2026-08-03 from three near-identical variants). Compose with
   .mut. */
.intro{margin:0 2px 14px;font-size:var(--ds-small);line-height:1.5}
/* Inline action sitting in a .section-title row (Add, Edit…): undoes the
   title's uppercase treatment and colours it as an affordance. */
.title-action{color:var(--accent);font-size:var(--ds-small);font-weight:700;text-transform:none;letter-spacing:0}
/* Micro margin nudges: flush kills an inherited top margin, tight/snug pull an
   element 2px/4px off its neighbour. For anything larger use the scales above. */
.mt-0{margin-top:var(--sp-0)}
.mt-tight{margin-top:2px}
.bd-warn{border:1px solid var(--warn)}
/* Secondary line under an item name in list cards (packages, sessions). */
.sub-line{font-size:var(--ds-small);margin-top:2px}
/* One gradient band groups a run of related sections into a single perceived
   chunk among peers — Progress's body data (Measurements → Mobility) today;
   wrap any future multi-subsection run in .band and it joins the system. It
   reaches a little past the content edge so the cards read as sitting ON it,
   and the wash dies to nothing before the band ends, so there is no hard
   lower edge to close it. Earn it: banding a screen's only content groups
   nothing. */
.band{
  /* Flush with the column (owner, 2026-08-15: "make the sides consistent
     with the rest? right now it sticks out too mcuh") — the band groups by
     tint above and below, not by being wider than everything it holds. */
  margin:22px 0 0;padding:2px 0 14px;
  border-radius:var(--r);
  background:var(--band-grad);
}
.band > .section-title:first-child{margin-top:14px}

/* Birthday rows on Home: pill-slim on purpose — a different species from the
   attention cards so a birthday reads as a footnote to the day, not a third
   stack of problems. The cake is the section label. */
.bday-row{
  display:flex;align-items:center;gap:10px;padding:8px 8px 8px 14px;
  background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r-pill);box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
}
.bday-row + .bday-row{margin-top:8px}
.bday-body{display:flex;align-items:baseline;gap:8px;flex:1;min-width:0;text-align:left}
.bday-cake{font-size:var(--ds-body);align-self:center}
/* On a narrow cover screen the META truncates, never the name — "Pr… ·
   turns 37" helps nobody; "Priya Nair · Wed 5…" still says whose day it is. */
.bday-name{font:600 var(--ds-small)/1.2 var(--sans);letter-spacing:-.005em;white-space:nowrap;flex:none;max-width:52%;overflow:hidden;text-overflow:ellipsis}
.bday-meta{font:400 var(--ds-label)/1 var(--mono);letter-spacing:.03em;color:var(--faint);white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}
/* WISHES is a status word, so it wears the status-word face — the same mono
   caps as CONFIRMED on the card above it. In the sans it read as a link in a
   sentence, which made one more typographic voice on a screen that already
   had enough of them. */
.bday-row .title-action{flex:none;padding:6px 10px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ok)}

/* Icon-and-text warning block inside a tinted card. */
.media{display:flex;gap:10px;align-items:flex-start}
.alert-ico{color:var(--warn);flex-shrink:0;margin-top:1px}
.alert-action{margin-top:3px}
/* Square icon badges. Radius 0 — the inline originals carried 9-10px rounding
   in violation of the system and it was corrected in this move. */
/* NO TILE (2026-08-20). Twenty-seven filled squares ran down Settings, one per
   row — the single largest concentration of boxes left in the app, and each was
   a surface drawn around an 18px glyph. The icon still earns its place: a long
   list of settings is scanned by shape before it is read. It is a glyph now,
   not a container. */
/* THE ONE ROW THAT DESTROYS SOMETHING says so on every part of itself — the
   mark, the name and the line under it. It sits at the foot of a list of
   fifteen harmless rows with no heading between them any more, so nothing else
   is standing between the coach and it. */
.row-danger .row-title,.row-danger .row-sub,.row-danger .set-ico{color:var(--bad)!important}
.set-ico{background:none!important;color:var(--faint)!important;width:22px;height:22px;
  border-radius:0;display:flex;align-items:center;justify-content:center;flex:none}
.badge-ico{width:38px;height:38px;background:var(--warn-bg);color:var(--warn);display:flex;align-items:center;justify-content:center;border-radius:var(--r-s)}
/* A whole card acting as one tappable button. */
.card-btn{display:flex;gap:12px;align-items:center;text-align:left;width:100%}
/* Pre-wrapped text blocks: AI/report prose, and the capped share preview. */
.prose-block{font-size:var(--ds-body);white-space:pre-wrap}
.preview-block{font-size:var(--ds-small);white-space:pre-wrap;max-height:40vh;overflow:auto}
/* One-off placements that belong to a specific component. */
.dlg-msg{margin:2px 2px 4px}
.ob-intro{margin:0 2px 16px}
.hint-plain{margin:0;color:var(--ink)}
.hint-above{margin:0 2px 10px}
.hint-below{margin:10px 2px 0}
.hint-tight{margin:-6px 0 14px}
.loc-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
.divider-loose{margin:16px 0}
.divider-top{margin:12px 0 0}
.textarea-tall{min-height:120px}
.input-xs{width:80px}
.end-controls{margin-left:auto;display:flex;align-items:center;gap:8px}
.summary-row{cursor:pointer;padding:8px 0}
.pkg-meta{display:flex;justify-content:space-between;margin-top:7px;font-size:var(--ds-small)}
.pay-hero{margin:4px 0 16px}

/* ---------- motion ----------
   Sleekness is mostly acknowledgement: every touch gets an answer inside one
   frame's worth of easing, and screens arrive instead of appearing. The scale
   is deliberately near-invisible — felt, not watched. view-in plays only when
   the route changes (app.js gates it); store commits repaint in place. */
button{transition:transform var(--dur-1) var(--ease),background-color var(--dur-1) ease,opacity var(--dur-1) ease,border-color var(--dur-1) ease}
button:active{transform:scale(.98)}
.session-item:active,.row:active{transform:scale(.99)}
/* Opacity only. The 6px rise used to travel the WHOLE shell, and a sticky
   child cannot hold still inside a moving ancestor — every route change made
   the frozen heads slide up into place. The fade alone reads the same. */
.shell.view-in{animation:view-in .24s var(--ease)}
@keyframes view-in{from{opacity:0}to{opacity:1}}
/* DIRECTION, ON THE CONTENT ONLY. The note above records why the shell itself
   cannot travel: a sticky child cannot hold still inside a moving ancestor, so
   translating the whole thing made every frozen head slide into place on every
   route change. Moving `.main` instead is not a workaround — it is closer to
   what a native stack actually does. The chrome stays put and the content
   arrives, which is what makes a push feel like a push.
   14px, not 40: felt, not watched. */
.shell.view-fwd .main{animation:slide-fwd .26s var(--ease)}
.shell.view-back .main{animation:slide-back .26s var(--ease)}
@keyframes slide-fwd{from{transform:translate3d(14px,0,0);opacity:.6}to{transform:none;opacity:1}}
@keyframes slide-back{from{transform:translate3d(-14px,0,0);opacity:.6}to{transform:none;opacity:1}}
/* A coach who has asked their phone to stop moving things gets a screen that
   changes without travelling. The app still says something happened — the fade
   above is opacity only and stays. */
@media (prefers-reduced-motion:reduce){
  .shell.view-fwd .main,.shell.view-back .main{animation:none}
}

/* One-time install nudge (js/lib/install.js): floats above the tab bar, never
   shifts layout, gone forever once dismissed or once the app runs standalone. */
.install-hint{
  position:fixed;left:16px;right:16px;z-index:40;
  bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + 14px);
  max-width:calc(var(--maxw) - 32px);margin:0 auto;
  display:flex;align-items:center;gap:10px;
  background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r-s);box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
  padding:10px 12px;
  animation:rise var(--dur-2) var(--ease);
}
.install-hint-copy{flex:1;font:500 var(--ds-small)/1.5 var(--sans);letter-spacing:0;color:var(--muted)}
.install-hint-actions{display:flex;align-items:center;gap:8px;flex:none}
.install-hint-x{width:26px;height:26px;border-color:transparent}
.progress{height:6px;background:var(--soft);overflow:hidden;border-radius:var(--r-pill)}
.progress > span{display:block;height:100%;background:var(--accent)}
.progress.low > span{background:var(--warn)}
.progress.out > span{background:var(--bad)}

/* ---------- shareable business card ---------- */
.bcard{
  background:var(--surface);background-image:var(--card-grad);padding:18px;
  border-radius:var(--r);overflow:hidden;box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
}
.bcard-top{display:flex;align-items:center;gap:13px}
.bcard-name{font-family:var(--display);font-size:var(--ds-heading);font-weight:700;letter-spacing:-.025em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bcard-role{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-top:6px}
.bcard-lines{margin-top:16px;padding-top:14px;border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:10px}
.bcard-line{display:flex;align-items:center;gap:10px;font:400 var(--ds-label)/1.4 var(--mono);color:var(--ink)}
.bcard-line svg{color:var(--faint);flex-shrink:0}
.bcard-line span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ---------- legal / long-form documents ---------- */
.doc{font-size:var(--ds-small);line-height:1.6}
.doc h4{
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  margin:20px 0 7px;color:var(--faint);
}
.doc h4:first-child{margin-top:4px}
/* legal copy is read carefully, not skimmed — keep it at full contrast and let
   the intro and the footnote carry the muted treatment instead */
.doc p{margin:0;color:var(--ink)}
.doc-intro{margin:0 0 4px!important;color:var(--muted)}
.doc-list{margin:4px 0 0;padding-left:19px;color:var(--ink)}
.doc-list li{margin-bottom:5px}
.doc-note{
  margin-top:14px!important;padding:12px 13px;
  border-radius:var(--r-s);background:var(--surface-2);
  font:400 var(--ds-small)/1.55 var(--sans);color:var(--muted);
}

/* toast */
.toast-wrap{position:fixed;left:0;right:0;bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + 20px);z-index:60;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;padding:0 16px}
.toast{
  border-radius:var(--r-s);box-shadow:var(--shadow);
  background:var(--toast-bg);color:var(--toast-ink);padding:12px 15px;
  font:500 var(--ds-label)/1.3 var(--mono);letter-spacing:.06em;text-transform:uppercase;
  animation:rise .2s ease;max-width:var(--maxw);width:100%;
  display:flex;align-items:center;gap:10px;
}
/* A toast that is also a door. The word on the right says so — a notice you
   can tap has to look different from one you cannot, or every toast starts
   getting poked at. */
/* The WRAP is pointer-events:none so a toast never blocks the screen behind it
   — correct for a statement, fatal for one that is also a door. The tappable
   variant takes its own events back; the wrap stays transparent to everything
   else. This is why the first version showed the notice and did nothing. */
.toast-act{cursor:pointer;gap:10px;pointer-events:auto;
  border:1px solid var(--accent);text-align:left;font:inherit}
.toast-go{font:600 var(--ds-small)/1 var(--sans);letter-spacing:0;
  color:var(--accent);flex:none}
.toast .x{margin-left:auto;opacity:.7}


/* ---------- month grid + calendar layout ---------- */
.month-panel{
  background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r);overflow:hidden;box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
  padding:14px;
}
.month-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.month-head .mtitle{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink)}
.month-nav{display:flex;gap:5px}
.month-nav .icon-btn{width:28px;height:28px}
.month-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:1px}
.month-dow{
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;color:var(--faint);
  text-align:center;padding:4px 0 7px;text-transform:uppercase;
}
.mday{
  aspect-ratio:1;display:flex;align-items:center;justify-content:center;
  font-size:var(--ds-small);font-weight:600;position:relative;
}
.mday:active{background:var(--surface-2)}
.mday.out{color:var(--faint);opacity:.4}
.mday.today{color:var(--accent);font-weight:700}
/* SELECTED IS A GREEN RING, everywhere (owner TODO, actioned 2026-08-07). The
   month grid still inverted the cell into an ink slab — the way this app said
   "selected" before 2026-08-03 — while the week strip six pixels above it drew
   the retired-and-replaced hairline ring. One screen, one concept, two
   vocabularies, and the louder of the two was the obsolete one.
   The ring also stops the selected day fighting the accent it sits beside:
   an ink slab bleached the today colour and the session dot out of the cell,
   which is why both needed their own overrides. */
.mday.on{border-radius:var(--r-s);box-shadow:inset 0 0 0 1.5px var(--accent);color:var(--ink)}
.mday.on.today{color:var(--accent)}
.mday .mdot{
  width:4px;height:4px;background:var(--accent);
  position:absolute;bottom:4px;left:50%;margin-left:-2px;
}
.mday.on .mdot{background:var(--accent)}
/* Same in the month panel: the date takes the colour, no corner dot. */
.mday.hol{color:var(--warn)}
.mday.hol.on{color:var(--warn)}

/* inline notice bar (public holiday, connection state) */
.notice{
  display:flex;align-items:center;gap:8px;padding:10px 12px;
  border:1px solid currentColor;border-radius:var(--r-pill);
  font:500 var(--ds-label)/1.3 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  margin-bottom:12px;
}
.notice-hol{color:var(--bad)}
/* a waiting booking request on the client card: warm, tappable, full width */
.notice-req{color:var(--warn);margin:12px 0 0;cursor:pointer;background:none}
.w-full{width:100%}
.ta-left{text-align:left}
.notice-ok{color:var(--ok)}
/* Tinted cards keep a hairline of the tone rather than a wash of it. */
/* Full borders of their own now that .card no longer draws one — a tinted
   alert keeps its outline because it is a statement, not furniture. */
.tint-warn{background:var(--warn-bg);border:1px solid var(--warn)}

/* a booking that overlaps something already in the PT's Google Calendar */
.clash{
  display:flex;align-items:center;gap:5px;margin-top:5px;
  font:500 var(--ds-label)/1.2 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--warn);
}

/* read-only rows mirrored from Google */
.gcal-row{gap:10px}
.gcal-bar{width:2px;align-self:stretch;background:var(--neutral-mark);flex-shrink:0}
.chev-rot{display:inline-flex;color:var(--muted);transition:transform .18s ease}
.chev-rot.open{transform:rotate(180deg)}
.month-inline{display:none;margin-bottom:4px}
.month-inline.open{display:block}
/* toggle already shows the month name — avoid duplicating it in the panel */
.month-inline .month-head{justify-content:flex-end;margin:-4px -4px 2px 0}
.month-inline .month-head .mtitle{display:none}

/* The calendar's two-up layout — a sticky month sidebar beside the day list —
   lived here behind a 900px query. The app is one layout now, so the sidebar
   and the wrappers that positioned it are gone from the markup entirely rather
   than kept and hidden; the inline month toggle is the only month. */

.pkg-history-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;
  padding:12px 0 2px;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.pkg-history-toggle .chev-rot{transition:transform .18s ease}
.pkg-history-toggle .chev-rot.open{transform:rotate(180deg)}

/* history: one collapsed group per closed package, sessions revealed inside */
.hist-group > div > .list{border-top:1px solid var(--line)}
.hist-group .row-title{font-weight:600}

/* ---------- client record: measurements ---------- */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:0 10px}
/* a text button that reads as a link without being one */
.linkish{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}

/* A hairline grid, not a set of floating tiles: the cells share one outer rule
   and divide with 1px, which is how the handoff draws its measurement strip. The
   divider is the cell's own rule clipped at the container edge rather than a gap
   over a coloured backdrop — a gap paints an unfilled slot in line colour, and an
   unfilled slot is exactly what a single headline metric leaves behind. */
/* Bevel treatment (2026-08-04): each metric is its own small floating card
   rather than a cell in one fused slab — the gap does the separating. Values
   sit on the report-stat scale (21px), not the old 26px hero size that made
   Measurements shout over Goals and Composition. */
.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.metrics-one{grid-template-columns:1fr}
/* Identical footprint to .mrow — two lines, same paddings and radius — with
   the sparkline sharing the label row and the value one step larger than an
   mrow's for headline weight without headline bulk. */
.mtile{
  background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r-s);box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
  padding:9px 13px 10px;min-width:0;
  display:grid;grid-template-columns:1fr auto;row-gap:4px;column-gap:8px;align-items:baseline;
}
.mtile-k{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:8px;
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.mtile-k-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mtile-v{font-size:var(--ds-title);font-weight:700;line-height:1.1;letter-spacing:-.02em;white-space:nowrap;font-variant-numeric:tabular-nums}
.mtile-v small{font-size:var(--ds-label);font-weight:500;color:var(--faint);margin-left:2px;letter-spacing:0}
.spark{color:var(--neutral-mark);flex:none;overflow:visible}
/* the delta is uncoloured unless a goal says which way is progress */
.mdelta{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.04em;color:var(--faint);font-variant-numeric:tabular-nums}
.mdelta.good{color:var(--accent)}
.mdelta.off{color:var(--warn)}
/* The tail of the measurement set — everything the client is not being trained on.
   Two-up, because a metric is a label and a number, and that pair fits a 160px cell
   twice over; one per line would run eleven rows down the page to say the same
   thing. Same hairline-grid construction as .metrics above, so the two blocks read
   as one table rather than two components that happen to sit together. */
.mrow-head{font:400 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--fainter);margin:13px 0 6px;padding:0 2px}
/* .metrics above draws its dividers as a 1px gap over a coloured backdrop, which
   works there because the tiles always fill the row. Here they do not — an odd
   metric count leaves a hole, and that trick paints the hole in line colour, so
   the gap in "Girth" looked like a fifth cell that had failed to load. These cells
   carry their own right and bottom rule instead and the container clips the ones
   that fall on its edge, so an unfilled slot is simply surface. */
/* Exactly two, not auto-fill. The point of a fixed count is that "odd" becomes a
   thing the stylesheet can know: auto-fill gives two columns on a phone and three
   at the 520px cap, so no rule could say which cell ends a row. With two, the odd
   one out takes the whole width and there is never a dangling empty slot. */
/* Same Bevel treatment as the headline tiles: every reading floats as its own
   small card, gap-separated — Composition, Girth and Mobility stop being fused
   slabs next to floating neighbours. */
/* Single column of one-line rows — the two-up grid halved every label's room
   and made the group read as tiles again (owner, 2026-08-15: lines of data
   under one clear heading, like the scan printout). */
.mrows{display:grid;grid-template-columns:1fr;gap:0;background:var(--surface);
  border:1px solid var(--line);border-radius:14px;padding:2px 13px}
/* A grid child's min width is its CONTENT width unless told otherwise, so one
   unwrappable injury note was quietly widening the column and carrying the
   +Add button, the pill and the chevron out past the card edge (owner,
   2026-08-15: "injuries and limitations fall out of the card"). Pin the
   children and the flex-grow cell to the track, and the ellipsis the note
   always asked for finally has an edge to stop at. */
.mrows>*{min-width:0}
.row>.grow{min-width:0}
/* It spans so there is no empty slot, but its contents stay inside the left half —
   otherwise the odd row throws its change out to the far edge and breaks the one
   column the eye was running down. */
/* Label across the top, then the reading and its change sharing one baseline. Two
   lines per cell, not three — the tile above needs three because it carries a
   sparkline, and this is precisely the row that does not. */
/* Fixed columns all the way across (owner: "muschle is not alined. make sure
   everything is aligned"). The misalignment was the UNIT: kg and cm have
   different widths in a proportional face, so each row's digits started at a
   different x and the decimals never stacked. The value column is fixed, the
   unit sits in its own fixed-width box, and the digits right-align against
   it — every decimal point on the card now shares a column. */
/* width:100% is the load-bearing line: a grid BUTTON shrinks to its content,
   so each row's 1fr label column resolved differently and every value landed
   at its own x. (Measured: Muscle's row 61px narrower than Weight's. The
   kg/cm unit-width theory in the comment below was wrong — the unit box
   stays because it still steadies the digits, but the row width was the bug.) */
/* FLEX, NOT GRID. Chrome resolved the same grid rule two ways on identical
   sibling buttons — one row's 1fr collapsed to min-content while its twin
   filled the card (measured, same matched rule on both). Buttons and grid
   have a long history of exactly this; flex with fixed basis columns gives
   the same four aligned columns without the quirk. */
.mrow{width:100%;background:none;border:0;border-bottom:1px solid var(--line);border-radius:0;
  display:flex;align-items:center;gap:10px;
  min-height:48px;padding:2px 0;cursor:pointer;font:inherit;color:inherit;text-align:left}
/* The label keeps a hard floor: one row (Sit & reach) rendered its label at
   zero width under both grid and flex while its siblings behaved — the floor
   ends the argument with the layout engine rather than continuing it. */
.mrow-k{flex:1 1 auto;min-width:88px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mrow>span{flex-shrink:0}
.mrow-k.mrow-k{flex-shrink:1}
.mrow-v{}
.mrow .mdisc{flex:0 0 24px}
.mrow-st{}
.mrow-st{font:500 var(--ds-small)/1.3 var(--sans);letter-spacing:0;text-align:right;color:var(--faint)}
.mrow-st.good{color:var(--ok,var(--accent))}
.mrow-st.watch{color:var(--warn)}
.mrow:last-child{border-bottom:0}
.mrow-k{font:400 var(--ds-body)/1.3 var(--sans);letter-spacing:-.01em;color:var(--ink);
  text-decoration:underline;text-decoration-color:var(--line-strong);
  text-decoration-thickness:1px;text-underline-offset:4px}
.mrow-v{font:600 var(--ds-strong)/1.2 var(--sans);letter-spacing:-.015em;text-align:right;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.mrow-v em{font-style:normal;font-weight:400;font-size:var(--ds-small);color:var(--faint)}
.mrow-v em.good{color:var(--ok,var(--accent))}
.mrow-v em.watch{color:var(--warn)}
.mrow-v small{display:inline-block;width:22px;text-align:left;font-size:var(--ds-label);
  font-weight:500;color:var(--faint);margin-left:3px;letter-spacing:0}
/* Right-aligned so the changes form a column the eye runs down, rather than a
   ragged edge chasing however wide each reading happens to be. */
.mrow-d{justify-self:end;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.04em;color:var(--faint);
  font-variant-numeric:tabular-nums;white-space:nowrap}
.mrow-d.good{color:var(--accent)}
.mrow-d.off{color:var(--warn)}
/* A caption, not a header (owner, 2026-08-16: "Latest should use a
   consistent font.. for this type of line") — same voice as every .hint in
   the app: small mono, sentence case, faint. The uppercase tracking it had
   made it read as one more header band above the real ones. */
.meas-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin:0 0 12px;padding:0 14px;font:400 var(--ds-label)/1.5 var(--mono);letter-spacing:.02em;
  color:var(--faint)}
.meas-foot .linkish{letter-spacing:.02em;text-transform:none}
/* The band's own heading and foot line up with the text INSIDE the cards
   below them, not with the card edges (owner, 2026-08-15: "Measurement and
   Add shoud be aligned to teh toher headings"). 13px card padding + 1px
   border = the 14px everything else starts at. */
.band>.mrows-h{padding-left:14px;padding-right:14px}

/* ---------- exercise log ---------- */
.logset{margin-top:9px;border-top:1px solid var(--line);padding-top:9px}
.logrow{display:flex;justify-content:space-between;gap:14px;padding:4px 0;font:400 var(--ds-label)/1.5 var(--mono)}
.logname{color:var(--ink);font-weight:500}
.logval{color:var(--faint);font-variant-numeric:tabular-nums;text-align:right;flex-shrink:0}
/* set number, weight, reps, RPE, remove */
/* THE SET ROW — a thing you tap, not a thing you fill in.
   Sizes here are gym-floor sizes: every control clears 44px in its tappable
   direction because this is used one-handed, mid-session, sometimes through a
   glove. The two steppers get all the spare width; the tick gets a target big
   enough to hit without looking. */
.setrow{display:grid;grid-template-columns:18px 1fr 1fr 44px 26px;gap:6px;align-items:center;margin-bottom:8px}
.setrow .input{padding:10px 9px;text-align:center}
.setn{font:500 var(--ds-label)/1 var(--mono);color:var(--faint);text-align:center}
.setrow-head{margin-bottom:6px}
.setrow-head span{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);text-align:center}

.step{display:grid;grid-template-columns:34px 1fr 34px;align-items:stretch;
  background:var(--surface-2);border:1px solid var(--line);border-radius:11px;overflow:hidden}
.stepb{border:0;background:none;color:var(--muted);font:400 var(--ds-title)/1 var(--sans);
  min-height:44px;cursor:pointer;-webkit-tap-highlight-color:transparent}
.stepb:active{background:var(--line);color:var(--ink)}
.stepv{border:0;background:none;color:var(--ink);cursor:pointer;min-height:44px;padding:0 2px;
  font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.02em;font-variant-numeric:tabular-nums;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stepv.empty{color:var(--faint);font-weight:400}
.step .stepin{width:100%;min-height:44px;border:0;border-radius:0;background:var(--bg);text-align:center;
  font:600 var(--ds-strong)/1 var(--sans);font-variant-numeric:tabular-nums}

/* Untouched rows read as a PROPOSAL: legible, but visibly not yet a record.
   The tick is the only lime on the row, so "what have I actually done" is
   answerable from across the gym. */
.setdone{min-height:44px;border:1px solid var(--line);border-radius:11px;background:var(--surface-2);
  color:var(--faint);display:grid;place-items:center;cursor:pointer;-webkit-tap-highlight-color:transparent}
.setrow.done .setdone{background:var(--accent);border-color:var(--accent);color:var(--bg)}
.setrow:not(.done) .step{opacity:.62}
.setrow:not(.done) .stepv{font-weight:400}
.setx{border:0;background:none;color:var(--line-strong);display:grid;place-items:center;
  min-height:44px;cursor:pointer}
.setx:active{color:var(--warn)}

/* RPE appears under a set once it exists — a rating of work that happened. */
.rpes{display:flex;align-items:center;gap:6px;margin:-2px 0 10px 24px}
.rpel{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;color:var(--faint)}
.rpes .chip-tog{min-height:32px;padding:0 11px;font-variant-numeric:tabular-nums}

.sethint{font:400 var(--ds-small)/1.5 var(--sans);color:var(--faint);margin:2px 0 0}

/* The rest timer. Prescribed rest, counting down, dismissible with a tap —
   it occupies no space until a set is ticked. */
.restbar{display:none;width:100%;margin-top:10px;padding:12px;border-radius:12px;
  border:1px solid var(--accent);background:none;color:var(--accent);cursor:pointer;
  font:500 var(--ds-small)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  font-variant-numeric:tabular-nums}
.restbar.on{display:block}

/* ---------- AI report composer ---------- */
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
.chip{padding:6px 9px;border:1px solid var(--line);border-radius:var(--r-pill);background:var(--surface);
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
/* Selectable version of the same chip, for picking cooking methods. On is a
   filled block — the primary button, the Book tab and this all say "chosen" the
   same way, so there is one thing to learn rather than three. */
.chips-pick{margin-bottom:0}
.chip-tog{cursor:pointer}
.chip-tog.on{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
/* Grams said back as calories and as portions a client can count on one hand.
   Mono because it is a label about the values above it, not a value itself. */
.macro-readout{
  font:400 var(--ds-label)/1.5 var(--mono);letter-spacing:.03em;
  color:var(--faint);margin:-4px 0 16px;
}
.promptbox{background:var(--surface);border-radius:var(--r-s);overflow:hidden;box-shadow:var(--shadow)}
.promptbox-head{display:flex;align-items:center;justify-content:space-between;
  padding:9px 13px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.promptbox-head span{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.promptbox-head .count{letter-spacing:.04em}
.promptbody{padding:13px 14px;max-height:320px;overflow:auto;
  font:400 var(--ds-label)/1.65 var(--mono);white-space:pre-wrap;word-break:break-word}
/* Three tiers so the shape of the prompt is legible at a glance: headings are
   the structure, data lines are what the model must not contradict, and the
   prose is the brief the coach is free to edit. */
.pl-h{color:var(--accent)}
.ctl-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}
.ctl-row > span{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.ctl-row .seg{flex:none}
.ctl-row .seg button{flex:none;padding:8px 13px}
/* IN A SHEET ROW the segmented control shares the width with a key column, so
   equal-width halves are what breaks it: "ONE ON ONE" and "GROUP" each took
   87.5px of a 180px column, which is 71px of text room for a label needing 85 —
   so the long one wrapped onto two lines while the short one sat in a pool of
   its own padding. Sized to content they both fit with room to spare. */
.sh-row-v.seg button{flex:0 1 auto;white-space:nowrap;padding-left:12px;padding-right:12px}
.disclose{font:400 var(--ds-label)/1.6 var(--mono);letter-spacing:.04em;text-transform:uppercase;color:var(--faint);margin:12px 0 0}

/* ---------- rendered report ---------- */
.report{background:var(--surface);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow)}
.report-head{display:flex;justify-content:space-between;align-items:center;gap:14px;
  padding:15px 16px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.report-who{display:flex;align-items:center;gap:11px;min-width:0}
.report-name{font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Block name, week and goals on one strip. Clamped rather than allowed to run,
   because a long programme name would otherwise push the status pill off the
   row — and the pill is the thing a coach reads first. */
.report-title{font:400 var(--ds-label)/1.35 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--faint);margin-top:6px;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
.report-head .pill{flex:none}
.report-body{padding:18px 16px;display:flex;flex-direction:column;gap:18px}
.report-verdict{font:600 var(--ds-title)/1.35 var(--sans);letter-spacing:-.01em;text-wrap:pretty}
.report h4{margin:0 0 9px;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint)}
.report p{margin:0;white-space:pre-wrap;font-size:var(--ds-small);line-height:1.55}
/* delta strip: one outer rule, 1px dividers, cells flow and wrap */
.report-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(124px,1fr));
  border:1px solid var(--line);gap:1px;background:var(--line)}
.rstat{background:var(--surface);padding:12px 13px}
.rstat-k{font:400 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.rstat-v{font:var(--ds-w-bold) var(--ds-figure)/1.1 var(--sans);letter-spacing:-.02em;margin-top:9px;font-variant-numeric:tabular-nums;white-space:nowrap}
.rstat-d{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.04em;color:var(--faint);margin-top:9px;font-variant-numeric:tabular-nums}
.rstat-d.good{color:var(--accent)}
.rstat-d.off{color:var(--warn)}
.rstat-d.bad{color:var(--bad)}
/* Marked lines: a 14px gutter holds the marker so the prose stays flush left
   however wide the mark is. */
.rlines{display:flex;flex-direction:column;gap:7px}
.rline{display:grid;grid-template-columns:14px 1fr;gap:9px;align-items:baseline}
.rline .mk{font:500 var(--ds-label)/1.5 var(--mono);color:var(--neutral-mark)}
.rline .mk.good{color:var(--accent)}
.rline .mk.watch{color:var(--warn)}
.rline .mk.bad{color:var(--bad)}
.rline span:last-child{font:400 var(--ds-small)/1.55 var(--sans);color:var(--ink);text-wrap:pretty}
.report-gaps{border:1px solid var(--line);background:var(--surface-2);padding:13px 14px}
.report-gaps div + div{margin-top:7px}
.report-gaps .g{font:400 var(--ds-small)/1.5 var(--sans);color:var(--muted)}
/* THE CONSULTATION SECTIONS. Prose, in a document otherwise built of anchored
   one-liners — so it needs a reading measure and room to breathe, and it must
   still print. 66ch keeps a paragraph scannable on a phone and on A4. */
.report-para{font:400 var(--ds-small)/1.7 var(--sans);color:var(--ink);
  max-width:66ch;margin:0 0 10px}
.report-para:last-child{margin-bottom:0}
/* Question and answer, set as a pair rather than two lines: the question is the
   client's voice and the answer is the coach's, and the indent is what says so
   without a label on each one. */
.report-qa + .report-qa{margin-top:12px}
.report-q{font:600 var(--ds-small)/1.5 var(--sans);color:var(--ink)}
.report-a{font:400 var(--ds-small)/1.65 var(--sans);color:var(--muted);
  max-width:66ch;margin-top:3px;padding-left:11px;border-left:2px solid var(--line)}
.report-foot{padding:12px 16px;border-top:1px solid var(--line);
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  font:400 var(--ds-label)/1.45 var(--mono);letter-spacing:.05em;text-transform:uppercase;color:var(--faint)}

/* ---------- desktop console (removed) ----------
   Roughly 280 lines of laptop layout used to sit here: a top nav bar, the
   week-and-day console grid, and two-column variants of Settings and Payments.
   The app is a phone app on every screen now — it centres in a 520px column
   and stops. Archived in .archive/desktop-2026-07-29/ rather than left behind
   a media query nobody opens. */

/* Printing targets the report alone — the sheet it sits in, the app behind it and
   the action buttons all have to come out, and it prints on white regardless of
   the theme the trainer happens to be using. Scoped to :has(.report) so hitting
   Cmd+P anywhere else in the app doesn't produce a blank page. */
@media print{
  @page{margin:14mm}
  body:has(.report){
    background:#fff!important;color:#000!important;
  }
  body:has(.report) .tabbar,
  body:has(.report) .fab,
  body:has(.report) .shell,
  body:has(.report) .sheet-head,
  body:has(.report) .sheet-foot,
  body:has(.report) .no-print,
  body:has(.report) .toast{display:none!important}
  body:has(.report) .scrim,
  body:has(.report) .sheet,
  body:has(.report) .sheet-body{
    display:block!important;background:#fff!important;color:#000!important;
    position:static!important;inset:auto!important;max-height:none!important;
    max-width:none!important;overflow:visible!important;
    padding:0!important;margin:0!important;border:0!important;animation:none!important;
  }
  .report{border-color:#ddd;color:#000;position:relative}
  /* AN UNAPPROVED REPORT CANNOT PRINT AS A FINISHED ONE. The sheet's own
     buttons ask for approval first; the browser's print command does not, and
     a PDF is the format a client is most likely to keep, forward and believe.
     Grey rather than red, and behind the text rather than over it: it has to be
     unmissable on the page and still leave the report readable, because a coach
     printing a draft deliberately — for their own notes — is a fair thing to do. */
  .report.draft::before{
    content:'DRAFT — NOT APPROVED';
    position:absolute;top:42%;left:50%;
    transform:translate(-50%,-50%) rotate(-24deg);
    font:var(--ds-w-bold) var(--ds-name)/1 var(--sans);letter-spacing:.06em;color:rgba(0,0,0,.10);
    white-space:nowrap;pointer-events:none;z-index:1}
  /* Sections stay whole. A stat block or a disclaimer split across two sheets
     of paper is how a caveat gets read as half a caveat. */
  .report-stats,.rstat,.report-gaps,.report-note,.report-head,.report-foot,
  .report section,.report h4{break-inside:avoid;page-break-inside:avoid}
  .report h4{break-after:avoid;page-break-after:avoid}
  .report-head,.report-gaps{background:#fafafa!important}
  .report-head,.report-foot,.report-stats,.rstat,.report-gaps{border-color:#ddd}
  .report-stats{background:#ddd}
  .rstat{background:#fff}
  .report-title,.report h4,.report-foot,.rstat-k,.rstat-d,.report-gaps .g{color:#555}
  /* These carry an explicit token colour, which in dark mode is near-white and
     would print as nothing at all. */
  .report-verdict,.rstat-v,.rline span:last-child{color:#000!important}
  /* Marks and deltas print in black. The glyph already says which way a line
     went — that is why it is a triangle and not a coloured dot — and a lime
     accent on white paper is unreadable however good it looks on screen. */
  .rline .mk,.rline .mk.good,.rline .mk.watch,.rline .mk.bad{color:#000!important}
  .rstat-d.good,.rstat-d.off,.rstat-d.bad{color:#555!important}
  .report-head .pill{color:#000!important;border-color:#999!important}
  .report-stats,.report h4,.report p,.rline,.report-gaps{break-inside:avoid}
}

/* One session, one card, stacked with air between them rather than divided by
   hairlines inside a single panel. A run of day is a set of separate things to
   walk into, not rows of a table.
   ------------------------------------------------------------------------
   The 3px rule down the left is the whole status system: accent is fine, amber
   wants attention this week, red wants it today. The word on the right says
   which — colour on its own is not a message, and about one man in twelve
   cannot read this one. */
.session-stack{display:flex;flex-direction:column;}
/* The tone bar is an inset shadow, not a border: a left border on a rounded
   box traces the corner arc as a tapering wedge, while an inset bar clips
   cleanly against the radius. Padding-left carries the 3px the border used
   to occupy so nothing shifts. */
.session-item{
  display:block;width:100%;text-align:left;
  background:var(--surface);
  background-image:var(--card-grad);
  border-radius:var(--r);overflow:hidden;
  box-shadow:inset 0 0 0 1px var(--edge),inset 3px 0 0 var(--line-strong),inset 0 1px 0 var(--hi),var(--shadow);
  padding:12px 14px 13px 17px;
}
.session-item.tone-warn{box-shadow:inset 0 0 0 1px var(--edge),inset 3px 0 0 var(--warn),inset 0 1px 0 var(--hi),var(--shadow)}
.session-item.tone-bad{box-shadow:inset 0 0 0 1px var(--edge),inset 3px 0 0 var(--bad),inset 0 1px 0 var(--hi),var(--shadow)}
/* GREEN PAYS YOU. Scoped to .attn — an attention card — and deliberately not
   to .tone-ok at large: 'ok' is also the resting tone of every healthy booking
   in the app, and a green edge on all of those would put an accent on cards the
   owner's rule reserves for things you act on. Here the edge is doing the one
   job it has left: orange means this costs you (unpaid, expiring, running out),
   green means somebody is asking to give you money (a booking request, a
   waitlist with a gap to fill). Both need doing — being in Needs attention is
   what says that — so the colour is free to say which kind. */
.session-item.attn.tone-ok{box-shadow:inset 0 0 0 1px var(--edge),inset 3px 0 0 var(--accent),inset 0 1px 0 var(--hi),var(--shadow)}
.session-item.tone-done,.session-item.tone-off{box-shadow:inset 0 0 0 1px var(--edge),inset 3px 0 0 var(--line-strong),inset 0 1px 0 var(--hi),var(--shadow)}
/* Calendar cards carry NO accent edge, whatever their tone — on that screen
   the status word is the signal. Declared after the tone rules on purpose:
   at equal specificity the last one wins, and sitting earlier in the file it
   was silently losing to .tone-warn.
   The Book board is the same sessions on a different screen, so it is named
   here too: scoping this to .cal-day alone let an unpaid booking keep its
   orange edge on the board while the very same booking showed none on the
   calendar. One session, one look, wherever it is read. */
.cal-day .session-item,.bk-sess .session-item{box-shadow:inset 0 0 0 1px var(--edge),inset 0 1px 0 var(--hi),var(--shadow)}
/* The next class due, anywhere in the diary: the same hairline green ring
   the selected day wears, and a tag in the corner that names it. One card in
   the app can hold this at a time, so the ring stays meaningful. */
.cal-day .session-item.up-next{position:relative;
  box-shadow:0 0 0 .5px var(--accent),inset 0 1px 0 var(--hi),var(--shadow)}
/* The tag is the one filled lime on this screen — the ring is a hairline
   whisper, so the words that name it have to be solid. */
/* One tag geometry: UP NEXT, CHECK-IN and ACTION are the same box whatever the
   word inside is, so the corner never changes shape from card to card. The
   min-width is set by the longest of them — CHECK-IN — because a shared box
   that the longest word grows out of is not a shared box. */
/* THE SHARED TAG BOX: 86 x --pill-h, the same rectangle the Payment and Nudge
   pills use on the cards below. These are the app's two ways of saying "this
   one needs you" — one filled, one outlined — and two sizes of the same
   statement read as two different kinds of thing. Sized in min-width and
   height rather than padding, so the box holds whatever word is in it.
   No longer absolutely positioned: the tag now sits IN the status slot at the
   top right, having replaced the status word rather than floating below it. */
.up-next-tag,.log-tag,.now-tag{
  min-width:86px;height:var(--pill-h);flex:none;align-self:center;
  display:inline-flex;align-items:center;justify-content:center;padding:0 9px;
  font:600 var(--ds-label)/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--accent-ink);background:var(--accent);border-radius:var(--r-pill)}
/* Next, but not yet agreed. Green says "turn up to this"; orange says "this is
   your next hour and nobody has confirmed it". Same word, because the timing
   fact has not changed — only what it asks of the coach. */
.up-next-tag.warn{background:var(--warn)}
/* A session whose hour has passed with nothing recorded against it: the TAG
   alone carries the orange. No ring — the green ring means "up next" and one
   ringed card at a time is the point; two coloured outlines in one day made
   the list read as an alarm. */
.cal-day .session-item.needs-log{position:relative}
/* The status word is REPLACED in the DOM (see putTag in calendar.js), not
   hidden here — so nothing is left for a screen reader to announce, and the
   corner holds exactly one statement. CONFIRMED describes a future; once the
   hour has gone it is no longer true of that card, and the only honest thing
   left to say is the one asking what happened. The word returns as DONE, LATE
   or NO SHOW the moment the coach says which. */
.log-tag{background:var(--warn)}
/* NOW: the hour the coach is standing inside. It takes the ring that UP NEXT
   wears, because the moment a session starts it stops being next and becomes
   the only card on the board that matters — one ringed card at a time is still
   the rule, and this is the card that earns it. The tag is lime, like UP NEXT,
   for the same reason: nothing is wrong, this is simply where you are. */
.cal-day .session-item.is-live{position:relative;
  box-shadow:0 0 0 1px var(--accent),inset 0 1px 0 var(--hi),var(--shadow)}
.now-tag{background:var(--accent);color:var(--accent-ink)}
/* Green = where you are, the same rule the NOW tag follows. */
.kv .v.v-live{color:var(--accent)}

/* Compact variant — the clients roster wears the same tone-edged card as the
   home stacks (owner call 2026-08-03: one card language across screens), but
   at row height: avatar, name and standing on one line, tighter stack gap.
   Composes with the .row-* anatomy rather than the .si-* block layout. */
.session-stack.stack-tight{gap:8px}
.session-item.si-compact{display:flex;align-items:center;gap:12px;padding:9px 12px 9px 15px}
.si-top{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
/* The top row RESERVES the tag's height on every calendar card, tagged or not.
   A tag is 25px where the status word it replaces is about 12, so the one card
   carrying UP NEXT stood taller than the rest and the list went uneven. Paying
   the height everywhere costs a few pixels per card and keeps the column
   straight, which is what the eye is actually using to scan it. */
.cal-day .session-item .si-top{min-height:var(--pill-h);align-items:center}
/* ...and the card gives that height back out of its own padding, so it lands
   at the size it has always been. The tag is 25px where the status word was
   about 12, which grew every card by 13; the tag carries its own vertical air
   (9px of type in a 25px box), so the padding it displaced was doing nothing
   the tag is not already doing. Card height unchanged at 84px. */
.cal-day .session-item{padding-top:6px;padding-bottom:6px}
/* Sentence case, not caps. The studio joined the time on this line and caps
   were tried; the line is the one thing on the card read at a glance while
   walking, and mixed case is the shape the eye reads fastest. */
.si-time{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.06em;color:var(--muted);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The studio, on the same line as the time and a size below it. Two studios and
   a home visit is the whole vocabulary here, so the word is recognised on shape
   long before it is read — it can afford to be small, and the hour cannot.
   Caps because at this size it is a label rather than a value: the same treatment
   the flag on the other end of the line already wears. */
.si-loc{font-size:var(--ds-micro);letter-spacing:.12em;text-transform:uppercase;color:var(--fainter)}
.si-flag{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--ok);flex:none}
/* The status word owns its colour, not the card's tone: CONFIRMED green,
   PENDING / CANCELLED / LATE / NO SHOW orange, DONE and BLOCKED plain.
   Declared after the tone rules so a cancelled card — grey edge, orange
   word — cannot have its word greyed by its own tone. */
.session-item.tone-done .si-flag,.session-item.tone-off .si-flag{color:var(--faint)}
.session-item .si-flag.f-ok{color:var(--accent)}
.session-item .si-flag.f-warn{color:var(--warn)}
/* NOW: the only flag that is filled rather than lettered. There is at most one
   of these on a board at a time, and for that hour it is the only card on the
   screen the coach is standing inside. */
.session-item .si-flag.f-live{color:var(--accent-ink);background:var(--accent);
  padding:3px 6px 2px;border-radius:var(--r-pill);letter-spacing:.09em}
/* The name is the only thing here set in Archivo. It is the value; everything
   round it is a label about the value, so everything round it is mono. */
.si-name{display:block;font:600 var(--ds-strong)/1.25 var(--sans);letter-spacing:-.01em;color:var(--ink);
  margin-top:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.si-meta{display:block;font:400 var(--ds-label)/1.45 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--faint);margin-top:6px}
.session-item.cancelled{opacity:.55}
.session-item.cancelled .si-name{text-decoration:line-through}
.strike{text-decoration:line-through;color:var(--faint)}

/* Scope-of-practice footer on the progress report. Small, plain, and always
   printed — a document that carries a calorie figure and a body-composition
   trend to a paying client has to say what it is and what it is not. */
.report-note{margin-top:16px;padding-top:12px;border-top:1px solid var(--line)}
.report-note p{font:400 var(--ds-label)/1.5 var(--sans);color:var(--faint);margin:0 0 6px}
.report-note p:last-child{margin-bottom:0}

/* The emergency contact sits with the waiver line — same register, same place
   on the record, because they answer the same question: is this person safe to
   train. A link, not a label: if you are reading it, you are calling it. */

/* The shared empty state, now a dashed bar rather than a filled card with an
   icon tile. The title keeps the bar's mono-caps register; the sentence under
   it drops to sentence case, because it is prose and reads badly in caps. */
.empty-bar.col{gap:8px}
.empty-t{font:inherit;letter-spacing:inherit;text-transform:inherit;color:var(--muted)}
.empty-p{font:400 var(--ds-small)/1.5 var(--sans);letter-spacing:0;text-transform:none;
  color:var(--faint);max-width:270px}
.empty-bar.col .btn{margin-top:2px}

/* A note beside a queue heading — "not snoozable", "comes back on its own".
   Quieter than the sec-link beside it, because it states a fact rather than
   offering an action. */
/* Prose, not a label — see the :has(.sec-note) rule. `font:inherit` gave this
   the heading's mono face and uppercase, so "until these are marked, the
   figures above are short" was set in tracked-out capitals and read as a second
   heading rather than the aside it is. */
.sec-note{font:400 var(--ds-small)/1.35 var(--sans);letter-spacing:0;text-transform:none;
  color:var(--faint);max-width:46ch}

/* The waitlist control on the client record. Same line species as the waiver
   and emergency-contact lines beneath the header — these three are the client's
   standing facts, and the two that can be changed are buttons. */
.cli-wl{display:block;width:100%;text-align:left;background:none;border:0;padding:0;
  color:var(--faint);font:inherit}
.cli-wl.on{color:var(--accent)}

/* THE ROSTER CARD stands at --card-h-lg, level with a calendar session card.
   Both are things the rest of the app is derived from; the summaries that point
   at them stay at --card-h. With the extra room the name takes the same 16px it
   has on a session card — the same person should not be smaller here than they
   are on the calendar. */
.session-item.si-compact.client-card.roster-card{min-height:var(--card-h-lg);gap:7px;
  padding:12px 14px 13px 15px}
.roster-card .row-title{font:600 var(--ds-strong)/1.25 var(--sans);letter-spacing:-.01em}

/* The remaining count etched over the bar it describes. Mono, right-aligned,
   and quiet — it qualifies the bar rather than competing with it. */
.pkg-bar{position:relative}
.pkg-left{position:absolute;right:0;top:-16px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.pkg-left.low{color:var(--warn)}
/* COMP: settled, but not revenue. Neutral rather than the paid green — a
   comped hour is not money in, and colouring it like one would flatter the
   figure every time the coach glanced at it. */
.pill-comp{color:var(--faint);border-style:dashed}

/* An unpaid package wears an orange HAIRLINE around the whole card, not the
   left tone bar. The left bar is the action-card device — it means "this row
   is on a list of things to do". A package is not on a list; it is a record
   the coach opened deliberately, and the debt is a property of the whole
   record, so the whole record is outlined. Drawn as an inset ring rather than
   a border so nothing moves by a pixel when it appears. */
.pkg-owed{box-shadow:inset 0 0 0 1px var(--warn),inset 0 1px 0 var(--hi),var(--shadow)}
/* Price and expiry: ONE row, right-aligned, on the same line as the package
   name. Two stacked lines put the money and its expiry on different baselines
   from the name they belong to. */
.pkg-terms{display:flex;align-items:baseline;gap:7px;flex:none;white-space:nowrap}
.pkg-terms .pkg-amt{font:600 var(--ds-small)/1.2 var(--sans);letter-spacing:-.005em;color:var(--ink)}
.pkg-exp{font:400 var(--ds-label)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
/* A DIARY card carries no accent edge, wherever it is read — the calendar, the
   Book board, and the client record's own session lists are the same session
   on three screens, and it must look the same on all three. The tone class
   stays: it still colours the status flag. Only action cards (the attention
   feed, the queue, the roster) wear the left bar. */
.session-item.si-diary{box-shadow:inset 0 0 0 1px var(--edge),inset 0 1px 0 var(--hi),var(--shadow)}

/* The owed amount IS the request-payment control. Orange like the ring around
   the card, with the send glyph riding after it so it reads as a thing you can
   press rather than a number that happens to be coloured. The 44px overhang
   comes from the shared hit-area rule below. */
.pkg-owe-btn{display:inline-flex;align-items:center;gap:5px;background:none;border:0;
  padding:0;color:var(--warn);cursor:pointer;font:inherit}
.pkg-owe-btn svg{opacity:.8}
.pkg-owe-btn:active{opacity:.6}

/* ---------- Pay: the period spine ----------
   The month control lives INSIDE the collected label, not above the pair of
   figures, because it governs only the right-hand cell. Outstanding is a
   balance as at now and must never appear to move when the month does. */
.pay-month{margin-left:-6px;}
.pay-month-l{}
.pay-step{background:none;border:0;padding:0 4px;color:var(--faint);
  display:inline-flex;align-items:center;cursor:pointer;position:relative}
.pay-step:active{color:var(--ink)}
/* Disabled rather than hidden: a control that vanishes at the edge of its
   range moves the label under your thumb on the very tap that reached it. */
.pay-step[disabled]{opacity:.25;pointer-events:none}
.pay-step::after{content:'';position:absolute;left:50%;top:50%;width:40px;height:40px;
  transform:translate(-50%,-50%)}
/* The month's own total, riding its heading — the list under it adds up to
   this, which is the only claim a ledger section has to make. */
.sec-fig{font:600 var(--ds-small)/1 var(--sans);letter-spacing:-.01em;color:var(--ink);
  font-variant-numeric:tabular-nums;text-transform:none}
/* The year to date lives under the months rather than in the frozen head: it
   is read once, at the end, not the figure you opened the screen for. It uses
   the client summary's fact rows — see .cli-fact — so the app has one way of
   stacking a label against a figure, not two. */

/* The closing line of a statement: a rule above it, the way a total is drawn
   on paper. Nothing else in the app totals a column, so it is scoped to the
   fact row rather than given a class of its own. */

/* An item whose numbers are its own, not the library's. Small and grey — it is
   a fact about where the prescription came from, not a warning. */
.item-pin{display:block;font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--faint);margin-top:3px}

/* The two block steppers sit tight to the right of the block name — they are a
   pair of taps on one control, not two buttons in a row of their own. */
.blk-step{flex:none;gap:6px}
.blk-step .btn{min-width:38px;padding-left:0;padding-right:0}
.blk-step .btn[disabled]{opacity:.3;pointer-events:none}

/* A superset label on a programme row. Mono and boxed so A1 reads as a
   position in the session rather than as part of the exercise name. */
.grp-tag{flex:none;align-self:flex-start;margin-top:1px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.08em;color:var(--accent);
  border:1px solid var(--line-strong);border-radius:var(--r-s);
  padding:4px 5px;min-width:24px;text-align:center}
/* Swaps, as removable chips. */
.chip-row{display:flex;flex-wrap:wrap;gap:6px}

/* ---------- strength ----------
   A sparkline on a list row: a shape, not a chart. Green when the last point
   is at or above the first, orange when it is below — the same two meanings
   the rest of the app gives those colours, and no third colour invented for
   "strength". */
.spk-up path,.spk-up circle{stroke:var(--accent);fill:var(--accent)}
.spk-down path,.spk-down circle{stroke:var(--warn);fill:var(--warn)}
.spk-up path,.spk-down path{fill:none}
.lift-spark{flex:none;display:flex;align-items:center;opacity:.9;margin-right:2px}
.lift-delta{font:700 var(--ds-title)/1 var(--display);letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.lift-chart{width:100%}
.lift-svg{width:100%;height:96px;display:block;overflow:visible}
/* preserveAspectRatio:none stretches the line to the card, which would stretch
   the stroke with it — vector-effect keeps it 2px whatever the scale. */
.lift-svg path,.lift-svg circle{vector-effect:non-scaling-stroke}

/* Pay's disclosure used to have its own head shape here. It shares .rep-link
   with the books line now — two peer lines that each carry a label, a summary
   and a chevron, so the only thing distinguishing them is the direction the
   chevron points: down opens in place, right goes somewhere. */

/* A dashed bar sitting straight under a card wants the same rhythm the cards
   have between themselves; without it an absence crowds the thing above it. */
.card + .empty-bar,.session-stack + .empty-bar{margin-top:10px}

/* The add button on a day heading. Small, in the heading's own register, and
   carrying the shared 44px hit area — the visible target is 14px of icon. */
.day-title.between{display:flex;align-items:center;justify-content:space-between}
.day-add{background:none;border:0;padding:0 2px;color:var(--accent);
  display:inline-flex;align-items:center;cursor:pointer;position:relative}
.day-add:active{opacity:.6}
.day-add::after{content:'';position:absolute;left:50%;top:50%;
  width:44px;height:44px;transform:translate(-50%,-50%)}

/* Reorder and remove, on one item row. The arrows are the same icon rotated —
   one glyph, two directions, no second asset. */
.item-ops{display:flex;align-items:center;gap:0;flex:none}
.item-mv{width:30px}
.item-mv[disabled]{opacity:.22;pointer-events:none}
.flip{transform:rotate(180deg)}
/* Copy sits with the count on a block heading, and with the add on a day. */
.blk-ops,.day-ops{display:flex;align-items:center;gap:8px;flex:none}

/* The nutrition shelf's provenance note. Plain text under its toggle — no
   surface, because a card would give a footnote the same standing as the food
   suggestions the coach came for. */

/* Sets and reps, right of the name. Mono and tabular so a column of them lines
   up digit for digit down a session — which is the whole reason they are here
   rather than buried in the sub-line. */
.dose-r{flex:none;align-self:center;font:500 var(--ds-label)/1 var(--mono);letter-spacing:.02em;
  color:var(--accent);font-variant-numeric:tabular-nums;white-space:nowrap;
  margin-left:auto;padding-left:8px}

/* Advanced: a qualifier on the movement, sitting with its name. Orange, which
   in this app means "needs attention before you act" — which is exactly what
   it means here too. */
.adv-tag{display:inline-block;margin-left:7px;vertical-align:1px;
  font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--warn);border:1px solid var(--warn);border-radius:5px;padding:3px 4px}
/* Demo: the same qualifier shape as Adv, because it says the same KIND of
   thing about the movement — but it is good news rather than a caution, so it
   takes the accent and no border. A ring around every movement that has a
   video would fence half the shelf. */
.vid-tag{display:inline-block;margin-left:7px;vertical-align:1px;
  font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--accent)}
/* A link sitting under the field it belongs to — check the URL you just pasted
   without leaving the form to find the exercise in a session. */
.field-go{display:inline-flex;align-items:center;gap:5px;margin-top:8px;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--accent);text-decoration:none}

/* The programme's "who is this for" line. Two lines then it clips — enough to
   choose from, not so much that eight programmes become a page of prose. */
.row-why{font:400 var(--ds-small)/1.45 var(--sans);color:var(--muted);margin:3px 0 2px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}

/* Nothing to count. Quieter than a real figure, so a roster still reads as a
   column of numbers with a couple of gaps in it rather than a column of words. */
.cc-rem-none{color:var(--fainter)}

/* A list row whose title shares its line with a figure. The title takes what is
   left after the figure, which is fixed — so the figures line up down the page
   however long the names are, and a long name clips rather than pushing the
   number off the row. */
.row-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.row-head .row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row-count{flex:none;font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--fainter);font-variant-numeric:tabular-nums}

/* Secondary actions on a share card: icons on the title row, so copy, preview
   and remove cost one row between them instead of one each. */

/* A URL as one quiet line that copies itself. Not a filled block: an address
   is a reference, and a reference the size of a paragraph stops being one.
   Scheme dropped, token elided in the middle — the tail is what tells two
   addresses apart, so it is the end that survives. */
.url-row{display:flex;align-items:center;gap:4px;
  width:100%;margin-top:8px;padding:6px 6px 6px 10px;background:var(--surface-2);
  border-radius:var(--r-s);color:var(--faint)}
.url-t{flex:1 1 auto;min-width:0;background:none;border:0;padding:0;text-align:left;
  color:inherit;cursor:pointer;font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.url-t:active{color:var(--ink)}
/* The controls that act on the address, ON the address. Small and even: the
   bin is not louder than the copy mark, because deleting is not the more
   likely of the two. */
.url-op{flex:none;background:none;border:0;padding:6px;color:var(--faint);
  display:inline-flex;cursor:pointer;border-radius:var(--r-s)}
.url-op:active{color:var(--ink);background:var(--soft)}
.url-rm:active{color:var(--bad)}
/* The pages hanging off one address, indented so the grouping is visible
   without a second heading level. */
.pg-card{margin-left:12px}

/* The repeat control: a stepper and the date it lands on. The number is what
   you set; the date is what you are actually deciding, so it gets equal
   billing on the same line. */
.rep-row{}
.rep-n{display:flex;align-items:center;gap:4px;flex:none}
.rep-v{min-width:76px;text-transform:uppercase;color:var(--ink);}
.rep-end{font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.06em;text-transform:uppercase;
  color:var(--faint);text-align:right;min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}

/* Two yes/no answers about the same thing, side by side. They were a segmented
   control and a card — three rows and two shapes for what is two checkboxes. */
.check-pair{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin-top:2px}
.check-pair .check-row{padding:8px 0}
.check-row.off{opacity:.4}

/* ---- Getting started: the new-client checklist ---------------------------
   A LIST OF DOORS, not a progress bar. No percentage, no confetti, no crossed
   -out rows: a step that is done simply is not on the list any more, because a
   coach reading this wants what is left, not a record of what they managed.
   Muted throughout — it sits above the packages on a screen whose accent
   belongs to booking, and it is a prompt, not an alarm. */
.start-list{overflow:hidden}
.start-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:11px 14px 10px;border-bottom:1px solid var(--line);
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--faint)}
.start-n{font-variant-numeric:tabular-nums;color:var(--muted)}
.start-row{display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  padding:11px 14px;background:none;border:0;color:var(--faint);cursor:pointer}
.start-row + .start-row{border-top:1px solid var(--line)}
.start-row:active{background:var(--surface-2)}
/* An open ring, not a tick and not a bullet: the shape of something waiting to
   be filled in. Amber would make six ordinary chores look like six problems. */
.start-dot{width:13px;height:13px;border-radius:50%;border:1.5px solid var(--btn-bd);flex:none}
.start-l{font:500 var(--ds-small)/1.35 var(--sans);letter-spacing:-.01em;color:var(--ink)}
.start-h{font:400 var(--ds-small)/1.35 var(--sans);color:var(--fainter);margin-top:1px}

/* ---- What each tab is for ------------------------------------------------------
   A LEGEND FOR THE TAB BAR: one row per tab, in bar order, wearing the bar's
   own icon. The icon is the only ornament and it is load-bearing — it is what
   turns four sentences into "that button, there". */
.prin{display:flex;gap:12px;padding:13px 14px}
.prin + .prin{border-top:1px solid var(--line)}
.prin-i{width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;
  border-radius:var(--r-s);background:var(--surface-2);color:var(--faint)}
.prin-t{font:600 var(--ds-body)/1.3 var(--sans);letter-spacing:-.015em;color:var(--ink)}
/* The closing notes: no card, no icon, no row. They are not places, and the
   moment they are boxed like the four above them a coach starts looking for
   the tab they belong to. */
/* The quiet second line — what a screen also does, under what it is for. */
.prin-x{font:400 var(--ds-small)/1.45 var(--sans);color:var(--fainter);margin-top:6px}
/* IN and OUT, as bullets. No markers: the bold lead-in is the marker, and a
   disc in front of it would put two bullets on the same line. */
.prin-b,.note-list{list-style:none;margin:8px 0 0;padding:0;display:flex;flex-direction:column;gap:6px}
.prin-b li,.note-list li{font:400 var(--ds-small)/1.45 var(--sans);color:var(--fainter);
  padding-left:10px;border-left:1.5px solid var(--line)}
.prin-b strong,.note-list strong{color:var(--muted);font-weight:600}
.prin-d{font:400 var(--ds-small)/1.45 var(--sans);color:var(--faint);margin-top:3px}

/* The bin is not half the row. It is one act with no label, sat beside a button
   whose words are the reason you are looking at this row at all. */
.btn-row .btn-trash{flex:0 0 62px;color:var(--faint)}
.btn-row .btn-trash:active{color:var(--bad)}

/* ---- THE REPORT (owner, 2026-08-08) --------------------------------------
   This region used to be cards: filled panels, chevrons, a row per finding. It
   replaced two dials, and the owner's note on seeing it was that a trainer is
   not necessarily good with numbers and this should read like a REPORT.

   So the chrome goes and typography does the work instead. A card says "tap
   me"; a report says "read me", and the difference is almost entirely fills
   and rules — ruled sections rather than filled panels, a figure hung in its
   own column so the eye can run down it, and the explanation set in the SANS
   face at a readable size rather than in 10px mono like every other hint in
   the app. Mono is right for a caption nobody has to read. It is wrong for the
   paragraph that says why a client is being singled out.

   The one piece of chrome that stays is the client's name as a control,
   because the finding is useless if you cannot get from it to the person. */
.rep{margin-top:2px}
.rep-lead{font:400 var(--ds-body)/1.55 var(--sans);color:var(--ink);margin:0 2px 6px}
.rep-lead b{font-weight:600;font-variant-numeric:tabular-nums}

/* A ruled heading: the label, then a hairline running out to the edge. It
   divides without boxing, which is the whole distinction being drawn here. */
.rep-h{display:flex;align-items:center;gap:10px;margin:20px 2px 2px;
  font:500 var(--ds-label)/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint)}
.rep-h .rule{flex:1;height:1px;background:var(--line)}
/* NIL, ON THE HEADING LINE. A section with nothing in it used to cost a
   heading plus a two-line paragraph saying "nobody" — three lines to report
   an absence, on a screen whose whole argument is that the findings should
   dominate. A schedule marks a nil line on the line itself, and so does this.

   It is stated rather than hidden on purpose: an empty section proves the app
   looked, and silence cannot tell "checked, all clear" from "not watching".
   The nil text carries the test as well as the result, so the one line does
   the job the paragraph was doing. */
.rep-h .nil{flex:none;color:var(--fainter);letter-spacing:.1em}

/* One finding. The sentence wraps and the figure does not — it holds its own
   column so a coach can read straight down the money without reading a word. */
.rep-row{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  padding:11px 2px;border-bottom:1px solid var(--line)}
.rep-row:last-of-type{border-bottom:0}
.rep-s{flex:1;min-width:0;font:400 var(--ds-small)/1.5 var(--sans);color:var(--muted)}
/* The name is the door. Underlined rather than coloured: an accent here would
   compete with the figure, which is the thing that should carry colour. */
.rep-who{font:600 var(--ds-small)/1.5 var(--sans);color:var(--ink);
  background:none;border:0;padding:0;text-align:left;
  text-decoration:underline;text-underline-offset:2px;
  text-decoration-color:var(--line-strong)}
.rep-who:hover{text-decoration-color:var(--ink)}
.rep-v{flex:none;text-align:right;font:600 var(--ds-body)/1.35 var(--sans);
  letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.rep-v span{display:block;font:400 var(--ds-label)/1.3 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--faint);margin-top:2px}

/* The total, and the caveat under it. The total is a rule-topped line because
   that is what a total looks like on paper. */
.rep-tot{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  padding:10px 2px 0;border-top:1px solid var(--line-strong);
  font:400 var(--ds-small)/1.5 var(--sans);color:var(--ink)}
.rep-tot b{font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
/* Prose, in the reading face. The app's .hint is 10px mono and right for a
   caption; this is a paragraph a coach has to actually take in. */
.rep-note{font:400 var(--ds-small)/1.55 var(--sans);color:var(--faint);margin:9px 2px 0}
.rep-none{font:400 var(--ds-small)/1.55 var(--sans);color:var(--muted);margin:11px 2px 0}

/* THE WAITLIST ROW, WITH ITS TWO ANSWERS. The name on its own line and the two
   choices under it at equal width: side by side they collided with the name at
   phone width, and "Just Tue 12 Aug" is not a label that shortens gracefully.
   Stacked, each button is a full thumb wide and the pair reads as one question
   with two answers rather than a control and an afterthought. */
.wl-row{display:block;padding:12px 14px}
.wl-row .row-title{display:block}
.wl-opts{display:flex;gap:8px;margin-top:9px}
.wl-opts .btn{flex:1 1 0;min-width:0}

/* THE BOOKS, AS A LINE (owner, 2026-08-08). It was a filled card sitting under
   a report, which made the export look like the most important thing on the
   screen when it is the footnote — the place the figures come from, for the
   evening somebody actually wants them. A rule above it and a chevron at the
   end says "there is more through here" without a panel claiming the eye. */
.rep-link{display:flex;align-items:baseline;gap:10px;width:100%;
  margin-top:20px;padding:13px 2px 0;border-top:1px solid var(--line);
  background:none;border-left:0;border-right:0;border-bottom:0;text-align:left;
  color:var(--ink);position:relative}
.rep-link-t{flex:none;font:600 var(--ds-small)/1.4 var(--sans)}
.rep-link-s{flex:1;min-width:0;font:400 var(--ds-label)/1.4 var(--mono);color:var(--faint);
  letter-spacing:.01em;overflow:hidden;
  /* "2026/27 · 14,500 in · 22,950 not yet given" was cut after "not yet" —
     a money figure with its qualifier missing is the one truncation that can
     actually mislead. */
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  font-variant-numeric:tabular-nums}
/* Same line, same face — but a summary reporting a finding is not the same
   kind of sentence as one reporting a total, so it takes the ink. The FORM
   stays identical, which is what makes the two lines read as a pair; only the
   weight of the news changes. */
.rep-link-s.on{color:var(--ink)}
.rep-link .chev{flex:none;display:flex;color:var(--faint)}
/* The whole line is the target, so the 44px floor is met by the row itself
   rather than by an invisible pad around the chevron. */
.rep-link{min-height:44px}

/* The caption on a link card: what it does to your day, one line, sitting
   against the thing it describes. Small enough to be skipped by a coach who
   already knows, close enough to be read by one who does not. */
/* THE PORTAL CARD'S EXTRAS (1.9.4). The pause chips are the four tabs that
   used to be four cards: pressed-in means live, popped-out ("off") means the
   tab shows its paused message on the client's page. The QR wrap gives the
   canvas a white mat — scanners want quiet margin, and the app is dark. */
.pg-pauses{display:flex;flex-wrap:wrap;gap:7px;margin-top:11px}
.pause-chip{display:inline-flex;align-items:center;gap:6px;padding:7px 11px;
  border:1px solid var(--line);border-radius:999px;background:none;cursor:pointer;
  font:600 var(--ds-label)/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
.pause-chip.off{color:var(--warn);border-color:var(--warn)}
.qr-wrap{display:inline-block;background:#fff;padding:14px;border-radius:14px;margin-top:6px}
.qr-wrap canvas{display:block}
.pg-note{font:400 var(--ds-small)/1.4 var(--sans);color:var(--fainter);margin-top:2px}

/* The send action, at icon size beside the preview. Tinted, because of the two
   buttons on that row it is the one that leaves the app. */
.icon-btn.icon-send{color:var(--accent)}

/* ---- What one calendar entry looks like ----------------------------------
   Drawn the way every calendar app on a phone draws an event: a coloured spine
   on the left, title, then the hours, then the place. It is a portrait of
   somebody else's screen, so it borrows their anatomy rather than this app's —
   the one place in Showup where looking like something else is the point. */
.cal-prev{border-radius:var(--r);overflow:hidden;background:var(--surface-2);
  box-shadow:inset 0 0 0 1px var(--edge)}
.cal-prev-cal{display:flex;align-items:center;gap:6px;padding:9px 12px;
  border-bottom:1px solid var(--line);
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.cal-prev-ev{margin:12px;padding:11px 13px;background:var(--surface);
  border-radius:var(--r-s);border-left:3px solid var(--accent);
  box-shadow:var(--shadow)}
/* A crossed-out entry keeps its place in the feed and loses its colour, the
   way a calendar app greys a declined event rather than deleting it. */
.cal-prev-ev.off{border-left-color:var(--line-strong)}
.cal-prev-ev.off .cal-prev-t{color:var(--faint);text-decoration:line-through}
.cal-prev-t{font:600 var(--ds-strong)/1.3 var(--sans);letter-spacing:-.015em;color:var(--ink)}
.cal-prev-when{font:400 var(--ds-small)/1.4 var(--sans);color:var(--muted);margin-top:3px;
  font-variant-numeric:tabular-nums}
.cal-prev-w{font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint);margin-top:1px}
/* The package line, set apart the way a calendar app sets the notes field
   apart from the event's own fields. */
.cal-prev-d{font:400 var(--ds-small)/1.45 var(--sans);color:var(--fainter);margin-top:8px;
  padding-top:8px;border-top:1px solid var(--line)}

/* The state line above a paired set of buttons: what this thing IS, so the
   labels beneath it are read against something instead of assumed. Mono and
   small — it is a fact about the object, not a heading and not an instruction.
   Amber only when other people are on the programme, which is the one version
   of this line that changes what a coach should do next. */
.own-line{font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--faint);margin-top:14px}
.own-line.warn{color:var(--warn)}

/* The training-max value on its row: the number is what the eye is here for,
   so it is mono and tabular and sits against the chevron. Muted when it is an
   estimate rather than something the coach typed — same shape, quieter, so a
   column of them reads at a glance as "these three are mine, that one is a
   guess". */
.row-v{font:500 var(--ds-small)/1 var(--mono);color:var(--ink);font-variant-numeric:tabular-nums;
  flex:none;margin-right:2px}
.row-v.mut{font-weight:400;color:var(--faint)}

/* What the app worked out, said out loud: the target, the weight, and where
   that weight came from — or, when it could not get one, why. It sits directly
   under the exercise picker because it is a consequence of that choice. */
.pres-line{font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint);margin-top:-2px;
  font-variant-numeric:tabular-nums}
.pres-line:empty{display:none}
.pres-line.warn{color:var(--warn)}

/* ══ THE REQUESTS INBOX, CARDLESS ═════════════════════════════════════════
   Eleven filled panels sat in the app's most-visited destination — every "from
   your clients" row lands here — while every screen around them had moved to
   the heading-plus-ruled-list anatomy. One request is a GROUP: a heading that
   names who and what, a ruled list of the facts, and the answers underneath.

   The heading carries the KIND on the right, which the panels buried mid-
   sentence. It is the one thing a coach sorts this sheet by, so it gets the
   column the eye runs down. */
.rq + .rq{margin-top:4px}
.rq .cl-sect{margin-top:22px}
.rq:first-child .cl-sect{margin-top:6px}
/* The name never wraps and never shrinks; the kind is one word on the right.
   The first pass put the whole ask in the heading and it wrapped to three
   lines, taking the name with it — owner's standing rule: no wrapped text. */
.rq-head > span:first-child{flex:none;white-space:nowrap;color:var(--ink);
  letter-spacing:.1em}
.rq-head > span:last-child{flex:none;color:var(--faint)}
.rq-head.warn > span:last-child{color:var(--warn)}
/* The answers hang off the same left rule as the facts they answer, so a
   request reads as one column rather than a list with a toolbar under it. */
.rq-acts{margin-left:calc(var(--tb-hero) * .29);padding-left:12px;
  border-left:1px solid var(--line);padding-top:2px;padding-bottom:2px;flex-wrap:wrap}
/* A LABEL IS ONE LINE. Four answers on a phone made "Set up the package" three
   lines tall inside its own button, which is the wrapped text the owner keeps
   striking out. The ROW wraps instead — two tidy rows of whole words beats one
   row of shredded ones. */
/* ON THE KIT (2026-08-20). Eighteen small outlined pills sat under the request
   cards — three per card, all the same weight, so "Accept", "Send details" and
   "Already handled" competed as equals when only one of them is why the card is
   there. The kit's answer everywhere else is one accent action and quiet mono
   for the rest, so that is what these become.

   Restyled here rather than at eighteen call sites: every branch goes through
   one acts() helper, the classes already encode the intent (primary = the
   thing, soft = the alternative, ghost = the way out), and a stylesheet change
   cannot alter behaviour. */
.rq-acts .btn{white-space:nowrap}
.rq-acts .btn-soft,.rq-acts .btn-ghost{
  background:none;border-color:transparent;padding-left:0;padding-right:0;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase}
.rq-acts .btn-soft{color:var(--accent)}
/* The way out is the quietest thing on the card — it is the answer when none of
   the others applies, and it should never be the first word the eye lands on. */
.rq-acts .btn-ghost{color:var(--faint)}
.rq-acts .btn-soft svg,.rq-acts .btn-ghost svg{display:none}
/* Space between text actions rather than between pills. */
.rq-acts{gap:18px}

.req-slot{font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint);margin-top:7px}
.req-slot.t-warn{color:var(--warn)}
.req-slot.t-ok-strong{color:var(--accent)}

/* THE DAY AROUND THE ASK. "Is that slot free" was already answered above; this
   is the other half of the same decision — whether the coach is already in the
   studio at nine, or crossing town for one hour. Wrapping chips rather than a
   list: it is glanced at, not read, and it must never be taller than the card
   it sits inside. */
.req-day{display:flex;flex-wrap:wrap;align-items:baseline;gap:5px 7px;margin-top:9px;
  margin-left:calc(var(--tb-hero) * .29);padding-left:12px;border-left:1px solid var(--line)}
.req-day-l{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--faint);width:100%}
.req-day-s{font:400 var(--ds-small)/1 var(--sans);color:var(--muted);
  background:var(--surface-2);border:1px solid var(--line);border-radius:999px;padding:5px 9px}
.req-day-s b{font-family:var(--mono);font-weight:500;color:var(--ink)}
/* The requested hour, threaded into the day in time order. Outlined in the
   accent rather than filled: it is the one chip that is NOT booked, and a solid
   fill would read as the most confirmed thing on the row. */
.req-day-s.asking{border-color:var(--accent);background:transparent;color:var(--accent)}
.req-day-s.asking b{color:var(--accent)}

/* A PAUSED PAGE, on the coach's side. Dimmed rather than hidden: the link is
   still live and still theirs to send, and a card that vanished would read as
   removed — which is the thing pausing exists to avoid. */
.pg-card.pg-paused{opacity:.6}
.pg-pill{margin-left:8px;padding:2px 7px 1px;border-radius:var(--r-pill);
  background:var(--surface-2);color:var(--faint);
  font:500 var(--ds-label)/1.5 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  vertical-align:middle}

/* THE RANGE SWITCH lives on the figure it changes — the week count was already
   the reason a coach looks at the right of that line. Not a button that looks
   like a button: it is the same text it always was, with a chevron admitting
   it does something. */
.cal-range{display:inline-flex;align-items:center;gap:5px;background:none;border:0;
  padding:0;color:inherit;font:inherit;cursor:pointer}
.cal-range .chev-rot{display:inline-flex;transition:transform .18s ease}
.cal-range .chev-rot.open{transform:rotate(180deg)}

/* A DAY INSIDE THE WEEK VIEW. A heading, not a card: the cards below it are the
   sessions, and a day that competed with them would make seven boxes of boxes.
   Tapping one drops back to that single day, because the week is for deciding
   which day you actually need. */
.wk-head{display:flex;align-items:baseline;gap:10px;width:100%;text-align:left;
  background:none;border:0;padding:14px 2px 7px;cursor:pointer;
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--faint)}
.wk-head.on{color:var(--accent)}
.wk-n{font-variant-numeric:tabular-nums;color:var(--fainter)}
.wk-head.on .wk-n{color:var(--accent)}

/* The strip's own caption: what those three numbers are FOR. Sits under the
   split, above the contact row, quiet enough that a coach who already knows
   reads straight past it. */
/* Kept for the export sheet, which uses it to label the period slider. */
.cli-col-h{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--fainter);padding-bottom:2px}

/* ---- THE CLIENT HEADER, IN THREE REGISTERS (owner, 2026-08-10) --------
   Two columns of label-and-value gave the card four ragged alignment edges and
   no two values started at the same x. Each register now takes the shape its
   content already wants: numbers in the stat pair the rest of the app aligns
   on, yes/no facts as chips, and the training in plain sentences. */

/* State, not a control — so a chip, not a row spending its width on the words
   "None on file". Quiet when it is done; orange only when it is the thing
   standing between the coach and a session. */
.cli-chips{display:flex;flex-wrap:wrap;gap:7px;padding:11px 14px 0}
.cli-chip{display:inline-flex;align-items:center;gap:5px;
  padding:4px 9px 4px 7px;border-radius:100px;
  font:500 var(--ds-label)/1 var(--sans);letter-spacing:.01em;border:1px solid transparent}
.cli-chip svg{flex:none}
.cli-chip.on{color:var(--faint);border-color:var(--line)}
.cli-chip.off{color:var(--warn);border-color:var(--warn)}

/* Unlabelled on purpose: "Base build · Phase 1 · week 4 of 8" says what it is,
   and a label in front of it would be a third word for the same idea. */
.cli-lines{padding:11px 14px 13px;display:flex;flex-direction:column;gap:3px}
.cli-line{font:400 var(--ds-small)/1.45 var(--sans);color:var(--ink)}
.cli-line.mut{color:var(--faint);font-size:var(--ds-small)}
.cli-line.warn{color:var(--warn)}

/* ---- THE SEND SHEET ---------------------------------------------------
   Who it is going to and what it is, then the act, then the words. The
   summary sits above the button because a coach sending five of these in a
   row needs to know which client this one is for without reading the message
   to find their name in it. */
.send-sum{font:400 var(--ds-small)/1.5 var(--sans);color:var(--muted);margin:0 2px 12px}
.send-sum b{color:var(--ink);font-weight:650}
/* The label and Copy share a line above the box, so the box itself starts at
   the same x as everything else in the sheet and nothing floats over it. */
.send-head{display:flex;align-items:baseline;justify-content:space-between;
  gap:12px;margin:16px 2px 6px;font:400 var(--ds-small)/1.4 var(--sans);color:var(--faint)}
.send-acts{display:flex;gap:16px;flex:none}
/* Mono, because what is in this box is going out character for character and
   the coach is proof-reading it, not composing prose. */
.send-box{width:100%;font:400 var(--ds-label)/1.65 var(--mono);min-height:150px;resize:vertical}

/* ---- THE NEW REQUEST KINDS -------------------------------------------
   A question is prose and a receipt is a photograph; neither fits the
   label-and-value shape the booking cards use. */
.q-text{font:400 var(--ds-small)/1.55 var(--sans);color:var(--ink);margin:10px 0 0;
  padding:10px 12px;border-left:2px solid var(--line-strong);background:var(--surface-2);
  border-radius:0 8px 8px 0}
.q-nums{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.q-num{font:400 var(--ds-small)/1 var(--sans);color:var(--muted);padding:6px 9px;
  border:1px solid var(--line);border-radius:100px}
.q-num b{color:var(--ink);font-weight:650;font-variant-numeric:tabular-nums}
/* A thumbnail wide enough to read an amount off, tappable for the rest. The
   coach is checking a reference against their bank app, not admiring it. */
.rcpt{display:block;width:100%;margin-top:10px;padding:0;border:1px solid var(--line);
  margin-left:calc(var(--tb-hero) * .29);width:calc(100% - var(--tb-hero) * .29);
  border-radius:10px;overflow:hidden;background:var(--surface-2);cursor:pointer}
.rcpt-img{display:block;width:100%;max-height:190px;object-fit:cover}
.rcpt-full{display:block;width:100%;height:auto;border-radius:10px}
/* The right column reads as prose, so its values wrap instead of ellipsising —
   "week 4 of 4" losing its tail is the one thing on that side worth reading. */

/* The one-line answer above the sources. A coach who is not being challenged
   never opens the door and still knows the numbers came from somewhere. */

/* ---- Hit areas -----------------------------------------------------------
   A CONTROL IS AS BIG AS YOUR THUMB, NOT AS BIG AS ITS INK. Audited 2026-08-07
   across every route: two controls sat under the WCAG 2.5.8 floor of 24px —
   "9 more", the door to the whole queue, at ELEVEN pixels high, and the week /
   day range switch I added the day before at fifteen. Both are text, both are
   on the busiest screen in the app, and both were missed because they look fine
   sitting in a line of type.
   The ink does not move. An invisible box is centred behind each one, the same
   trick .pay-step already used — so a small label stays a small label and the
   thumb gets 44px to land in. */
.sec-link, .cal-range, .title-action, .me-btn, .bday-body { position:relative }
.sec-link::after, .cal-range::after, .title-action::after, .me-btn::after {
  content:'';position:absolute;left:50%;top:50%;
  min-width:44px;min-height:44px;width:100%;height:100%;
  transform:translate(-50%,-50%)}

/* ————— 2.4.0: the bell, the x, and the readability floor ————————————————
   The review queue lives behind a bell in the Home header (owner, 2026-08-14:
   "should it be a bell opening to its own page, or its own button on the lower
   content bar?"). Bell: the bottom bar is four rooms plus create and must
   never crowd, and the queue is a review surface you visit when the badge
   says so, not a place you live. */
.bell-btn{position:relative;width:38px;height:38px;border:0;border-radius:12px;
  background:none;color:var(--muted);display:grid;place-items:center;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
.bell-btn:active{background:var(--surface-2);color:var(--ink)}
.bell-badge{position:absolute;top:2px;right:0;min-width:17px;height:17px;padding:0 4px;
  border-radius:999px;background:var(--warn);color:var(--ink-invert);
  font:700 var(--ds-label)/17px var(--mono);text-align:center;
  /* the ground's colour ringing the badge so it reads as sitting ON the bell */
  box-shadow:0 0 0 2px var(--bg)}

/* A message the coach chose to close (dismissable() in ui.js). The x sits in
   the row's own padding, 44px of target, and never overlaps the content. */
.dismissable{position:relative}
.dismissable>.dismiss-x{position:absolute;top:2px;right:2px;width:34px;height:34px;
  border:0;border-radius:10px;background:none;color:var(--fainter);
  display:grid;place-items:center;cursor:pointer}
.dismissable>.dismiss-x:active{color:var(--ink);background:var(--surface-2)}
/* Rows that carry their own right-side action shift it clear of the x. */
.dismissable .bday-row{padding-right:36px}
/* Today's note rows carry a WISHES link on the right, which the x was landing
   on top of. Same fix, sized to this row's own action rather than the old
   card's. */
.dismissable .tb-note{padding-right:32px}
.dismissable > .dismiss-x{top:50%;transform:translateY(-50%);right:-4px}

/* Metric history sheet (coach app) — mirror of the portal's: date left, value,
   step-delta judged against the goal. Tiles and rows became buttons; keep
   their look, add the affordance only on press. */
button.mtile, button.mrow { text-align: left; cursor: pointer; font: inherit; color: inherit; }
button.mtile:active, button.mrow:active { background: var(--surface-2); }
.mh { margin-top: 4px; }
.mh-row { display: flex; align-items: baseline; gap: 12px; padding: 10px 2px;
  border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.mh-row:last-child { border-bottom: 0; }
.mh-d { flex: 1; color: var(--faint); font-size: var(--ds-small); }
.mh-v { font-weight: 600; }
.mh-v small { color: var(--faint); font-weight: 400; margin-left: 2px; }
.mh-dd { width: 64px; text-align: right; font-size: var(--ds-small); color: var(--faint); }
.mh-dd.good { color: var(--ok, var(--accent)); }
.mh-dd.watch { color: var(--warn); }
.mh-goal { font: 500 var(--ds-label)/1 var(--mono); letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 8px; }

/* The group's name inside its card, same as the portal (owner: uniformity). */
.mrows-h{text-transform:uppercase;
  color:var(--ink);}
.mrows{margin-bottom:12px}

/* ————— 2.8.0: THE DAY IS THE CARD, sessions are its rows —————————————————
   (owner, 2026-08-15: "can we apply the same cardless concept to the calendar
   and home page for booekd sessions?" then "please do this across the board?
   I think the cards were what made the format too messy".)
   Card = group, never row. The stack — one day's sessions, or the Most-urgent
   group on Home — becomes the surface; every session/attention item inside
   flattens to a hairlined row. The 3px inset tone bar SURVIVES: it is the
   money signal, and a flat row carries it as well as a card ever did.
   The up-next / live rings become accent bars for the same reason — on a
   flat row an outline ring has no card edge to sit on. */
.session-stack{overflow:hidden;gap:0;}
.session-stack>.session-item,
.session-stack>.session-item.si-compact.client-card{
  background:none;background-image:none;border-radius:0;
  box-shadow:inset 3px 0 0 var(--line-strong);
  border-bottom:1px solid var(--line);
}
.session-stack>.session-item:last-child{border-bottom:0}
.session-stack>.session-item.tone-warn{box-shadow:inset 3px 0 0 var(--warn)}
.session-stack>.session-item.tone-bad{box-shadow:inset 3px 0 0 var(--bad)}
.cal-day .session-stack .session-item.up-next{box-shadow:inset 3px 0 0 var(--accent)}
.cal-day .session-stack .session-item.is-live{box-shadow:inset 3px 0 0 var(--accent);background:var(--surface-2)}

/* ————— 2.9.0: the heading inside the group, larger; journal lines; ————————
   squarer chrome (owner, 2026-08-15: "have the headings of the cards within
   it (like Evolt).. more visible.. large font"; "journal kinda line format..
   still kinda boxy"; "make the wk calendar scroll less round").
   .group-h and its following stack render as ONE object: the heading takes
   the surface's top, the stack completes it. The heading is a size up in the
   display face — visibly the group's NAME, not another label. */
.group-h{
  background:var(--surface);border:1px solid var(--line);border-bottom:0;
  border-radius:12px 12px 0 0;margin-bottom:0;padding:13px 15px 4px;
}
.group-h, .group-h b, .group-h .grow{
  font:700 var(--ds-title)/1.25 var(--sans);letter-spacing:-.02em;color:var(--ink);
  text-transform:none;
}
.group-h .cal-range{font:500 var(--ds-label)/1 var(--mono);letter-spacing:.06em}
.group-h + .session-stack, .group-h ~ .session-stack{
  border-top-left-radius:0;border-top-right-radius:0;border-top:0;margin-top:0;
}
/* Journal lines: the surface stays but the box calms down — flat fill, a
   hairline edge, no gradient, no drop shadow, tighter radius. */
.session-stack{
  background:var(--surface);background-image:none;box-shadow:none;
  border:1px solid var(--line);border-radius:12px;
}
/* In-card group headings everywhere step up to match. */
#pane-progress .pcard-h{font:700 var(--ds-title)/1.25 var(--sys);letter-spacing:-.02em;padding:13px 2px 5px}
.mrows-h{font:700 var(--ds-strong)/1.25 var(--sans);letter-spacing:-.02em;padding:13px 0 5px}
/* The week strip: less capsule, more calendar. */
.wk-d{border-radius:10px;background-image:none}

/* Worth a look, in the app's one row grammar (owner, 2026-08-15: "the format
   is so inconsistent.. clean it up so the text, font is consistent"). Name
   left and underlined like every tappable label; the finding right, in the
   same size; colour carries tone, the arrow carries the direction.
   SCOPED to .rep — the report container — because book.js also has a
   .rep-row, where rep means REPEAT: the weekly-booking stepper. An unscoped
   version of this rule turned that stepper into a report line. */
.rep .rep-row{display:grid;grid-template-columns:1fr auto;align-items:baseline;gap:12px;
  min-height:44px;padding:2px 0;border-bottom:1px solid var(--line)}
.rep .rep-row:last-of-type{border-bottom:0}
.rep .rep-who{background:none;border:0;padding:0;cursor:pointer;text-align:left;
  font:400 var(--ds-body)/1.4 var(--sans);color:var(--ink);letter-spacing:-.01em;
  text-decoration:underline;text-decoration-color:var(--line-strong);
  text-decoration-thickness:1px;text-underline-offset:4px}
.rep .rep-r{font:500 var(--ds-body)/1.4 var(--sans);letter-spacing:-.01em;text-align:right;
  font-variant-numeric:tabular-nums;white-space:nowrap}

/* A history row: the ledger line (count · day · hour · place, status word
   right), then the quiet meta line under it — what the session was (owner:
   "PT 60min etc"), which the ledger line has no room to say. */
.session-item.si-hist{display:block;min-height:44px;padding:7px 14px 7px 17px}
/* The ledger line leads in the app's own voice (owner, 2026-08-16: "the
   font for the session history cards are horrible") — a full line of mono
   read as terminal output, not a row. Archivo like every row-title in the
   app, tabular digits so 01/20 and 19:30 still stack into columns, meta
   staying small mono faint beneath. */
.si-hist .si-time{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font:500 var(--ds-small)/1.35 var(--sans);letter-spacing:0;color:var(--ink);
  font-variant-numeric:tabular-nums}
.si-hist .si-meta{margin-top:2px}


/* Metric/lift detail sheets (coach app) — the portal sheet's grammar. */
.mh-spark{margin:6px 0 10px;color:var(--accent)}
.mh-spark svg{width:100%;height:56px}
.mh-facts{border-bottom:1px solid var(--line-strong);margin-bottom:4px}
.mh-fact{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  padding:9px 2px;border-bottom:1px solid var(--line);font:400 var(--ds-body)/1.4 var(--sans)}
.mh-fact:last-child{border-bottom:0}
.mh-fact span{color:var(--faint)}
.mh-fact b{font-weight:600;font-variant-numeric:tabular-nums}
.mh-fact b.good{color:var(--ok,var(--accent))}
.mh-fact b.watch{color:var(--warn)}
.lift-chart{margin:4px 0 10px}

/* Five columns now — status trims to keep the label breathing on 375px. */
.mrow-st{flex:0 0 64px}
.mrow-v{flex:0 0 76px}
/* In-card heading with its action on the right, same as section-title had. */
.mrows-h.between{display:flex;align-items:center;justify-content:space-between}
.mrows .note{padding:6px 0 12px}

/* No drift, no falling-over (owner): a long Progress line wraps as a clean
   right-aligned second line rather than shoving its label. */
.mh-fact b{text-align:right;min-width:0;overflow-wrap:anywhere}
.mh-fact span{flex:none}

/* THE GOAL BAR: start · current · goal as three ticks on one track, values
   beneath each, the covered span shaded in the judgment's colour. */
.gbar-wrap{margin:8px 0 14px}
.gbar{position:relative;height:26px}
.gbar-track{position:absolute;left:0;right:0;top:11px;height:4px;background:var(--line-strong);border-radius:2px}
.gbar-fill{position:absolute;left:0;top:11px;height:4px;border-radius:2px;background:var(--faint)}
.gbar-fill.good{background:var(--ok,var(--accent))}
.gbar-fill.watch{background:var(--warn)}
.gbar-tick{position:absolute;top:3px;width:2px;height:20px;margin-left:-1px;background:var(--faint);border-radius:1px}
.gbar-tick.cur{background:var(--ink);width:3px}
.gbar-labs{position:relative;display:flex;justify-content:space-between;margin-top:3px;
  font:500 var(--ds-small)/1.3 var(--sans);color:var(--faint);font-variant-numeric:tabular-nums}
.gbar-curlab{position:absolute;transform:translateX(-50%);color:var(--ink);font-weight:600}
.gbar-curlab.good{color:var(--ok,var(--accent))}
.gbar-curlab.watch{color:var(--warn)}

/* The getting-started card's header, borrowed for the measurement group
   cards (owner, 2026-08-15: "borrow that format for the heading for each of
   the measurement summary cards.. the main differen ceis really the
   header") — a true header BAND: small mono caps on a hairline rule that
   runs the full card width, not a bold line floating in the card. The tick
   ring and chevron stay where they were. */
.mrows-h.as-head{margin:0 -13px 2px;padding:12px 14px 10px;border-bottom:1px solid var(--line);
  font:400 var(--ds-label)/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--faint)}

/* The coach-notes peek: tappable tail of the note. The clamp on the prose
   guards the note written as one unbroken paragraph — three source lines can
   still wrap to a dozen visual ones, and the ceiling holds either way. */
.note-peek{display:block;width:100%;text-align:left;padding:0}
.note-peek .prose-block{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
.note-peek-more{display:block;padding-top:6px;font:400 var(--ds-label)/1.5 var(--mono);letter-spacing:.02em;color:var(--faint)}

/* The library shelf as ENTIRE LISTS — all three tabs: a label, then bare
   rows hairline to hairline on the page itself, no surfaces, no arrows.
   Rows indent a step from the label (owner, 2026-08-16: "should the
   exercises themselves indent a bit? from the titele?") so the label reads
   as owning the run beneath it. */
.shelf-flat{margin-bottom:20px}
.shelf-flat .row{}
.shelf-flat .row + .row{}

/* ---- THE PROGRAMME TILE (owner, 2026-08-20: "exercises and nutrition can be
   simple, but programmes should look nice and big") --------------------------

   THE APP'S ONE EARNED FILL, and the kit's own test is what earns it: a surface
   belongs to an OBJECT, not to a list. An exercise is a word and a flat row is
   the right shape for a word. A programme is the coach's method — weeks of
   structure they will put a person through — and as the same row as a single
   movement, the most valuable object in the app looked like the least.

   Sized off the design kit: the name at --ds-title so it reads as a heading
   rather than a list item, the shape line in mono because "8 WEEKS · 2 DAYS/WK"
   is a measure, the pitch in Archivo because it is a sentence (see the two
   voices rule). */
.pg-tiles{display:flex;flex-direction:column;gap:10px;margin-bottom:20px}
.pg-tile{display:block;width:100%;text-align:left;padding:15px 16px 16px;
  background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r);
  box-shadow:inset 0 0 0 1px var(--edge),inset 0 1px 0 var(--hi),var(--shadow)}
.pg-tile:active{transform:scale(.995)}
.pg-tile-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
/* THE FOCUS MARK. What the programme builds, as one glyph — see FOCUS in
   catalogue.js for why this is a mark for a quality rather than a picture of
   the programme. `flex:none` and a baseline nudge so it sits with the first
   line of a name that wraps to two, rather than centring itself against both. */
.pg-focus{flex:none;display:inline-flex;align-items:center;color:var(--muted);
  position:relative;top:2px;margin-right:-4px}
/* THE NAME WRAPS, IT DOES NOT TRUNCATE. On a row, a clipped title is fine —
   there are twenty of them and the eye is scanning. On a card the name is the
   headline, and "Back to it — desk job, no training hi…" is the app hiding the
   half of the sentence that says who it is for. Two lines, then clip. */
/* `flex:1` because the row is space-between and the mark made it three
   children: without it the name floated into the middle of the card, which is
   the one place a headline must never be. The name grows, the count stays
   pinned right, and the mark sits against the left edge where it started. */
.pg-tile-n{flex:1;font:var(--ds-w-bold) var(--ds-title)/1.25 var(--sans);letter-spacing:-.02em;
  color:var(--ink);min-width:0;display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}
/* WHO IS ON IT — the fact a coach-facing shelf needs and a consumer app has no
   use for. Accent, because a programme with clients on it is live work. */
.pg-tile-on{flex:none;font:var(--ds-w-semi) var(--ds-micro)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--accent)}
/* HOW LONG, HOW OFTEN, HOW MANY PHASES — the three facts every programming app
   leads with, and a measure, so mono. */
.pg-tile-shape{margin-top:7px;font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);
  letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
/* Who it is for. A sentence, so Archivo. */
.pg-tile-why{margin-top:9px;font:var(--ds-w-regular) var(--ds-small)/1.45 var(--sans);
  color:var(--muted);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden}
/* The phases, named. Separated by an em dash rather than a dot because they run
   in sequence — Phase 1 THEN Phase 2 — and a dot list reads as unordered. */
.pg-tile-phases{margin-top:11px;padding-top:10px;border-top:1px solid var(--line);
  font:var(--ds-w-medium) var(--ds-micro)/1.4 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--faint)}
/* WHAT IS ACTUALLY IN IT — the one thing that lets a coach recognise a
   programme without opening it. The old row said "14 items", which is a number
   nobody chooses a programme by. */
.pg-tile-peek{margin-top:6px;font:var(--ds-w-regular) var(--ds-small)/1.45 var(--sans);
  color:var(--fainter);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}


/* ==========================================================================
   TODAY (owner mockup, 2026-08-16 — transcribed in DESIGN-today.md)

   THE RULE: NO CARDS. One banner carries a fill because it is an interruption
   that has to interrupt; everything else on this screen sits flat on the page
   ground. Boundaries are hairlines and mono section labels, and a list is
   marked by a LEFT RULE rather than by a box around it. If you find yourself
   adding background/border-radius/box-shadow to a .tb-* rule, re-read the
   spec first — three rebuilds of this screen failed by drifting back to cards.
   ========================================================================== */
/* ══ THE DESIGN KIT ═══════════════════════════════════════════════════════
   Extracted from the mock — see DESIGN-kit.md for the counts behind every
   number. One ladder for the whole app, so a screen cannot drift from its
   neighbours by being sized by hand.

   ON :root, NOT ON .tb (fixed 2026-08-19). They were declared INSIDE the Today
   container, so they resolved on exactly one screen and evaluated to nothing
   everywhere else — and an empty value in a `font:` shorthand makes the whole
   declaration invalid, so the browser drops it and the element silently
   inherits. Every rule outside Today that had been moved onto the ladder was
   therefore not on the ladder at all; it had no size of its own. Found by
   reading --ds-label off document.documentElement and getting "".

   The comment they carried said "these sit alongside --tb-* rather than
   replacing it... nothing changed when these landed" — which was true, and true
   for the wrong reason: nothing changed because outside Today nothing could
   use them. */
:root{
  /* ABOVE THE LADDER'S TOP. The mock's own table stops at 44–58 "display" and
     counts three uses; the app has four distinct scales up here because it
     shows figures the coach reads from a metre away, mid-set, holding a
     dumbbell. Named rather than left as bespoke clamps so a fifth cannot
     appear without someone deciding it should (owner, 2026-08-19: "nothing
     hard coded, everything becomes a design kit rule"). */
  --ds-numeral:clamp(52px,15vw,74px);   /* the running clock */
  --ds-name:clamp(24px,6.6vw,32px);     /* the exercise being done */
  --ds-figure:clamp(20px,5.6vw,25px);   /* weight and reps on the set row */
  --ds-display:clamp(38px,11.2vw,52px);
  --ds-heading:clamp(19px,5.6vw,22px);
  --ds-title:clamp(17.5px,5vw,19.5px);
  --ds-strong:clamp(15px,4.3vw,16px);
  --ds-body:clamp(14.5px,4.1vw,15.5px);
  --ds-small:clamp(13.5px,3.8vw,14.5px);
  /* THE BOTTOM OF THE LADDER CAME UP (owner, 2026-08-26: "the details like
     clients calendar are all a bit small esp when it's a 1 line item.. can we
     make all the text size a bit bigger").
     --ds-label was 9.5-10px and --ds-micro 9-9.5px, and between them they carry
     nearly every mono label in the app — the row keys, the meta lines, the
     status words. Ten point is below where secondary text stays comfortable on
     a phone held at arm's length in a gym, and a one-line row is the worst case
     because there is no second line for the eye to fall back on. Up a point
     each; the top of the ladder is unchanged, so the hierarchy keeps its
     shape rather than everything inflating together. */
  /* UP AGAIN (owner, 2026-08-27: "how can we make font sizes bigger"). The
     August 26th raise took the bottom of the ladder from 9.5 to 10.5 and it
     was still not enough: measured at 375px, three-quarters of the roster and
     of Today were rendering under 12px, and mono — which the design system
     means as the MINORITY voice — was carrying six to seven words in ten.
     Twelve is the floor where a mono label stays comfortable on a phone at
     arm's length; below it the tracking that makes a label a label is the
     same thing that pulls it apart.
     The body sizes came up with them rather than only the bottom, because
     lifting the labels alone flattens the ladder into one size. */
  --ds-label:clamp(11.5px,3.2vw,12px);
  --ds-micro:clamp(11px,3vw,11.5px);
  /* ---- THE SPACING SCALE ----------------------------------------------
     Owner, 2026-08-27: "an 8pt spacing scale, enforced. The type ladder is
     tokenised and spacing is not — margins are written per rule, which is how
     two screens drift apart while both look correct in isolation."

     Right about the cause, and the fix is narrower than it sounds. Measured:
     1,017 px spacing values in this file, and the off-grid ones cluster at 6,
     10, 14 and 18 — an even, optical rhythm chosen for small type, not drift.
     Retrofitting a thousand values onto 8/16/24 would move hundreds of screens
     for a principle rather than for a reader, and would be a large change with
     nothing to show for it.

     What actually causes two screens to disagree is a NEW margin invented at a
     call site. So the scale exists to be reached for, the shared utilities are
     built from it, and the css suite holds those utilities to it. The rest of
     the file is left where a designer put it. */
  --sp-0:0;
  --sp-1:4px;    /* hairline separation inside one object */
  --sp-2:8px;    /* between two lines of the same thing */
  --sp-3:14px;   /* between two things */
  --sp-4:22px;   /* between two groups */
  --sp-5:34px;   /* between two sections */
  --sp-6:52px;   /* between two subjects */
  --ds-w-regular:400;
  --ds-w-medium:500;
  --ds-w-semi:600;
  --ds-w-bold:700;

  /* THE OLD LADDER, NOW ALIASES OF THE KIT — AND ON :root (owner, 2026-08-19:
     "point everything to design kit").
     It lived inside .tb, exactly as the kit rungs did, so it resolved on Today
     and nowhere else. That was survivable only because .rn and .cl each carried
     their own copy — which is the "declared three times" defect TODO.md has
     been listing, and the reason those two screens sat a step and a half small
     while Today was on the ladder. Deleting those copies without moving this
     would have left the client record and the runner with no sizes at all.
     92 declarations still say var(--tb-meta); renaming them is mechanical and
     has no visual consequence, so it can follow. */
  --tb-hero:var(--ds-display);
  --tb-name:var(--ds-title);
  --tb-row:var(--ds-strong);
  --tb-meta:var(--ds-body);
  --tb-body:var(--ds-small);
  --tb-btn:var(--ds-label);
  --tb-label:var(--ds-label);
  --tb-micro:var(--ds-micro);
}

.tb{padding-top:14px}
/* ---- the scale, locked to the mockup's own ratios ----
   MEASURED, NOT GUESSED. Two passes at this were eyeballed and both missed:
   the first transcribed the mockup's literal pixels (too heavy for a 375px
   phone), the second over-corrected until every mono clamp bottomed out.
   These numbers come off a clean crop of the hero, as ratios against the one
   element big enough to measure accurately — the time. Ratios are
   scale-free, so they hold whatever width the mockup was drawn at:

     time : name          2.24        time : prep text     3.61
     time : hero meta     3.42        time : button text   4.06
     time : row title     3.10        time : section label 4.06
     time : button height 0.96        time : row meta      4.33

   The row figures come off a second crop of THE REST OF TODAY at the same
   frame width, measured by string width rather than cap height — 19 glyphs of
   "10:00 · Marcus Chan" across a known span pins the size far more accurately
   than eyeballing an ascender. That correction pulled the row titles down
   (2.79 -> 3.10) and nudged the mono labels up (4.48 -> 4.06): the list is a
   scan, and it had been competing with the hero it sits under.

   So the time is the only free number here and everything else is derived
   from it. Retune the screen by changing --tb-hero alone; the proportions
   cannot drift apart because nothing else is independently set. */
.tb{

/* RETUNED TO THE KIT (2026-08-18). The ladder's own shape was close at the
   ends and wrong in the middle. At 375px, hero resolves to 42px, so:

     name  42/2.24 = 18.75  →  kit says 19   ✓ near enough, left alone
     row   42/3.10 = 13.5   →  kit says 15   ✗ a step and a half small
     meta  42/3.42 = 12.3   →  kit says 14   ✗ 12% small, and it is BODY TEXT
     label 42/4.06 = 10.3   →  kit says 10   ✓
     micro 42/4.33 = 9.7    →  kit says 9.5  ✓

   So "everything feels small" was two divisors, not a hundred rules. --tb-meta
   is the app's body size and it was running 12% under the design; --tb-row is
   what row titles and buttons use. Fixing the two moves the 85 declarations
   that already sit on the ladder — every screen at once, in the right
   direction, from one place. The ~220 hard-coded sizes are untouched and still
   have to be converted; this is the part that could be done safely today.

   Divisors, not literals, so the desktop end of every clamp scales with it. */
  /* The ladder moved to :root — see the kit block. This copy was the reason
     it resolved on Today and nowhere else. */
  --tb-h1:var(--tb-name);
  --tb-btn-h:calc(var(--tb-hero) * 1.046);
}

/* ---- header ---- */
/* THE DATE, IN THE FROZEN BAR. Not a page heading — a heading scrolls away,
   and which day it is has to stay true while the coach reads down the screen.
   The type scale lives on .tb, which is further down the tree, so these two
   carry their own clamps rather than reading tokens they cannot see. */
/* THE DAY, IN ONE MONO LINE. Not a sans heading over a mono sub-line — the
   mockup runs the date and the day's figures together in a single uppercase
   mono run, and that is right: they are three facts of the same kind. It
   rides in the frozen bar so which-day stays true while the page scrolls.
   Sized to fill the width at 375px without wrapping (38 characters is the
   realistic worst case: SUN 16 AUG · 4 SESSIONS · 1 OPEN SLOT). */
/* The day's facts on the left, the one door on the right. Laid out rather than
   left to flow: the owed figure's Open button ran straight into the word before
   it — "HK$3,500 OWEDOpen" — and pushed the line onto a second row in the one
   bar on the screen that must not change height. */
/* 7px was measured at 375px and read as crowded (owner, 2026-08-26: "give the
   font a review on the logo banner.. should there be better spacing"). The
   wordmark is 15px tall in a 36px row set by the avatar beside it, so ~10px of
   that row is already dead space UNDER the word — 7px more put the day line
   nearer the avatar than the brand it belongs to. 12px reads as a gap. */
.tb-bar-day{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  padding-top:12px;font:500 var(--ds-label)/1.4 var(--mono);
  letter-spacing:.03em;text-transform:uppercase;color:var(--muted)}
/* THE BAR CANNOT CHANGE HEIGHT. It is sticky and app.js measures it into
   --top-h every render, so a second line moves every frozen head on the screen
   below it. The type ladder came up on 2026-08-27 and "FRI 28 · 3 SESSIONS · 1
   OPEN" stopped fitting beside the money — it wrapped, and took the layout with
   it. The left side gives way; the money on the right never does, because it is
   the half a coach is looking for. */
.tb-bar-day > span:first-child{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tb-bar-go{flex:none;background:none;border:0;padding:0;cursor:pointer;
  font:var(--ds-w-semi) var(--ds-label)/1.4 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--accent)}
.tb-bar-go.warn{color:var(--bad)}
/* A plain thin ring, not a filled disc: the only circle on the screen, and it
   is a doorway rather than a surface. */
.tb-avatar{flex:none;width:44px;height:44px;border-radius:50%;background:none;
  border:1px solid var(--line-strong);display:flex;align-items:center;justify-content:center;
  font:700 var(--ds-small)/1 var(--sans);color:var(--ink)}

/* ---- the banner: the ONE filled thing on this screen ---- */
.tb-banner{display:flex;align-items:baseline;gap:10px;width:100%;text-align:left;
  padding:14px 15px;border-radius:12px;background:var(--surface-2);color:var(--ink)}
.tb-banner-t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font:600 var(--tb-body)/1.3 var(--sans);color:var(--ink)}
.tb-banner-m{flex:none;font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}
.tb-banner-a{flex:none;display:flex;gap:4px;font:700 var(--tb-label)/1 var(--mono);
  letter-spacing:.09em;text-transform:uppercase;color:var(--ink)}

/* ---- label — rule — value, on one baseline ---- */
/* UP NEXT is the loudest label on the screen and carries the weight to say so
   — 700, not 600, and brighter than the section labels below it. It names the
   one thing the coach is about to walk into; THE REST OF TODAY names a list. */
/* THE ONE FILLED THING ON TODAY (owner, 2026-08-16: "give the up next on
   today a fill of light grey"). It is what the coach is about to walk into and
   the only block they ACT on rather than read, so it gets a surface and
   everything around it stays flat. Used once it is emphasis; a second one
   would make both of them decoration. */
/* Room to breathe on both sides: it is the one raised thing on the screen and
   it should read as sitting ON the page rather than wedged between its
   neighbours. */
/* Measured off the reference: the rule sits about 26px below the card's top
   edge and the buttons the same clear of the bottom. At 18/20 the panel was
   gripping its contents — a raised surface has to hold them, not wrap them. */
/* IT HAS TO LOOK LIFTED, NOT JUST LIGHTER (owner, 2026-08-18: "can we make the
   up next pop more" — "the box, not the words"). It was a flat --surface-2
   rectangle on --bg: two very close darks, no edge, no depth, so the one card
   on the screen that carries the next hour read as a slightly paler patch of
   background. The typography was never the problem.
   Depth from the app's own tokens, the same pair the create button uses: a
   one-pixel highlight along the top edge where light would catch, and --shadow
   under it. No drawn border — this system reserves those (see the note at the
   top of this file). */
.tb-hero{background:var(--raised);border-radius:14px;padding:26px 18px;
  margin:26px 0 28px;
  box-shadow:inset 0 1px 0 var(--hi),var(--shadow)}
.tb-hero .tb-rule{margin-top:0}
.tb-rule{display:flex;align-items:center;gap:12px;margin-top:34px}
.tb-rule-k,.tb-rule-v{flex:none;font:700 var(--tb-label)/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase}
.tb-rule-k{color:var(--muted)}
.tb-rule-v{color:var(--accent)}
.tb-rule-line{flex:1;height:1px;background:var(--line)}

/* ---- the section hero, on the list screens ----------------------------------
   A figure and a line, on the ground. No surface and no border: it sits above a
   list and must not read as the first card in it — Today's hero can be a card
   because it IS the screen, and these are a sentence about what follows. The
   size carries it, which is the point (owner: "heros to highlight what is
   important in each section"). */
.sh-hero{display:block;width:100%;text-align:left;background:none;border:0;padding:0;
  margin:18px 0 22px;cursor:default}
button.sh-hero{cursor:pointer}
.sh-hero-fig{font:700 clamp(30px,8.4vw,40px)/1.05 var(--sans);letter-spacing:-.03em;
  color:var(--ink);font-variant-numeric:tabular-nums;display:flex;align-items:baseline;gap:8px}
.sh-hero-fig.warn{color:var(--warn)}
.sh-hero-fig.acc{color:var(--accent)}
/* The unit rides the figure at label size, so "4 sessions" reads as one object
   and the number is still the thing the eye lands on. */
.sh-hero-u{font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--faint)}
.sh-hero-l{font:400 var(--ds-body)/1.45 var(--sans);color:var(--muted);margin-top:7px;
  text-wrap:pretty}
button.sh-hero:active .sh-hero-l{color:var(--ink)}

/* ---- the hero ---- */
.tb-hero-time{display:flex;align-items:baseline;gap:12px;margin-top:14px}
.tb-hero-n{font:700 var(--tb-hero)/1 var(--sans);letter-spacing:-.035em;color:var(--ink);
  font-variant-numeric:tabular-nums}
.tb-hero-d{font:500 var(--tb-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.tb-hero-name{font:700 var(--tb-name)/1.2 var(--sans);letter-spacing:-.018em;color:var(--ink);margin-top:6px}
/* ONE MARK, ONE PLACE (owner, 2026-08-18: "all status should surface in this
   location (renew, unpaid)"). Everything true about the PERSON — money owed, a
   pack running out, a birthday — is a chip immediately after their name, on the
   hero and on the rows, in the same markup. What is true about the BOOKING
   stays in the meta line beneath.
   Amber is what is wrong or costs money; accent is the good kind of thing to
   notice. Never both tones on one chip. */
.tb-mark{display:inline-flex;align-items:center;gap:5px;vertical-align:middle;
  margin-left:8px;padding:3px 8px 3px 6px;border-radius:var(--r-pill);
  font:700 var(--tb-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  white-space:nowrap}
.tb-mark svg{stroke-width:1.9}
.tb-mark.acc{background:color-mix(in srgb,var(--accent) 14%,transparent);color:var(--accent)}
.tb-mark.warn{background:color-mix(in srgb,var(--warn) 15%,transparent);color:var(--warn)}
/* On a row the name is small, so the chip comes down with it. */
.tb-row-t .tb-mark{margin-left:7px;padding:2px 7px 2px 5px;font-size:calc(var(--tb-label) * .92)}
.tb-hero-meta{font:400 var(--tb-meta)/1.4 var(--sans);color:var(--muted);margin-top:4px}
.tb-none{font:600 var(--tb-row)/1.3 var(--sans);color:var(--faint);margin-top:34px;
  padding-top:18px;border-top:1px solid var(--line)}

/* PREP is fenced by hairlines above and below — the one thing on the screen
   that has to survive being skimmed. */
/* TIGHT. The mockup fences PREP close on both sides — it is one line of text
   between two rules, not a padded band. At 16px top and bottom it read as a
   section; at 11px it reads as what it is, a note clipped to the session. */
.tb-prep{display:flex;align-items:baseline;gap:10px;margin-top:12px;
  padding:calc(var(--tb-hero) * .13) 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.tb-prep-k{flex:none;font:700 var(--tb-label)/1 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--warn)}
.tb-prep-t{flex:1;font:400 var(--tb-body)/1.4 var(--sans);color:var(--ink)}

/* ---- actions ---- */
/* The rule above the buttons comes from PREP when there IS a prep note; when
   there is not, the row grows its own, or the hero runs into its actions with
   no boundary at all. */
.tb-acts{display:flex;gap:10px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line)}
.tb-prep + .tb-acts{margin-top:12px;padding-top:0;border-top:0}
/* A generously rounded RECTANGLE, deliberately not a capsule — the mockup's
   button is square-shouldered enough to read as a slab, not a pill. */
/* 48px, not 56 (owner: "buttons are too thick height wise"). At 56 the mint
   slab read as a panel with a word on it rather than as a control. */
.tb-start{flex:1;height:var(--tb-btn-h);border-radius:11px;background:var(--accent);color:var(--accent-ink);
  font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.006em}
.tb-move{flex:none;min-width:17%;padding:0 4px;height:var(--tb-btn-h);border-radius:11px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);
  font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.006em}

/* ---- section labels ---- */
/* EVERY SECTION IS FENCED. The mockup separates its bands with a hairline
   above the label, not with whitespace alone — without it the screen reads as
   one long column and the labels look like they belong to the rows above
   them rather than the ones below. */
.tb-sect{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-top:34px;margin-bottom:10px;padding-top:22px;border-top:1px solid var(--line);
  font:600 var(--tb-label)/1 var(--mono);letter-spacing:.13em;text-transform:uppercase;color:var(--faint)}

/* ---- lists: a LEFT RULE, never a box ---- */
/* A LIST HAS ALREADY ENDED ITSELF. A section label following one takes no rule
   (owner: "there was no hairline after the rest of today") — the rule exists to
   fence the sections off from the HERO, and a second fence between two lists
   reads as a boundary where there is only a change of subject. */
.tb-list + .tb-sect{border-top:0;padding-top:0;margin-top:26px}
/* Nor does the FIRST section on a screen take a rule: there is nothing above
   it to be fenced off from, and the bar already draws that boundary. */
.tb > .tb-sect:first-child{border-top:0;padding-top:0;margin-top:16px}
/* A section following the hero takes no rule: the panel's own edge has already
   closed that block, and a hairline a few pixels under it fences the same
   boundary twice. */
.tb-hero + .tb-sect{border-top:0;padding-top:0}
.tb-list{border-left:1px solid var(--line);padding-left:calc(var(--tb-hero) * .29)}
.tb-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  padding:calc(var(--tb-hero) * .22) 0;background:none}
.tb-row + .tb-row{border-top:1px solid var(--line)}
.tb-row-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.tb-row-t{font:600 var(--tb-row)/1.25 var(--sans);color:var(--ink)}
.tb-row-t.big{font-size:calc(var(--tb-row) * 1.12);letter-spacing:-.012em}
.tb-row-t .mut{color:var(--muted);font-weight:600}
.tb-row-m{font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.07em;text-transform:uppercase;
  color:var(--faint)}
/* The status word is the one thing in the meta line worth finding, so it is
   the one thing brighter than the rest of it. */
.tb-row-m b{color:var(--ink);font-weight:700}
/* An offer waiting on an open hour is an OPPORTUNITY, not a warning — it takes
   the accent, where UNPAID and RENEW take plain ink. */
/* Somebody wants this hour — worth noticing, but it is a fact about the row
   rather than the thing to do next, so it takes plain ink and lets the accent
   go to the hero. */
.tb-row-m b.acc{color:var(--ink-2)}
/* ACCENT IS SPENT ON ONE THING PER SCREEN (owner, 2026-08-25: "review the use
   of accents/color.. notification from client is very busy when you open the
   page"). Measured before changing anything: TEN accent-green elements on
   Today, five of them this class — OFFER › OPEN › OFFER › OFFER › OPEN, one
   per row, every one of them saying the same thing, which is that the row can
   be tapped. The chevron already says that, and the whole row is a button.
   Ten accents is no accent: the eye has nowhere to land, and the ONE thing a
   coach should do next — start the session in front of them — was competing
   with a disclosure control repeated five times.
   Quiet now, and the arrow does the work it was always doing. */
.tb-row-a{flex:none;display:flex;gap:4px;font:700 var(--tb-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}

/* ---- the bar: labels in mono caps, the + captioned under its circle ---- */
/* Smaller (owner). The icon carries the meaning; the word confirms it. */
/* SMALLER AGAIN (owner, 2026-08-18). These are five words the coach learned in
   their first minute and never reads again — the label exists to teach the
   icon, not to be scanned. Tighter tracking too: at 8px, .08em was pushing
   CALENDAR wider than its own cell. */
/* The five destinations are NAMES, and a name is speech. Both platforms set
   their tab labels in sentence case for the same reason: it is the smallest
   type on the screen and caps take the word-shape away exactly where there is
   least stroke to read. */
.tab-label{font:var(--ds-w-semi) var(--ds-micro)/1 var(--sans);letter-spacing:.01em}
/* The horn sits with the words it labels, not above them — the heading is one
   line and the icon is a qualifier on it, the way GOAL qualifies a metric. */
.tb-sect svg{vertical-align:-2px;margin-right:7px}
/* THE QUIET BLOCK AT THE FOOT (owner, 2026-08-19). Unlogged hours are the one
   section of the list that is not a client waiting on an answer — they are the
   coach's own record going cold. Present, because nothing else in the app will
   ever mention them again once the day scrolls past, and because they are the
   only item here that gets WORSE with time. Quieter, because they are not why
   anybody opens this screen. */
.tb-sect-quiet{color:var(--fainter)}
/* The Progress tab's headline: the window on the left, what happened in it on
   the right. A `.tb-sect` that is also a button when there is something
   unlogged to go and fix — so the right half stays a plain fact until it has an
   action, and only then grows one. Full width and left-aligned like every other
   section header, which is what `button` has to be told. */
/* NO RULE, because it is always the first thing on the tab and the tab row
   above it has already drawn that boundary — the same reason
   `.tb > .tb-sect:first-child` drops it on the five destinations. That
   selector cannot reach here (the parent is the client record, not `.tb`), so
   the headline states it for itself rather than inheriting a fence it is on
   the wrong side of. */
.pg-window{width:100%;text-align:left;background:none;border:0;cursor:default;
  margin-top:14px;padding-top:0}
.pg-window[onclick],button.pg-window{cursor:pointer}
/* The right half is not a heading — it is the sentence the heading is about, so
   it drops the tracking and the caps that the label half carries. */
.pg-window-t{flex:none;display:flex;align-items:baseline;gap:8px;
  font:500 var(--ds-label)/1.3 var(--mono);letter-spacing:.04em;
  text-transform:none;color:var(--muted);min-width:0}
/* Colour is spent on the one part that varies — the same rule the strip kept:
   the label "LAST 6 WEEKS" never changes, so it never warns. */
.pg-window.warn .pg-window-t .t-warn{color:var(--warn)}
.pg-window-a{flex:none;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--accent)}

/* A section heading that is also its own fold (owner: "i can't collapse the
   from clients messages on home"). It keeps the heading's exact type and rule —
   a fold styled like a button would make six headings read as six controls. */
.tb-fold{}
.tb-fold svg{}
/* "3 more waiting" — a row, so it sits in the list it belongs to rather than
   floating under it, and quiet, because it is a count rather than a job. */
.tb-more{justify-content:flex-start}
/* The door out of the preview. Quiet like the rest of the block — after this,
   the ONLY accent text left on Today is the countdown to the next session,
   which is the one fact on the screen that is running out. */
.tb-more .tb-row-m{color:var(--muted)}
/* A section heading that is also its own fold. It has to keep the heading's
   type and rule exactly — a fold that looks like a button would make the six
   headings on this screen read as six controls. */
.tb-fold{width:100%;text-align:left;background:none;border:0;border-top:1px solid var(--line);
  cursor:pointer}
.tb-fold svg{vertical-align:-2px;margin:0 7px 0 0}
/* The FAB is now a wrapper, not the disc: the disc is .tab-fab-circle and the
   caption sits under it, outside the circle, so the + keeps its geometry. */
.tab-fab{width:auto;height:auto;border-radius:0;background:none;border:0;box-shadow:none;
  display:flex;flex-direction:column;align-items:center;gap:4px}
.tab-fab:active{background:none}
.tab-fab-circle{width:54px;height:54px;border-radius:50%;display:flex;
  align-items:center;justify-content:center;color:var(--ink);
  background:var(--glass);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  border:1px solid var(--line-strong);box-shadow:inset 0 1px 0 var(--hi),var(--shadow)}

/* WHEN SOMEBODY IS WAITING, IT LOOKS LIKE IT (owner, 2026-08-26: "the messages
   button should have the number in red.. or a coloured button.. something to
   call some attention to it").
   Filled in the accent rather than red, and that is the palette's decision
   rather than a softening of the request: this app has no red — what used to be
   --bad is the deeper amber, and amber means WRONG here. A client asking for an
   hour is not wrong, it is the thing to do next, and the accent is exactly the
   colour this app reserves for that. Filled it is also the loudest control on
   the screen, which is the point.
   Only in this state. The + keeps the quiet glass circle, because "you could
   make something" should never shout. */
/* AMBER, TO MATCH THE BADGE (owner, 2026-08-26: "why is the messages button
   green but when you move out of today, the alert on today is orange.. should
   be the same.. orange may be better").
   Both, and the badge was the one that was right. I filled this green on the
   argument that a client asking for an hour is not WRONG — true of that one
   row, but the corner opens the whole queue, and the queue is mostly things
   going wrong: hours that ran and were never written up, packs about to
   expire. Amber is what this app says that in, and it is what the badge has
   always said it in. One fact, one colour, whichever screen you are on. */
.tab-fab-circle.has-count{background:var(--bad);border-color:var(--bad);
  color:var(--ink-invert);backdrop-filter:none;-webkit-backdrop-filter:none;
  box-shadow:var(--shadow)}
.tab-fab-circle.has-count svg{color:var(--ink-invert)}


/* ---- notes: birthdays and anything else that is a nudge, not a chase ----
   Sits with the banner, above the hero, because it is time-sensitive in a way
   the rest of the screen is not. No fill and no tone edge — the icon is the
   label, and a birthday dressed as an attention card reads as a problem. */
.tb-notes{margin-top:10px;display:flex;flex-direction:column}
.tb-note{display:flex;align-items:center;gap:10px;padding:calc(var(--tb-hero) * .17) 2px}
.tb-note + .tb-note{border-top:1px solid var(--line)}
.tb-note-i{flex:none;font-size:var(--tb-meta);line-height:1}
.tb-note-b{flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;text-align:left;background:none}
.tb-note-t{font:600 var(--tb-meta)/1.3 var(--sans);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tb-note-m{flex:none;font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}
.tb-note-a{flex:none;display:flex;gap:4px;background:none;
  font:700 var(--tb-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--accent)}

/* THE RUN IS A CARD (owner, 2026-08-20: "when a session programme starts it
   should kind of a card, a pop up box.. that runs"). The kit's rule is that a
   surface belongs to an OBJECT — and a live hour is the most object-like thing
   in the app: it has a start, an end, a clock, and a person attached. Raised on
   the page ground with the tab bar gone, it reads as a session you are IN and
   step out of, not a page you happen to be on. */
.main.rn{background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r);margin:10px 12px calc(14px + env(safe-area-inset-bottom));
  padding:16px 15px 18px;
  box-shadow:inset 0 1px 0 var(--hi),var(--shadow)}
.tabbar-gone{display:none}

/* ---- START HERE: the empty Today, which is a new coach's whole first
   impression (owner, 2026-08-20) -------------------------------------------
   NAMED .tb-onboard, NOT .tb-start — that class is already the hero's START
   SESSION button forty lines up, so the first cut inherited its 44px height,
   its accent fill and its uppercase, and the card collapsed to one line with
   its own text spilling over the Money section. Caught in the browser; nothing
   in the suite would ever have seen it.
   It takes the hero's slot because it IS the hero until there is a session to
   put there. One instruction at a time, in the order the app works — a client
   before a package, a package before a booking — so nobody has to infer the
   sequence from a blank screen and an unlabelled plus. */
.tb-onboard{background:var(--surface);background-image:var(--card-grad);
  border-radius:var(--r);padding:20px 18px 20px;margin-top:14px;
  box-shadow:inset 0 1px 0 var(--hi),var(--shadow)}
.tb-onboard-k{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.12em;
  text-transform:uppercase;color:var(--accent)}
.tb-onboard-t{font:var(--ds-w-bold) var(--ds-name)/1.15 var(--sans);letter-spacing:-.02em;
  color:var(--ink);margin-top:10px}
.tb-onboard-s{font:var(--ds-w-regular) var(--ds-body)/1.5 var(--sans);color:var(--muted);
  margin-top:8px;max-width:34ch}

/* ---- THE LITTLE POP-UP WINDOW (owner, 2026-08-20) -------------------------
   A bottom sheet is a room; this is a note held up in front of you. Centred,
   narrow, and gone in one decision. The scrim centres itself whenever the
   sheet inside it is a pop. */
.scrim:has(.sheet-pop){align-items:center;padding:24px 22px}
.sheet-pop{width:100%;max-width:330px;border-radius:20px;max-height:none;
  box-shadow:var(--shadow-up);animation:rise .18s cubic-bezier(.2,.8,.2,1)}
.sheet-pop .sheet-grip{display:none}
.sheet-pop .sheet-head{padding:14px 16px 0;position:static}
.sheet-pop .sheet-body{padding:2px 16px 16px}

/* Two-second anatomy: a tiny context line, the DAY as the headline, one quiet
   preview line, one big button, two small ways out. Nothing else. */
.st-pop{text-align:center;padding-top:2px}
.st-ctx{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--fainter)}
.st-day-n{font:var(--ds-w-bold) var(--ds-name)/1.1 var(--sans);letter-spacing:-.02em;
  color:var(--ink);margin-top:9px}
.st-day-p{font:var(--ds-w-regular) var(--ds-small)/1.45 var(--sans);color:var(--faint);
  margin-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.st-pop .btn{margin-top:16px}
.st-alts{display:flex;justify-content:center;gap:22px;margin-top:14px}
.st-alts button{background:none;border:0;padding:6px 2px;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--faint)}
.st-alts button:active{color:var(--ink)}

/* ==========================================================================
   THE LIVE SESSION RUNNER (owner mockups, 2026-08-16 — spec in DESIGN-today.md)
   One exercise at a time, the current set the only thing with controls on it,
   and the way forward always in the same place. Same cardless rules as Today;
   the two panels that DO carry a fill (the clock, the training-max note) are
   explaining rather than listing, which is what earns them a surface.
   ========================================================================== */
/* THE SECOND COPY IS GONE (2026-08-19). It redeclared the whole ladder inside
   the runner, so retuning :root left this screen behind — one of the three
   copies TODO.md has been carrying as a standing defect. It now inherits. */
.rn-head{position:sticky;top:0;z-index:20;background:var(--chrome);
  border-bottom:1px solid var(--line-strong);
  padding:calc(env(safe-area-inset-top) + 12px) 16px 12px;
  display:flex;align-items:center;gap:12px}
/* NOTHING IN THIS BAR WRAPS (design reference, 2026-08-18). The name broke
   across two lines, the sub across three, and the running clock — the one
   figure a coach glances at mid-set — came last on the third. The reference
   has one line of name, one of programme, and the clock large on the right.
   Both text lines shrink to fit rather than wrapping; the bar is 64px of a
   working screen and every line it grows costs a set row. */
/* The name gets the bar. With the day chip gone there are three things left —
   back, name, clock — and only the name wants the space. */
.rn-who{flex:1;min-width:0}
.rn-head h1{font:700 var(--ds-figure)/1.15 var(--sans);letter-spacing:-.022em;margin:0;
  color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rn-sub{font:500 var(--ds-label)/1.35 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);margin-top:4px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Mono and tabular so the digits do not shuffle as the minutes tick over. */
.rn-clock{flex:none;font:600 var(--ds-heading)/1 var(--mono);letter-spacing:.01em;
  color:var(--ink);font-variant-numeric:tabular-nums}
/* At the foot now, beside the way forward — square-ish rather than a circle so
   it reads as the pill's companion rather than as a stray icon button. */
.rn-back{flex:none;width:56px;border-radius:var(--r-pill);background:none;
  border:1px solid var(--line-strong);color:var(--muted);
  display:flex;align-items:center;justify-content:center}

/* Progress: how far through the session, as a fraction and as a bar — and now
   the day label too, which used to crowd the client's name out of the top bar.
   The bar takes whatever the label and the fraction leave. */
.rn-prog{display:flex;align-items:center;gap:12px;padding:16px 0 6px}
.rn-prog .rn-day{flex:none;margin:0}
.rn-prog-t{flex:1;height:3px;border-radius:2px;background:var(--line);overflow:hidden}
.rn-prog-f{height:100%;background:var(--ink);transition:width .25s ease}
.rn-prog-n{flex:none;font:500 var(--tb-micro)/1 var(--mono);color:var(--faint)}

.rn-eyebrow{font:700 var(--tb-label)/1 var(--mono);letter-spacing:.13em;
  text-transform:uppercase;color:var(--accent);margin-top:16px;
  font-size:var(--ds-label);letter-spacing:.13em}
/* THE EYEBROW AS A ROW (owner mock): EXERCISE 1 ——————————— SWAP ›. The rule
   between them is what makes the two ends read as one line rather than as two
   labels that happen to share a row. */
.rn-eyebrow-row{display:flex;align-items:center;gap:12px}
.rn-eb-rule{flex:1;height:1px;background:var(--line)}
.rn-eb-go{flex:none;display:flex;align-items:center;gap:4px;background:none;border:0;padding:0;
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--accent);text-decoration:none}
/* ══ THE RUNNER'S OWN TYPE SCALE ══════════════════════════════════════════
   Owner, 2026-08-18: "did I not already ask to match the font sizing, type,
   general scaling as in the mocks I sent. I still see differences."

   Measured, and they were right twice over.

   FIRST, there was no hierarchy where the reference has the most. The client
   name, the exercise name and the set values all rendered at 18.75px — three
   different KINDS of thing at one size, because all three reached for the
   shared --tb-name token. In the reference the exercise name is the largest
   thing on the screen by a clear margin and everything else steps down from
   it: normalised to that name, the client name and the set values sit at 0.8,
   the clock at 0.7, the prescription at 0.53, the mono labels at 0.41.

   SECOND, the whole screen ran about a third small. The reference's exercise
   name is ~6.8% of the viewport width; ours was 5%.

   So the runner gets its own scale rather than borrowing the app's. It is the
   one screen read at arm's length, mid-set, by someone who is also watching a
   person lift — the case for bigger type here is different from anywhere else
   in the app, which is why sharing tokens with the client record was wrong.
   Ratios below are the reference's, held across the clamp. */
.rn-name{font:700 var(--ds-name)/1.12 var(--sans);letter-spacing:-.024em;
  color:var(--ink);margin:9px 0 0}
.rn-pres{font:var(--ds-w-regular) var(--ds-body)/1.4 var(--sans);color:var(--muted);margin-top:5px}

/* Set rows. Done greys out and keeps its tick; the live one carries steppers. */
/* NO RULE ABOVE THE HEADINGS (owner, 2026-08-19). The block opened with a
   hairline and then the SET / WEIGHT / REPS row sat under it, which boxed the
   headings in between two lines and made them read as a row of the table
   rather than as its label. The mock runs the prescription straight into the
   headings; the only rule up here is the one UNDER them, which is what a
   column header is for. */
.rn-sets{margin-top:12px}
/* Three tracks when the movement has no load — see the note in session.js.
   The reps column takes the width the weight column gave up rather than
   leaving a hole where a stepper used to be. */
.rn-sets.unweighted .rn-set,.rn-sets.unweighted .rn-set-h{
  grid-template-columns:26px minmax(0,1fr) 38px}
.rn-sets.ruled{border-top:0;border-left:1px solid var(--line);padding-left:14px;margin-top:14px}
/* THE TICK IS NEVER PUSHED OFF THE SCREEN. On the live row the two stepper
   groups plus the tick came to more than the width of a phone, and because the
   tick is the last flex child it was the one that went — the commit control,
   clipped, on the row the coach is actually working. min-width:0 lets the
   steppers give way instead, and the tick is flex:none so it always lands. */
/* A GRID, NOT FLEX (owner mock, adopted 2026-08-19). Under flex the live row's
   two steppers are far wider than a done row's plain figures, so the reps
   column landed somewhere different on every row — which is why the table never
   looked organised and why no column header could have pointed at anything.
   Four fixed tracks: set, weight, reps, commit. Every row keeps them whatever
   it is holding, so a header row can label them and be telling the truth. */
.rn-set,.rn-wave,.rn-set-h{display:grid;
  /* 26px, not 18: the set NUMBER needs 18, but the word SET above it does not
     fit there and ran straight into WEIGHT — "SETWEIGHT". The track is sized to
     the widest thing in the column, which is the header, and the numbers simply
     sit at the left of it. */
  grid-template-columns:26px minmax(0,1.2fr) minmax(0,1fr) 38px;
  /* 14px, not 8: "SET" fills its whole track, so an 8px gap left the two
     headings reading as one word — SETWEIGHT. The gap is what separates them,
     because neither the size nor the colour does. */
  align-items:center;gap:14px;padding:7px 0;
  min-width:0;border-bottom:1px solid var(--line)}
.rn-set:last-child,.rn-wave:last-child{border-bottom:0}
/* THE HEADER ROW. Tiny mono, the quietest thing in the block — it is read once
   on the first set of the first exercise and never needed again, so it must not
   compete with the figures underneath it. No bottom rule of its own: the header
   and the first set are one object. */
.rn-set-h{padding:0 0 6px;border-bottom:1px solid var(--line)}
.rn-set-h>span{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.12em;
  text-transform:uppercase;color:var(--fainter)}
.rn-set-h>span:nth-child(3){text-align:center}
/* 0.8 of the exercise name — a step down, not a match. These are the numbers
   the coach reads off the row; the name is what the row is ABOUT. */
.rn-set-n{font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);color:var(--faint)}
/* OFF-LADDER ON PURPOSE, and DESIGN-kit.md step 4 asks for the reason in
   writing: the runner is read at arm's length, mid-set, holding a dumbbell.
   The two figures on this row are the only thing on the screen that has to be
   legible from there, so they keep the runner's own scale rather than dropping
   to --ds-heading. Everything AROUND them is on the ladder — headers, set
   numbers, units, the RPE label and the cap — which is what stopped this block
   drifting from the rest of the app. */
.rn-set-v{font:700 var(--ds-figure)/1 var(--sans);color:var(--ink);
  font-variant-numeric:tabular-nums}
.rn-set-v i{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);font-style:normal;margin-left:5px}
.rn-set-r{text-align:center;
  font:700 var(--ds-figure)/1 var(--sans);color:var(--ink);font-variant-numeric:tabular-nums}
.rn-set-load{flex:none;font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}
.rn-set.is-done .rn-set-v,.rn-set.is-done .rn-set-r,
.rn-wave.is-done .rn-set-v,.rn-wave.is-done .rn-set-r{color:var(--faint)}
.rn-set-v.mut,.rn-set-r.mut{color:var(--faint);font-weight:500}

/* The tick is the commit. Empty ring until the set is done, filled after. */
/* ONE COLUMN, EVERY ROW. The ticks on the done rows sat flush right while the
   live row's was pushed 40px inboard by its steppers — three commit controls
   on three different vertical lines, which is exactly the sort of thing that
   makes a screen feel unconsidered. margin-left:auto takes the slack wherever
   it is and puts the tick on the same edge in all of them. */
.rn-tick{width:38px;height:38px;justify-self:end;border-radius:50%;background:none;
  border:1px solid var(--line-strong);display:flex;align-items:center;justify-content:center;
  color:var(--ink)}
.rn-tick.on{background:var(--soft);border-color:transparent;color:var(--ink)}

.rn-step{display:flex;align-items:center;gap:6px;min-width:0}
/* The same unit mark the done rows carry, so the weight column does not change
   its mind about what it is measuring halfway down the table. */
.rn-step-v i{font:var(--ds-w-medium) var(--ds-micro)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);font-style:normal;margin-left:3px}
.rn-step-b{width:32px;height:32px;flex:none;border-radius:9px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);font:600 var(--ds-title)/1 var(--sans)}
/* Shrinks before the buttons do — a number can lose a little width, a target
   the coach has to hit one-handed cannot. */
.rn-step-v{min-width:34px;text-align:center;font:700 var(--ds-figure)/1 var(--sans);
  color:var(--ink);font-variant-numeric:tabular-nums}
/* The figure is a control when it can be typed into. No border and no box: it
   already looks like the thing you would touch to change a number, and putting
   a field around it would be a third rectangle on a row that is mostly thumb
   targets. A generous hit area is what it needs instead. */
.rn-step-v.can-type{cursor:text;padding:6px 2px;margin:-6px 0;border-radius:7px}
.rn-step-v.can-type:active{background:var(--sunken)}
/* Replaces the figure in place, so the row does not jump when it opens. */
.rn-step-in{min-width:56px;width:56px;text-align:center;
  font:700 var(--ds-figure)/1 var(--sans);font-variant-numeric:tabular-nums;
  color:var(--ink);background:var(--sunken);border:1px solid var(--accent);
  border-radius:7px;padding:5px 2px}
/* The spin arrows are a second stepper inside the control that already has
   one, and on a narrow row they steal the digits' width. */
.rn-step-in::-webkit-outer-spin-button,
.rn-step-in::-webkit-inner-spin-button{appearance:none;margin:0}
.rn-step-in[type=number]{-moz-appearance:textfield}
@media (max-width:400px){
  .rn-step-b{width:33px;height:33px}
  .rn-set,.rn-wave{gap:7px}
}

/* THE CLOCK — for carries, cardio and anything measured in time. Carries a
   fill because it is a live instrument, not a list. */
.rn-timer{display:block;width:100%;text-align:center;margin-top:14px;padding:20px 16px;
  border-radius:14px;background:var(--surface-2);color:var(--ink)}
/* BIG. This is the instrument the coach is watching from across the room
   while a carry or a cardio piece runs, not a caption on a card. */
.rn-timer-v{font:700 var(--ds-numeral)/1 var(--mono);letter-spacing:.01em;
  font-variant-numeric:tabular-nums}
.rn-timer-p{font:700 var(--tb-micro)/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint);margin-bottom:10px}
/* Resting reads cooler than working: the accent bar is the "you are on the
   clock" signal, and rest is the half of it the coach can ignore. */
.rn-timer.is-rest .rn-timer-p{color:var(--accent)}
.rn-timer-t{height:3px;border-radius:2px;background:var(--line);overflow:hidden;margin:14px 12px 0}
.rn-timer-f{height:100%;background:var(--accent);transition:width 1s linear}
.rn-timer-c{margin-top:10px;font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--faint)}

/* THE TRAINING-MAX NOTE — the one panel that explains rather than lists. */
.rn-tm{display:flex;align-items:center;gap:12px;margin-top:14px;padding:13px 14px;
  border-radius:12px;background:var(--surface-2)}
.rn-tm-k{flex:none;font:700 var(--tb-micro)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--faint)}
.rn-tm-t{flex:1;font:400 var(--tb-meta)/1.35 var(--sans);color:var(--ink)}
.rn-tm-a{flex:none;display:flex;gap:4px;background:none;
  font:700 var(--tb-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--accent)}

.rn-wave-h{display:flex;align-items:center;gap:12px;margin-top:18px;padding-bottom:8px;
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.rn-wave-h span:nth-child(1){width:14px}
.rn-wave-h span:nth-child(2){width:52px}
.rn-wave-h span:nth-child(3){flex:1}
.rn-wave-h span:nth-child(4){min-width:44px;text-align:right}
.rn-wave-h span:nth-child(5){width:44px}
.rn-wave-p{flex:none;width:52px;font:500 var(--tb-micro)/1 var(--mono);
  letter-spacing:.06em;color:var(--faint)}
.rn-wave-p.on{color:var(--accent);font-weight:700}
.rn-foot{margin-top:12px;font:500 var(--tb-micro)/1.4 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* THE WAY FORWARD. White, and the only white thing in the app — it means "move
   on", which is the one action always available on this screen, and it is
   always in the same place so it can be hit without looking. */
/* 0.56 of the exercise name, per the reference — the primary action reads at
   the same weight as the prescription above it, not as a caption. */
.rn-next{display:block;width:100%;margin-top:24px;padding:17px 16px;border-radius:14px;
  background:var(--ink);color:var(--ink-invert);
  font:700 var(--ds-title)/1 var(--sans);letter-spacing:-.01em}

/* ==========================================================================
   THE CLIENT RECORD (owner mockups, 2026-08-16 — spec in DESIGN-today.md)
   Same cardless rules. The header names the person and their standing; the
   tabs are text with an underline, not a segmented pill, because four of
   them in a pill at phone width leaves no room for the words.
   ========================================================================== */
/* THE THIRD COPY IS GONE (2026-08-19), and with it the bug its own comment
   described: the client record and the calendar both live inside .cl, so
   retuning the ladder at :root moved Today to 15/14 and left this screen at
   13.5/12.3 on the same deploy — same tokens, same rules, a different scope
   quietly overriding them. One ladder now, at :root, pointing at the kit. */
.cl-head{display:flex;align-items:flex-start;gap:12px;padding:2px 0 0}
.cl-head h1{font:700 var(--ds-name)/1.1 var(--sans);letter-spacing:-.025em;margin:0;color:var(--ink)}
.cl-sub{font:500 var(--tb-micro)/1.5 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint);margin-top:6px}
/* A STATE, not a decoration: outlined in the accent while the client is
   active, in the muted line once they are not. */
.cl-state{flex:none;border-radius:var(--r-pill);background:none;
  border:1px solid var(--accent);color:var(--accent);text-transform:uppercase;}
.cl-state.off{border-color:var(--line-strong);color:var(--faint)}

.cl-tabs{display:flex;gap:18px;margin-top:14px;border-bottom:1px solid var(--line)}
/* THE LIBRARY'S THREE SHELVES SPREAD ACROSS THE PAGE (owner, 2026-08-18).
   Exercises / Nutrition / Programmes are the whole screen's structure — not a
   sub-navigation inside something else, the way the client record's four tabs
   are — so they take the full width and share it equally. Left-huddled they
   read as a filter on a list rather than as three separate shelves. */
.lib .cl-tabs{gap:0;justify-content:stretch}
.lib .cl-tabs .cl-tab{flex:1;text-align:center;padding-bottom:12px}

/* A FOLD IS A TAP TARGET (owner: "our collapsed title bars are very skinny,
   fingers may not access them well"). The heading was styled as a label and
   inherited a label's height — around 20px, less than half the 44px a thumb
   needs — while being the ONLY way to open the thing underneath it. */
/* A FOLDABLE HEADING IS A CONTROL, so it keeps the 44px target — but its text
   was sitting at the TOP of that box and dumping the whole 32px underneath,
   which is the "huge gap" under WAITLIST on the calendar. It already asked for
   align-items:center and did not get it: .cl-sect is declared fifty lines below
   with align-items:baseline, same specificity, so the later rule won. Stated
   after it now, where it can actually take effect. */
.cl-fold{min-height:44px;display:flex;align-items:center}
/* Smaller than a body line (owner): four of these sit in a row and they are
   navigation, not content — the underline does the pointing, so the words do
   not also have to be loud. */
.cl-tab{background:none;padding:0 0 10px;position:relative;
  font:600 var(--tb-row)/1 var(--sans);color:var(--faint)}
.cl-tab.on{color:var(--ink);font-weight:700}
.cl-tab.on::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:2px;
  background:var(--accent);border-radius:2px}

/* A strip that names a state and offers the one action that clears it —
   NEEDS LOG, LIMITS. Left rule in the tone, no fill. */
.cl-strip{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  padding:13px 2px 13px 12px;margin-top:16px;background:none;
  border-left:2px solid var(--line-strong);border-bottom:1px solid var(--line)}
/* Leading the bands, it takes the space a band label would have had. */
/* Above the day's heading it needs its own air, or it reads as part of the
   frozen bar it sits under. */
.cl .cl-sect:first-child{margin-top:14px}
.cl .bk-list + .dayline{margin-top:20px;padding-top:16px;border-top:1px solid var(--line)}
.cl-strip.warn{border-left-color:var(--warn)}
.cl-strip-k{flex:none;font:700 var(--tb-label)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--faint)}
.cl-strip.warn .cl-strip-k{color:var(--warn)}
/* The period label on the attendance strip is CHROME, not the warning. Without
   this it inherited the warn colour and an unlogged session lit two things at
   once — the left rule and a word that says the same thing every week. The rule
   carries the state; the amber inside the sentence names what is actually
   wrong. */
.cl-strip.k-plain .cl-strip-k{color:var(--faint)}
/* A STRIP SAYS WHAT IS WRONG, SO IT SAYS ALL OF IT (owner, 2026-08-26: review
   every screen for consistency). Swept the whole app and truncation was the one
   finding that repeated — five pages, and three of them were this class. It
   clipped mid-word on one line, so the app told a coach "Sleep poor — RPE 7
   cap, no loaded flexio…" on the tab where that cap decides what goes on the
   bar. A warning that cannot finish its sentence is worse than no warning: the
   coach has to open something to find out what the app already knew.
   Two lines, then clip — long enough for every real strip in the app, and still
   bounded so a paragraph cannot push the screen around. */
.cl-strip-t{flex:1;min-width:0;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  font:400 var(--tb-meta)/1.3 var(--sans);color:var(--ink)}
/* Areas, in the mono voice the design gives them — they are a short list of
   named things, not a sentence. */
.cl-strip-t.areas{font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.07em;
  text-transform:uppercase}
/* THE ACTION SLOT HOLDING A VALUE. .cl-row-a is accent because it normally says
   LOG › or ADD › — the thing to do. Where the same slot carries a figure (the
   Plan tab's working weight) it is a fact, and a fact does not wear the colour
   reserved for the one action on the screen. */
.cl-row-a.val{color:var(--ink);font-variant-numeric:tabular-nums}
.cl-row-a.val.mut{color:var(--faint)}
.cl-strip-a{flex:none;display:flex;gap:4px;font:700 var(--tb-label)/1 var(--mono);
  letter-spacing:.09em;text-transform:uppercase;color:var(--accent)}

/* Group label, and the left-ruled rows under it — the same anatomy as the
   queue and Today, because it is the same kind of list. */
.cl-sect{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-top:26px;margin-bottom:8px;
  font:600 var(--tb-label)/1 var(--mono);letter-spacing:.13em;text-transform:uppercase;color:var(--faint)}
/* ...except when it folds: see .cl-fold above. A control centres its label in
   its target; a plain heading sits on its baseline. */
.cl-sect.cl-fold{align-items:center}
/* The one control a group label is allowed to carry — "Edit" beside CONTACT.
   It borrows the label's own type so it reads as part of the heading rather
   than as a button parked on top of it; accent, because it is a verb. */
.cl-sect-a{background:none;padding:0;font:inherit;letter-spacing:inherit;
  text-transform:inherit;color:var(--accent)}
.cl-list{border-left:1px solid var(--line);padding-left:calc(var(--tb-hero) * .29)}
/* VERTICAL RHYTHM, NOT DELETION (owner, 2026-08-27, from the measured review:
   "0.35 is crowded"). Clients was at 0.35 ink where a list screen wants 0.15 to
   0.25 — and it got there honestly, every row earning its place with nothing on
   it decorative. Which is exactly why the answer is air rather than cuts: a
   64px floor and real space between the name and the line under it. The coach
   scrolls one more thumb-length and reads twice as fast, and that trade is the
   most reliable premium signal there is. */
.cl-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:none;
  min-height:64px;padding:12px 0;border-bottom:1px solid var(--line)}
.cl-row:last-child{border-bottom:0}
/* The mono key column on Details, and the date column on Sessions. */
.cl-row-k{flex:none;width:76px;font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}
/* STACKED, NOT WRAPPED. "Wed 19 Aug" is wider than 76px, so it broke wherever
   the string allowed and a shorter date did not break at all — row heights
   varied with the spelling of the date. Two spans, one line each, so every row
   in a history is the same height and the weekday reads down its own column.
   Same reasoning as .bk-when on the calendar. */
.cl-row-k2{display:flex;flex-direction:column;gap:1px;line-height:1.25}
.cl-row-k-d{color:var(--fainter)}
/* 4px was a hairline between a name and the sentence explaining it; at the
   sizes both are set at now they read as one clot of text. */
.cl-row-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:7px}
/* ══ THE TWO VOICES ═══════════════════════════════════════════════════════
   Owner, 2026-08-18: "what is it about this presentation that makes everything
   look better? is it the spacing, font, font size? once you know, copy it
   across the app."
   It is not the size. It is that a row says two things in two DIFFERENT
   VOICES, and every axis moves at once:
     the subject   sans, 600, --tb-row, ink,   normal case
     the qualifier mono, 500, --tb-micro (~0.7x), faint, UPPERCASE, +.07em
   Typeface, weight, size, colour and case all change together. That is why the
   name reads instantly without being large — the contrast does the work, so
   the type can stay small and the row stays short.
   The spacing follows from it: the pair sits TIGHT (they are one thought) and
   the rows sit LOOSE (they are separate ones). Proximity groups the row, which
   is what lets the hairline be almost invisible and no box be needed at all.
   Three row families had drifted off it — .row-title/.row-sub with hardcoded
   14.5px and 10.5px, and .bk-name/.bk-meta with hardcoded 15px and 9.5px — so
   the Library, Settings, every sheet and the calendar were each a slightly
   different app. They are declared once here and referenced below. */
.v-subject{font:600 var(--tb-row)/1.3 var(--sans);letter-spacing:-.005em;color:var(--ink)}
.v-qualifier{font:500 var(--tb-micro)/1.35 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}

.cl-row-t{font:600 var(--tb-row)/1.3 var(--sans);color:var(--ink)}
/* SENTENCE CASE, AND A SIZE UP (owner, 2026-08-27: "how can we make font sizes
   bigger, surface what is useful"). This is the line the coach actually reads
   on every client row, every session row and every payment row — "3 left ·
   next Thu 18:00" — and it was set in tracked uppercase micro-mono, the
   hardest-to-read combination the app has: caps remove word shape, the size
   removes stroke detail, and the tracking pulls what is left apart.

   MONO STAYS. Unlike a row SUBTITLE, this really is measures — counts, times,
   fractions — and the design system is right that mono is their voice. It was
   never the face that made it unreadable. Two rules below already had to undo
   the uppercase locally (.pg-unset, .pg-fold), which was the tell. */
.cl-row-m{font:500 var(--tb-label)/1.4 var(--mono);letter-spacing:.02em;
  color:var(--faint)}
.cl-row-a{flex:none;display:flex;gap:4px;background:none;
  font:700 var(--tb-label)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--accent)}
.cl-row-a.mut{color:var(--faint)}
.cl-row-a.warn{color:var(--warn)}
/* A fold header is a section label you can press — same type, plus a cursor
   and the +/− the label itself carries. */
.cl-fold{width:100%;background:none;text-align:left}
/* THE SAME CORNER AS EVERYTHING AROUND IT (owner, 2026-08-26: "why is progress
   report build not consistent? would it help to keep it consistent shape?").
   It was the one 999px pill on a tab of 12px cards, so the page's last element
   read as borrowed from another app. It stays FILLED — it is the outcome the
   whole tab is for — but it stops being a different shape while it says so. */
.cl-book{display:block;width:100%;margin-top:26px;padding:16px;border-radius:12px;
  background:var(--accent);color:var(--accent-ink);
  font:600 var(--ds-strong)/1 var(--sans);letter-spacing:-.006em}
/* ---- Plan tab ---- */
.pl-head{margin-top:18px;align-items:baseline}
.pl-title{font:700 var(--tb-name)/1.2 var(--sans);letter-spacing:-.02em;color:var(--ink);
  text-transform:none}
.pl-bar{height:3px;border-radius:2px;background:var(--line);overflow:hidden;margin-top:2px}
.pl-bar-f{height:100%;background:var(--accent)}
.pl-note{margin-top:10px;font:500 var(--tb-micro)/1.4 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}
.pl-acts{display:flex;gap:10px;margin-top:26px}
.pl-act{flex:1;padding:15px 8px;border-radius:12px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);
  font:700 var(--tb-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase}
/* The strip's day cell counts open time now, so it needs the accent when
   there is some and a dash when there is none. */
.cal-off{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
  margin-top:20px;padding:15px;border-radius:12px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);
  font:700 var(--ds-label)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase}

/* ---- Progress: the headline goal, and whether it is on pace ---- */
/* The headline is a button now — tapping it edits the goal it is showing,
   which is the only place a coach can see what they are editing. */
/* THE GOALS PANEL — the same object as Today's hero (owner, 2026-08-16: "put
   a box around it like the one in TODAY UPCOMING NEXT"), so the two screens
   agree about what a headline looks like. Tokens, not a second set of numbers:
   change the hero and this follows. */
/* THE HERO IS THE ONE PANEL ON THIS TAB (owner, 2026-08-26: "the hero card can
   be more prominent"). It was painted with --surface-2, which is the PRESSED
   tint in light and an elevated one in dark — the same token that made the
   Today hero sink through this whole week — so the one thing this screen is
   built around read as a recess. --raised lifts it in both themes, and the
   ring and shadow every other card in the app wears now apply to the card that
   most needs them. */
.pg-box{background:var(--raised);border-radius:16px;padding:24px 18px 12px;
  border:1px solid var(--edge);box-shadow:var(--shadow);
  margin:var(--cl-gap) 0 26px}
/* Inside the panel the first child sets its own top edge — the lead's outer
   margin would stack on the panel's padding and push the goal off centre. */
.pg-box > .pg-lead{margin-top:0}
/* THE LEFT RULE STAYS (owner, 2026-08-16: "i mean the hairline down
   vertically, so it shows as a heading to waist, deadlift and intake target").
   I had stripped it on the theory that a rule inside a filled panel is a
   second frame — but it is not a frame, it is a BRACKET: it runs down beside
   the three goals and ties them to the OTHER GOALS heading above them, which
   is the one thing a horizontal line under that heading cannot do. A rule
   under a heading separates it from what follows; a rule beside the rows
   claims them. */
.pg-box .cl-sect{border-top:1px solid var(--line);padding-top:16px;margin-top:16px}
/* The pace row already closes itself with a hairline, so the section head
   below it must not draw a second one — two rules with a gap between them
   read as an empty band nobody put there. */
.pg-box .pg-pace + .cl-sect{border-top:0;padding-top:0;margin-top:14px}
/* TAPPABLE, NOT NAVIGATIONAL. These rows change the chart above and stay put,
   so they get the press-feedback of a control rather than the chevron of a
   link. The word on the right is the verb; the whole row is the target. */
.pg-goalrow{cursor:pointer}
/* The bar-and-percent column sets the right edge of this list. A goal with no
   target cannot have a percentage, but its stand-in has to hold the same
   column or the edge goes ragged from row to row — which is the whole of what
   "not aligned" looked like on this list. */
.pg-goalrow .cl-row-a{flex:none;min-width:90px;text-align:right}
.pg-goalrow:active{background:var(--surface-3, rgba(255,255,255,.03))}
.pg-goalrow.is-static .cl-row-a{color:var(--faint)}
.pg-box .cl-row:last-child{border-bottom:0}

/* "Other goals" is a HEADING, in the page's own voice — sans, sentence case
   (mockup). The mono uppercase label is for column heads and kind tags: things
   that name a FIELD. This names a section, and the record's other section
   titles are about to follow it. */
/* The switcher under the hero is a section of its own now that the hero is a
   card — it used to sit inside the panel, so it inherited its padding and read
   as part of it. Given its own air it reads as what it is: the list that
   changes what the card above is showing. */
.pg-sect-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-bottom:4px}
/* text-transform and letter-spacing are inherited from .cl-sect, which is
   mono-uppercase by design. Both have to be unset explicitly or "Other goals"
   comes out as "OTHER GOALS" in a sans face — the worst of both. */
.pg-sect-h h2{font:700 var(--tb-name)/1.15 var(--sans);letter-spacing:-.018em;
  text-transform:none;color:var(--ink)}
.pg-sect-h .cl-row-a{flex:none}
/* A sub-line under the heading, carrying the counts the heading used to
   swallow. */
.pg-sect-s{display:block;margin-top:6px;font:500 var(--tb-micro)/1.4 var(--mono);
  letter-spacing:.07em;text-transform:uppercase;color:var(--faint)}

/* The intake row is the panel's only UNSET thing, and it is drawn as one:
   grey title, its own words in lower case. Everything above it is a number
   somebody has committed to. */
/* A NOTE IS A PARAGRAPH, not a row with a value on the right. The date and its
   reason ride above; the words get the full width, because the half a coach
   would lose to a right-hand column is the half carrying the instruction. */
/* NEEDS ATTENTION: kind, body, action (mockup). The kind column is fixed-width
   so fifteen rows line their labels up — a ragged left edge is exactly what
   made the old inline version unscannable. */
.q-row{align-items:flex-start;gap:12px}
.q-kind{flex:none;width:74px;padding-top:3px;
  font:600 var(--tb-micro)/1.3 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  /* A category label, not a call to action. Amber still means "wrong" below —
     that distinction is the one worth keeping colour for. */
  color:var(--muted)}
/* Amber is "wrong", accent is "a client did something". One glance sorts the
   list without reading a word of it. */
.q-kind.warn{color:var(--warn)}
/* A file that arrived, announced before the sentence is read. */
.q-chip{flex:none;display:grid;place-items:center;width:44px;height:34px;border-radius:6px;
  border:1px solid var(--line);background:var(--surface-2);
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.06em;color:var(--faint)}
.q-row .tb-row-a{padding-top:3px}

.pg-note{align-items:flex-start}
.pg-note-k{display:flex;align-items:baseline;gap:10px;margin-bottom:5px}
.pg-note-d{font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.08em;color:var(--faint);
  font-variant-numeric:tabular-nums}
.pg-note-t{font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted)}
.pg-note-t.ok{color:var(--accent)}
.pg-note-t.warn{color:var(--warn)}
/* Wraps. The row's default is one clipped line, which on a note means the
   coach reads the setup and loses the point. */
.pg-note-x{white-space:normal;line-height:1.45;font-weight:400}

.pg-unset .cl-row-t{color:var(--muted);font-weight:600}

.pg-lead{margin-top:18px;display:block;width:100%;text-align:left;background:none}
/* A goal with no metric or target cannot drive the chart, so it does not
   pretend to: it still opens its own form. */
.cl-row.is-static .cl-row-a{color:var(--faint)}
.pg-lead-k{font:600 var(--tb-label)/1.3 var(--mono);letter-spacing:.11em;
  text-transform:uppercase;color:var(--faint)}
/* The figure and the delta hold the two ends of one line. */
.pg-lead-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:8px}
/* Unit small and on the baseline beside the number, not folded into the same
   string — "80.4 cm" set at one size makes the unit shout as loudly as the
   reading. */
.pg-lead-v i{font:600 calc(var(--tb-hero) * .32)/1 var(--sans);font-style:normal;
  color:var(--faint);margin-left:5px}
.pg-lead-dw{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:3px;text-align:right}
.pg-lead-note{font:500 var(--tb-micro)/1.2 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* Footer and pace share one cell: a value over its label. Three of them across
   is the panel's whole rhythm, and two families of near-identical CSS is how
   they drift apart. */
.pg-cell{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.pg-cell:nth-child(2){text-align:center;align-items:center}
.pg-cell:nth-child(3){text-align:right;align-items:flex-end}
.pg-cell-v{font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);font-variant-numeric:tabular-nums}
.pg-cell-v.acc{color:var(--accent)}
.pg-cell-l{font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);font-variant-numeric:tabular-nums}
.pg-lead-v{font:700 var(--tb-hero)/1 var(--sans);letter-spacing:-.03em;color:var(--ink);
  font-variant-numeric:tabular-nums}
/* Bigger than a caption. It is the second number on the line and the one a
   coach reads after the reading itself — at micro it looked like a footnote to
   the figure rather than its companion. */
.pg-lead-d{font:600 var(--tb-row)/1 var(--mono);letter-spacing:.05em;
  text-transform:uppercase;color:var(--accent);font-variant-numeric:tabular-nums}
/* Inside the instrument's own error it is NOT a finding, so it loses the
   accent — the caveat under it explains why, and a green number over the words
   "within error" is the app arguing with itself. */
.pg-lead-d.flat{color:var(--ink)}
.pg-spark{margin-top:14px}
.pg-spark svg{width:100%;height:64px;display:block}
.pg-trend{overflow:visible}
.pg-lead-f{display:flex;justify-content:space-between;gap:10px;margin-top:14px;
  padding-top:10px;border-top:1px solid var(--line);
  font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}

/* THE PACE ROW. Three figures a coach would otherwise work out on the back of
   a receipt: how far, how fast they need to go, and how long they have. */
.pg-pace{display:flex;gap:10px;margin-top:14px;padding:14px 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.pg-pace-c{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}
/* THE SAME THREE COLUMNS AS THE FOOT CELLS ABOVE (design, 2026-08-18). .pg-cell
   got centre/right alignment on its 2nd and 3rd children and this row was
   simply missed — so "11 wks · TO 01/11" floated in the middle of its column
   instead of sitting flush with the panel's right edge, and the verdict was
   left-aligned under a centred heading. Two rows of three, one grid. */
.pg-pace-c:nth-child(2){text-align:center;align-items:center}
.pg-pace-c:nth-child(3){text-align:right;align-items:flex-end}
.pg-pace-v{font:700 var(--tb-row)/1.1 var(--sans);color:var(--ink)}
/* The verdict is the same SIZE as the figures either side of it — it is the
   middle of three columns, not a headline. Uppercase and a little tracking is
   the whole difference; at --tb-name it wrapped "ON TRACK" onto two lines and
   shoved the label under it out of the row. */
.pg-pace-v.verdict{letter-spacing:.06em;white-space:nowrap}
.pg-pace-v.warn{color:var(--warn)}
.pg-pace-v.ok{color:var(--accent)}
.pg-pace-l{font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* A goal's progress, measured from where they STARTED — not from zero. */
.pg-pct{flex:none;display:flex;align-items:center;gap:8px}
.pg-pct-t{display:block;width:52px;height:3px;border-radius:2px;background:var(--line);overflow:hidden}
/* NEUTRAL WHILE IN PROGRESS (mockup). Four bars all painted accent means the
   accent says "this is a bar", not "this is done" — and the one goal that IS
   done then has no way to look different. Grey until hit; accent is the
   finish line. */
.pg-pct-f{display:block;height:100%;background:var(--muted)}
.pg-pct.hit .pg-pct-f{background:var(--accent)}
.pg-pct-n{font:600 var(--tb-micro)/1 var(--mono);color:var(--faint);min-width:30px;text-align:right}
.pg-pct-n.acc{color:var(--accent);text-transform:uppercase;letter-spacing:.08em}
.note-line{font-weight:400;color:var(--muted)}
.note-line + .note-line{margin-top:4px}
.pl-note.center{text-align:center;margin-top:10px}

/* ---- Progress: the measurements table, flat ---- */
.pg-cols{display:flex;align-items:baseline;gap:10px;margin-top:10px;padding:0 0 8px;
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.pg-cols span:nth-child(1){flex:1}
.pg-cols span:nth-child(2){width:60px;text-align:center}
.pg-cols span:nth-child(3){width:64px;text-align:right}
.pg-cols span:nth-child(4){width:52px;text-align:right}
.pg-row{gap:10px}
/* Name, its GOAL chip, and the provenance line under both. Wraps to two rows
   rather than three columns, so the chip stays beside the name it qualifies. */
.pg-m{flex:1;min-width:0;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
/* A fine underline under the WORD, not the row (mockup) — it marks the name as
   the thing you tap without borrowing the row's own bottom hairline. */
.pg-m-t{font:600 var(--tb-row)/1.25 var(--sans);color:var(--ink);
  border-bottom:1px solid var(--line-strong);padding-bottom:2px}
.pg-m-s{flex-basis:100%;font:500 var(--tb-micro)/1.3 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint);margin-top:5px}
.pg-goal{flex:none;padding:2px 6px;border-radius:4px;border:1px solid var(--line-strong);
  font:600 var(--ds-micro)/1.4 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.pg-t{width:60px;flex:none;display:flex;justify-content:center;opacity:.8}
.pg-n{width:64px;flex:none;text-align:right;font:700 var(--tb-row)/1 var(--sans);
  color:var(--ink);font-variant-numeric:tabular-nums}
.pg-n i{font:500 var(--tb-micro)/1 var(--mono);font-style:normal;color:var(--faint);margin-left:3px}
.pg-c{width:52px;flex:none;text-align:right;font:600 var(--tb-micro)/1 var(--mono);color:var(--faint)}
.pg-c.good{color:var(--accent)}
.pg-c.watch{color:var(--warn)}
/* Sits under the section title now rather than at the foot of the block, so
   the margin closes up to the heading and opens below, toward the columns it
   introduces. */
.pg-more{display:block;width:100%;margin:0 0 4px;padding:2px 0 10px;background:none;text-align:left;
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}

/* THE REST OF THE SCAN, folded. It is a row in the same list on the same left
   rule, because what is behind it is more of that list — a bordered panel
   would make "the other twelve numbers" look like a different kind of thing.
   The names of what is hidden ride on the sub line: a fold that will not say
   what is inside it is a fold nobody opens. */
.pg-fold .cl-row-t{font:600 var(--tb-row)/1.25 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* CONSISTENT SPACING (owner: "when you give too much spacing, it throws it
   off"). Every section on a record breathes the same amount — the sections
   were each tuned in isolation and drifted apart, which reads as sloppiness
   rather than as rhythm. One value, one place. */
.cl{--cl-gap:22px}
.cl .cl-sect{}
.cl .pg-lead{margin-top:var(--cl-gap)}
.cl .cl-strip{margin-top:var(--cl-gap)}
.cl .pg-pace{margin-top:14px;padding:14px 0}
.cl .cl-book{margin-top:var(--cl-gap)}
/* Three verbs where a word used to be one door. Small, outlined, and set in
   the row's right column so they read as the row's own controls. */
.cl-icons{flex:none;display:flex;gap:8px}
.cl-ico{width:38px;height:38px;border-radius:10px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);
  display:flex;align-items:center;justify-content:center}
.cl-ico:active{background:var(--surface-2)}
/* The day a coach is standing in, marked in the week list. */
/* An open hour is an absence: stated, tappable, and never as loud as the work
   around it. On the calendar the booked sessions are the only thing that gets
   to be obvious — everything else is context for them. */
.cl-row.is-open .cl-row-t{font-weight:500;color:var(--muted)}

/* ---- Book: the coach's board, cardless (owner mockup, 2026-08-16) ----
   The time sits in its own left column as a stacked range — a coach reads DOWN
   that column to find a moment — and the client's name is the row's title
   rather than a fragment of one. These rows are the only ink on the board;
   everything else is context for them. */
.bk-nav{display:flex;justify-content:flex-end;gap:8px;margin-bottom:10px}
.bk-nav .icon-btn{width:38px;height:38px;border-radius:10px;
  border:1px solid var(--line-strong);color:var(--ink)}
/* TIGHT (owner, twice: "still doesn't look as tight"). Measured off the
   reference rather than eyeballed: at the mockup's own scale the name sets
   around 14px, the meta around 9, and the time column around 9.5 — every part
   of the row a step smaller than a body line, because a board is SCANNED and a
   dense board fits a day on one screen. The row was built at reading sizes and
   ran about half again too tall. */
.bk-row{display:flex;align-items:center;gap:11px;width:100%;text-align:left;background:none;
  padding:7px 0;border-bottom:1px solid var(--line)}
.bk-row:last-child{border-bottom:0}
.bk-when{flex:none;width:50px;display:flex;flex-direction:column;
  font:500 var(--ds-label)/1.45 var(--mono);color:var(--muted);font-variant-numeric:tabular-nums}
/* A standing ask is a time too, so it sits in the time column — but it is
   words, so it wraps rather than being clipped to a clock's width. */
.bk-when .ask{white-space:normal;text-transform:uppercase;letter-spacing:.05em;
  font-size:var(--ds-micro);line-height:1.25;color:var(--faint)}
.bk-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.bk-name{font:600 var(--tb-row)/1.3 var(--sans);letter-spacing:-.005em;color:var(--ink)}
.bk-meta{font:500 var(--tb-micro)/1.35 var(--mono);letter-spacing:.07em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bk-state{flex:none;font:600 var(--ds-micro)/1 var(--mono);letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint)}
.bk-state.warn{color:var(--warn)}
/* The band label, flat — it was a sticky bar with a count in it. */
.bk-band{border-top:1px solid var(--line);padding-top:12px;margin-top:12px}
/* Picking a start inside an open run: the times as a grid, because they are
   siblings and the coach is choosing one, not reading a list. */
.pick-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:8px}
.pick-t{padding:14px 6px;border-radius:12px;background:none;
  border:1px solid var(--line-strong);color:var(--ink);
  font:600 var(--ds-strong)/1 var(--mono);font-variant-numeric:tabular-nums}
.pick-t:active{background:var(--surface-2)}
/* The board's own section rhythm — tighter than a record's, because a day has
   three bands on one screen and a record has four sections down a scroll. */
.cl .cl-sect + .bk-list{margin-top:0}
.cl .cl-sect{margin-top:13px;margin-bottom:4px}
/* The board's rows sit in one left-ruled list, like every other list. */
.bk-list{border-left:1px solid var(--line);padding-left:14px;margin-top:0}
/* OPEN RECEDES FURTHER (owner: "make open more subtle so it doesn't compete
   with the booked slots"). Not just lighter — SMALLER. Set at the booked
   name's size it still held the same visual weight whatever colour it wore,
   because size is what the eye sorts by first. It drops to the meta line's
   scale: present, tappable, and plainly not the point of the screen. */
/* An open slot is deliberately a step quieter than a booking — it is an
   absence, not an appointment — but a step, not a different scale. */
.bk-row.is-open .bk-name{font:500 var(--tb-meta)/1.3 var(--sans);color:var(--faint)}
.bk-row.is-open .bk-when{color:var(--faint);opacity:.75}
/* A WAITLIST NAME IS NOT A BOOKING (owner: "maybe the names don't need to be
   as big"). It is somebody who would LIKE an hour, set below the people who
   have one — the same rule that quietened the open stretches, applied to the
   band above them. Smaller name, tighter row, so the band costs the day less
   room than the day itself. */
/* THE FIRST WAITLIST ROW WAS CROWDED, the rest were fine (owner, 2026-08-17).
   Every row gets 5px of its own padding, but rows 2..n also sit below a
   hairline with the previous row's 5px above it — about 11px of air — while
   row 1 had only its own 5px between it and the fold heading. It read as
   attached to the heading rather than as the first of a list.
   The gap goes on the LIST, not the row, so the run stays evenly spaced
   inside itself. */
/* UNPAID, beside the name it belongs to. Small and outlined rather than a
   filled chip: it qualifies the person, it is not a second object on the row,
   and the amber says the rest. */
/* QUIETER (owner, 2026-08-18: "does the UNPAID status marker need to be in
   bold? maybe it can be more subtle"). It was a 600-weight word inside a solid
   amber outline — two ways of shouting at once, on a row whose job is the name
   and the time. The colour alone already says it, and the calendar is scanned
   a day at a time: five of these down a Tuesday read as an alarm rather than
   as five facts. Regular weight, a hairline at 45% rather than a full-strength
   border, and the amber dimmed a step. Still unmissable, no longer loud. */
/* .bk-owed IS GONE (2026-08-19). The calendar drew its own "Unpaid" span with
   no icon, no Renew and no Birthday, so a client one session from running out
   looked identical to one with a full pack. It wears clientMarks() now, the
   same builder Today uses.
   The reasoning above SURVIVES the change and is why this rule exists: the
   shared chip is tuned for Today, where a client appears once. A calendar is
   scanned a day at a time and five full-strength chips down a Tuesday read as
   an alarm rather than as five facts. Same shape and same words, one step
   quieter — which is a density decision, not a different vocabulary. */
.bk-row .tb-mark{opacity:.82;font-size:var(--ds-micro)}
.bk-row .tb-mark svg{width:10px;height:10px}
/* WHO ELSE WANTS THIS HOUR — only ever on a held row, so it is never furniture.
   Quieter than the programme line above it: the coach reads WHO and WHEN first,
   then what they are doing, and only then the trade they could make. Amber
   because it exists to be weighed, not to be admired. */
.bk-wait{display:block;margin-top:3px;font:400 var(--ds-label)/1.4 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:color-mix(in srgb,var(--warn) 78%,var(--muted))}
/* THE PROGRAMME, third line. Quieter than the meta above it — a coach scanning
   the day reads WHO and WHEN first, and only drops to this line once they have
   found the row they want. */
.bk-prog{display:block;margin-top:2px;font:500 var(--tb-micro)/1.35 var(--mono);
  letter-spacing:.05em;color:var(--faint);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The name line now carries a chip, so it must lay out rather than just wrap. */
.bk-name{display:flex;align-items:center}

/* BLOCKED TIME, HATCHED (owner: "should appear with a hash or something").
   Diagonal stripes rather than a fill: the row is saying "nothing happens
   here", and any solid treatment makes it read as an appointment with a
   missing name. Low-contrast on purpose — it must be visible without competing
   with the sessions it sits between. */
.bk-row.is-blocked .bk-when,.bk-row.is-blocked .bk-name,.bk-row.is-blocked .bk-meta{color:var(--faint)}
.bk-row.is-blocked .bk-body{position:relative;align-self:stretch;
  background-image:repeating-linear-gradient(135deg,
    var(--line) 0 1px, transparent 1px 7px);
  border-radius:4px;padding:6px 8px}
.bk-row.is-blocked .bk-name{font-weight:600}
/* "asked 12/08" beside the name, quieter than it. Two people wanting the same
   hour is decided by who asked first, and that is the only place the answer
   appears. */
.bk-since{margin-left:8px;font:500 var(--tb-micro)/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* The flag in the runner's header: present on every exercise, quiet until
   needed. A limitation that only appears BEFORE the hour is one the coach has
   stopped seeing by exercise four. */
.rn-flag{flex:none;width:38px;height:38px;border-radius:50%;background:none;
  border:1px solid var(--line-strong);color:var(--faint);
  display:flex;align-items:center;justify-content:center}
.rn-flag:active{color:var(--warn);border-color:var(--warn)}
.rn-limits{display:flex;align-items:center;gap:10px;width:100%;text-align:left;background:none;
  padding:9px 0;border-bottom:1px solid var(--line);border-left:2px solid var(--warn);
  padding-left:12px}
.rn-limits-k{flex:none;font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--warn)}
.rn-limits-t{font:400 var(--tb-meta)/1.35 var(--sans);color:var(--muted)}
/* NOTHING WRAPS, AND IT IS TIGHT (owner, 2026-08-18: "my rule, do not wrap ..
   please make the box look tight and premium"). The note ran full width under
   the area, so a two-clause limitation took three lines and the action buttons
   beside it were pushed onto a fourth. The text truncates now and the row is
   one line high, whatever is in it. */
.rn-inj-acts{flex:none;display:flex;gap:6px}
/* SMALL, because the WORDS are the row. Two full-size buttons reading REVIEWED
   and RESOLVED took two thirds of a phone's width, so the limitation itself —
   the sentence the coach has to act on — was truncated to "ACTIVE · SLE…".
   The controls are the second thing on this row and they are now sized like
   it: the text keeps the width, the taps stay above 30px of height. */
.rn-inj-b{width:36px;height:36px;flex:none;display:flex;align-items:center;justify-content:center;
  padding:0;border-radius:8px;background:none;border:1px solid var(--line);
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);min-height:34px}
.rn-inj-b svg{stroke-width:2}
.rn-inj-b.ok{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
.rn-inj-b.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}

/* THREE ROWS, NOT ONE (owner, 2026-08-18: "you can have 3 rows per issue.. I
   just wanted it structured"). Clamping to a single line was over-reading the
   no-wrap rule: what that rule is against is text breaking a LAYOUT — a name
   pushing a control off a bar, a heading taking three lines from a card. A
   limitation is a sentence a coach has to read and act on, and truncating it
   to "ACTIVE · SLE…" made the row tidy and useless.
   So the note gets up to three lines and stops there, which keeps the rows a
   predictable height without spending the content to get it. The AREA stays on
   one line: it is the subject, and it is short by nature. */
.sheet .cl-row .cl-row-b{min-width:0}
.sheet .cl-row .cl-row-t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sheet .cl-row .cl-row-m{display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:3;line-clamp:3;overflow:hidden;white-space:normal}
/* Top-aligned once a row can be three lines tall — centred controls floating
   beside a paragraph is what makes a list look unconsidered. */
.sheet .cl-row:has(.rn-inj-acts){align-items:flex-start}
.sheet .cl-row .rn-inj-acts{margin-top:1px}

/* The superset partner: a second, quieter way forward. It sits ABOVE the
   primary because the pair is the inner loop and "Next" leaves it. */
/* NO WRAPPED TEXT ON A STATE MARKER (owner, 2026-08-18: "i do not like
   wrapped text, and the DAY A LOWER / ACTIVE markings can be smaller — this is
   also on the client pages").
   A status word is a LABEL: it names a state and it is read in one glance. Two
   lines makes it look like a sentence that ran out of room, and it drags the
   row it sits in taller than every other row on the screen. So they stay on
   one line and shrink to fit, rather than wrapping. */
.cl-state,.rn-day,.pill,.bk-state,.cl-row-a,.tb-row-a{white-space:nowrap}
/* DECLARED TWICE, AND THE SECOND ONE WINS (owner: "why is the ACTIVE font for
   Sarah Wong so big?"). Line ~3675 sets it at --tb-micro inside .cl; this rule
   sets it again — but the client's top bar is OUTSIDE .cl, so it resolves
   :root's copy of the ladder instead, and the two copies had drifted. The same
   three-ladders defect that made the client record's rows land at 13.5 while
   Today's landed at 15.
   A status pill is the smallest kind of label there is, so it is pinned to the
   micro step explicitly rather than inheriting whichever ladder is nearest. */
.cl-state{font:600 var(--ds-micro)/1 var(--mono);letter-spacing:.09em;
  padding:5px 10px}
.rn-day{font-size:calc(var(--tb-micro) * .95)}

/* ══ HOW HARD IT WAS ══════════════════════════════════════════════════════
   Directly under the set table, because it is a question about the set that
   was just ticked and the answer is one tap away from the tick.

   Big targets: this is used mid-set, one-handed, by someone who is also
   watching a person lift. 44px is the smallest thing a thumb finds reliably,
   and the row has five of them plus a label, so the chips take whatever the
   label and the cap leave and never shrink below it. */
/* WRAPS BY ITSELF, WITH NO BREAKPOINT TO GET WRONG. Measured at 375px the row
   needs 383px inside 343: label 81 + circles 124 + cap 158 + gaps. It fits in
   the mock because the mock is set in IBM Plex Mono and we deliberately forked
   to JetBrains, which is wider — DESIGN-kit.md records the fork; this is the
   first place it costs a line.
   So the cap falls to a second line when it cannot fit and sits on the right
   there. What must NOT move is the circles: the owner's note is that they
   follow the label ("SET 1 RPE circle circle", not space space circle), which
   the old max-width:359px rule broke by dropping them to their own row. */
.rn-rpe{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;margin-top:12px;padding-top:11px;
  border-top:1px solid var(--line)}
.rn-rpe-k{flex:none;font:var(--ds-w-medium) var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--faint);white-space:nowrap}
/* THE CIRCLES FOLLOW THE LABEL (owner, 2026-08-19: "in the mock it follows the
   SET 1 REP circle circle.. not SPACE SPACE SPACE circle circle"). This was
   flex:1 with justify-content:center, so the group took every spare pixel and
   floated the circles in the middle of the row — three controls adrift between
   two labels. In the mock they sit against the label that names them, and the
   cap takes the far edge. gap:8px, tighter than the 10px between a stepper and
   its value, because these three are one control, not three. */
.rn-rpe-cs{flex:none;display:flex;gap:8px}
/* SMALLER, PER THE MOCK (owner, 2026-08-19: "i prefer the mock ups, smaller
   icons"). They were 50px — larger than the − and + buttons directly above
   them, which made the optional rating the biggest control on a screen whose
   job is weight, reps and a tick. The mock draws them at roughly the size of a
   stepper button, which is also the relationship that reads correctly: same
   weight of control, lesser importance. Still well clear of the 24px floor the
   2026-08-07 hit-area audit set. */
.rn-rpe-b{width:36px;height:36px;flex:none;border-radius:50%;background:none;
  border:1px solid var(--line-strong);color:var(--muted);
  font:600 var(--ds-title)/1 var(--sans);font-variant-numeric:tabular-nums}
/* The chosen one is a ring, not a fill: the runner's one filled control is the
   tick, and a second solid circle on the same screen competes with it. */
.rn-rpe-b.on{border-color:var(--accent);color:var(--accent);border-width:2px}
/* PAST THE CAP. Recordable — RPE is what the client SAID, and a control that
   refuses to record a 9 makes the record wrong rather than the session easier —
   but never silent. Amber ring on the chip, amber word on the line, so the cap
   the coach wrote is doing something on the screen instead of describing
   something. */
.rn-rpe-b.over{border-color:color-mix(in srgb,var(--warn) 55%,transparent);color:var(--warn)}
.rn-rpe-b.over.on{border-color:var(--warn);color:var(--warn);border-width:2px}
.rn-rpe-cap.over{color:var(--warn);font-weight:var(--ds-w-bold)}
.rn-rpe-cap{flex:none;font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.08em;
  text-transform:uppercase;color:var(--warn);white-space:nowrap;margin-left:auto}
/* The old max-width:359px rule lived here. It dropped the CIRCLES to their own
   line, which is the one thing that had to stay put, and it never fired at 375
   where the overflow actually starts — so on a stock iPhone the cap simply ran
   off the edge and read "CAP 7 — PREP NO". Replaced by the wrap above, which
   needs no breakpoint and moves the right thing. */

/* ══ THE FOOT ═════════════════════════════════════════════════════════════
   Two registers, and they were both full-width slabs stacked on each other:
   a dashed "Add a note" the width of the phone, a "Back to A1" the same, and
   the primary action underneath — three bars of identical weight, so nothing
   read as the main thing.

   The second-order controls get one quiet row of their own, sized to their
   own words. The way back and the way forward share the last row, as a pair,
   which is what the reference does and what a pair of directions wants. */
/* TWO ROWS, NOT ONE (owner, 2026-08-19: "can swap and add a note be on
   separate rows.. give it a bit of space from the exercise itself.. and make
   it look nice").
   Side by side they were two pills competing for one line and, on a narrow
   phone with a long swap label, wrapping into a ragged shape that read as an
   accident. Stacked, each is its own decision.
   LEFT-ALIGNED AND CONTENT-WIDTH, though — deliberately NOT full-width slabs.
   The note at the foot of this file records what that looked like the last
   time: a dashed slab the width of the phone sitting between the sets and the
   way forward, reading like the main event. These are second-order controls
   and their size should say so.
   28px above, against 12 between them: the gap to the exercise is what makes
   them read as a footnote to it rather than part of the set table. */
.rn-tools{display:flex;flex-direction:column;align-items:flex-start;gap:9px;margin-top:28px}
.rn-note{display:inline-flex;align-items:center;justify-content:center;gap:7px;
  padding:9px 15px;border-radius:var(--r-pill);background:none;
  border:1px solid var(--line);color:var(--faint);
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  white-space:nowrap}
.rn-note.on{border-color:var(--line-strong);color:var(--ink)}
.rn-note svg{opacity:.8}
.rn-swap{flex:none;max-width:100%;padding:9px 15px;border-radius:var(--r-pill);
  background:none;border:1px solid var(--line);color:var(--faint);
  font:var(--ds-w-semi) var(--ds-label)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.rn-foot-row{display:flex;align-items:stretch;gap:10px;margin-top:14px}
.rn-foot-row .rn-next{flex:1;margin-top:0}
/* The day chip in the runner header. */
.rn-day{flex:none;padding:5px 10px;border-radius:6px;background:none;
  border:1px solid var(--line-strong);color:var(--muted);
  font:600 var(--tb-micro)/1 var(--mono);letter-spacing:.07em;text-transform:uppercase}

.bk-row.is-wait{padding:5px 0}
.cl-fold + .bk-list{padding-top:8px}
/* On the ladder, not a literal. These two overrode .bk-name's --tb-row with a
   hardcoded 13px, so a waitlist row and a busy row sat two steps under every
   booking beside them on the same list — which is what "the calendar didn't
   move" turned out to be. */
.bk-row.is-wait .bk-name{font:600 var(--tb-row)/1.25 var(--sans);color:var(--muted)}
.bk-state.acc{color:var(--accent)}
/* BOOK IS A VERB (owner: "should BOOK be green too?"). Yes — accent is this
   app's word for the thing to act on, and an open stretch exists to be
   filled. It sat in the same grey as CONFIRMED, which is a statement about
   something already done. */
.bk-r.open .bk-cta,.bk-open-cta,.bk-row .bk-state.book{color:var(--accent)}
.bk-state.mut{color:var(--faint)}
/* A section label you can press. Same type as the labels it sits among — the
   +/− it carries is the only thing saying it is a control. */
.cl-sect.cl-fold{width:100%;background:none;text-align:left}
/* CLEAN AND WELL SPACED (owner, 2026-08-16). A screen of folds needs the fold
   headers to breathe more than the rows inside them do — otherwise a closed
   pack and an open pack's first row sit at the same rhythm and the structure
   stops reading. Open folds keep their rows tight; the gap goes between
   GROUPS. */
.cl-sect.cl-fold{margin-top:20px;padding-top:2px}
.cl-sect.cl-fold + .cl-list{margin-top:2px}
/* A day inside an open plan is a sub-heading, not a section of the page: it
   sits closer to its rows and lighter than the fold above it. */
.cl-sect.cl-sub{margin-top:14px;margin-bottom:4px;color:var(--faint)}
/* The one part of the day line that can be amber: what wants the coach. */
.dayline .cal-range b{color:var(--warn);font-weight:600}
/* The coach's own commitments: present in the day, plainly not sellable. */
.bk-row.is-busy .bk-name{font:600 var(--tb-row)/1.25 var(--sans);color:var(--muted)}
.bk-row.is-busy .bk-when{color:var(--faint)}


/* ---- Library: the shelf takes the app's section grammar ----
   The group header was its own shape — sans on the right where every other
   section on every other screen is mono. A shelf is a list of things under a
   label, which is what .cl-sect names; matching it means the Library reads
   like the record, the board and the queue rather than like a fourth idea. */
/* .grp / .grp-t / .grp-s were declared a SECOND time here, four thousand lines
   below the first set, and being later they won. So the accordion rendered with
   this block's mono uppercase summary while the original block's own comment —
   "the summary is a value, so it is Archivo" — described styling that had not
   applied for as long as both existed. One definition now, up there. */
.shelf-flat{border-left:1px solid var(--line);padding-left:14px;margin:0 0 6px}
.shelf-flat .row{padding:10px 0;background:none}
.shelf-flat .row + .row{border-top:1px solid var(--line)}
.shelf-flat .row-title{font:600 var(--ds-strong)/1.25 var(--sans);color:var(--ink)}
.shelf-flat .row-sub{font:500 var(--ds-micro)/1.35 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);margin-top:2px}
.lib-head .cl-tabs{margin-top:0;margin-bottom:12px}
/* A SHELF IS FOR FINDING, so a row is one line of prose at most. Fifteen
   programmes each carrying a three-line summary is a wall the coach reads
   instead of scans — and the whole summary is one tap away, on the screen
   that exists to show it. */
.shelf-flat .row-why{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;
  overflow:hidden;font:400 var(--ds-small)/1.4 var(--sans);color:var(--muted);margin-top:3px}
.shelf-flat .row-blocks{font:500 var(--ds-micro)/1.35 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint);margin-top:3px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ---- Money: two figures, no panel ----
   A balance beside a flow. They were in a bordered card with a divider down
   the middle — the last card in the app, and the only place two numbers were
   boxed to say they belonged together. Set on the page with a rule between
   them they say the same thing and stop competing with the rows below. */
.pay-two{display:flex;gap:14px;padding:14px 0 16px}
.pay-f{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.pay-f + .pay-f{border-left:1px solid var(--line);padding-left:14px}
.pay-l{font:600 var(--ds-label)/1 var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--faint)}
.pay-n{font:700 var(--ds-name)/1 var(--sans);letter-spacing:-.03em;color:var(--ink);
  font-variant-numeric:tabular-nums}
.pay-n.warn{color:var(--warn)}
.pay-s{font:500 var(--ds-label)/1.35 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}
.pay-month{display:flex;align-items:center;gap:6px}
.pay-month-l{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The right column carrying a FIGURE rather than a verb: it is data, so it
   takes the ink every other figure on the screen takes. */
.cl-row-a.fig{color:var(--ink);font-weight:700}
