/* 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. */
  --bg:#e3e6ea;
  /* 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:#f1f4f6;
  --soft:#d8dde2;
  --ink:#16191c;
  --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:#414951;
  --faint:#5b646d;
  --fainter:#5f6871;
  /* 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:#e0e4e8;
  --line-strong:#b6bec6;
  --btn-bd:#8b939c;
  /* Down from oklch(0.55) — at the old lightness the accent cleared 4.5:1 on
     an almost-white page and lost it the moment the page went grey. */
  --accent:oklch(0.50 0.12 236);
  --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);
  --bad:oklch(0.53 0.20 25);
  /* 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;
  --r-sm:0;
  --maxw:520px;
  --nav-h:76px;
  /* 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;
  /* Depth: one downward shadow for resting surfaces, one upward for the
     sheet. Light needs far less ink than dark to read as lift. */
  --shadow:0 8px 24px rgba(15,20,25,.10);
  --shadow-up:0 -12px 32px rgba(15,20,25,.14);
  /* 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. */
  --card-grad:linear-gradient(180deg,#ffffff 0%,#f6f8fa 100%);
  --hi:rgba(255,255,255,0);
  /* Frosted glass for the floating bar island and the + circle. */
  --glass:rgba(255,255,255,.78);
  --glass-hi:rgba(15,20,25,.07);
  /* The grouping wash behind a banded region (Progress's body-data run):
     present at the top, gone by the bottom. */
  --band-grad:linear-gradient(180deg,rgba(15,20,25,.08),rgba(15,20,25,0));
  /* 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;
  --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. */
  --fainter:#bcc3ca;
  --line:#33373d;
  --line-strong:#3d4248;
  --btn-bd:#4a5057;
  --accent:oklch(0.86 0.19 122);
  --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:linear-gradient(180deg,rgba(255,255,255,.075),rgba(255,255,255,0));
  /* 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. */
  --warn:oklch(0.83 0.15 70);
  --bad:oklch(0.85 0.12 25);
  --scrim:rgba(0,0,0,.55);
}

*,*::before,*::after{box-sizing:border-box}
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:14px;
  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. */
.time-col .t,.stat .n,.big-amount,.kv .v,.mono,.dnum,.mday,.money,.mtile-v{font-variant-numeric:tabular-nums}
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 10.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--faint)}
.eyebrow-sm{font:500 9.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.meta{font:400 10.5px/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. */
.topbar{
  position:sticky;top:0;z-index:20;
  background:var(--chrome);
  border-bottom:1px solid var(--line-strong);
  padding:calc(env(safe-area-inset-top) + 13px) 18px 13px;
  display:flex;align-items:center;justify-content:space-between;gap:12px;
}
/* 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}
/* 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:24px;font-weight:700;line-height:1.15;letter-spacing:-.02em;margin:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.topbar .sub{font:400 10.5px/1.4 var(--mono);letter-spacing:.05em;text-transform:uppercase;color:var(--faint);margin-top:5px}
.topbar .sub-over{margin:0 0 6px}
.main{flex:1;padding:4px 16px calc(var(--nav-h) + env(safe-area-inset-bottom) + 24px)}

/* ---------- 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 15px/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 10.5px/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. */
.cal-nav{display:grid;grid-template-columns:auto 1fr auto;
  border:1px solid var(--line-strong);border-radius:var(--r-s);overflow:hidden;background:var(--surface);margin-bottom:7px}
/* 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. */
.cal-nav > *{padding:14px;color:var(--muted);border:0;background:none;
  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}
.cal-nav .cal-nav-mid{
  border-left:1px solid var(--line-strong);border-right:1px solid var(--line-strong);
  gap:8px;font:500 11px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink);
}
.wk-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:0;border-radius:var(--r-s);overflow:hidden}
.wk-strip.hidden{display:none}
.wk-d{
  border:1px solid var(--line-strong);border-left-width:0;
  background:var(--surface);padding:6px 2px 5px;
  display:flex;flex-direction:column;align-items:center;gap:3px;
}
.wk-d:first-child{border-left-width:1px}
.wk-d-w{font:500 9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--fainter)}
.wk-d-n{font:600 15px/1 var(--mono);letter-spacing:.02em;color:var(--muted);font-variant-numeric:tabular-nums}
/* A 4px square, not a ring: it says "something is on" and nothing more, and at
   this size a filled block survives where an outline turns to mush. */
.wk-d-mk{flex:none;width:4px;height:4px;padding:0;background:var(--accent);margin-top:1px}
.wk-d-mk.blank{background:transparent}
.wk-d.today .wk-d-w,.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. */
.wk-d.hol .wk-d-w,.wk-d.hol .wk-d-n{color:var(--bad)}
.wk-d.hol{position:relative}
.wk-d.hol::after{content:'';position:absolute;top:3px;right:3px;
  width:4px;height:4px;background:var(--bad)}
.wk-d.today.hol .wk-d-w,.wk-d.today.hol .wk-d-n{color:var(--accent)}
.wk-d.on{background:var(--ink);border-color:var(--ink)}
.wk-d.on .wk-d-w{color:var(--soft)}
.wk-d.on .wk-d-n{color:var(--bg)}
.wk-d.on .wk-d-mk{background:var(--bg)}
.wk-d.on.hol::after{background:var(--bg)}
/* :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}
.wk-d.on.off .wk-d-n{color:var(--soft)}

/* 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 10.5px/1.4 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--faint);padding:11px 1px 9px}
.dayline b{font-weight:400;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. */
.list-controls{padding:10px 0 14px;border-bottom:1px solid var(--line-strong);margin-bottom:14px}
.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 13px/1 var(--mono);letter-spacing:.02em;
  font-variant-numeric:tabular-nums;color:var(--ink)}
.row-lab{font:500 9.5px/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. */
.t-ok{color:var(--ok)}
.t-warn{color:var(--warn)}
.t-bad{color:var(--bad)}
.t-off{color:var(--faint)}

/* ---------- 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}
.brand-mark{width:17px;height:17px;background:var(--accent);flex:none}
.brand-word{font:700 16px/1 var(--mono);letter-spacing:.15em;text-transform:uppercase;color:var(--ink)}
.topbar-r{display:flex;align-items:center;gap:9px;flex:none}
/* A statement, not a control: no hover, no pointer, and the muted ink every
   other read-only label in the app uses. */
.chip-mode{
  font:500 9.5px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--faint);border:1px solid var(--line-strong);border-radius:var(--r-pill);
  padding:8px 10px 7px;user-select:none;
}
/* Round, like .avatar — the two marks in the system that stand for a person
   rather than for a box. */
.me{
  width:34px;height:34px;border-radius:50%;flex:none;
  background:var(--soft);color:#fff;
  display:flex;align-items:center;justify-content:center;
  font:700 12px/1 var(--mono);letter-spacing:.02em;
}
/* 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. */
.daystamp{
  font:400 10.5px/1.4 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--faint);padding:10px 2px 12px;
}

/* ---------- 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;
  padding:0 14px calc(10px + env(safe-area-inset-bottom));
  pointer-events:none;
}
.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}
.tab{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:8px 4px 7px;border-radius:var(--r-pill);
  color:var(--faint);font:600 11px/1 var(--sans);letter-spacing:.01em;
}
.tab.active{background:var(--glass-hi);color:var(--ink)}
.tab-ico{position:relative;display:flex}
.tab-fab{
  width:54px;height:54px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;color:var(--ink);
}
/* Icon tile inside the create sheet's rows. */
.create-ico{background:var(--surface-2);color:var(--muted)}
/* 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 9px/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}

/* ---------- 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 1px 0 var(--hi),var(--shadow);
}
.card + .card{margin-top:12px}
.card-pad{padding:14px 16px}
.section-title{
  font:500 10.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint);margin:22px 2px 10px;
}
/* ---------- 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-n{font:500 9.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
  color:var(--accent)}
.blk-t{font:600 15px/1.2 var(--sans);letter-spacing:-.01em;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.blk-note{font:400 11.5px/1.4 var(--sans);color:var(--fainter);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.blk-count{font:400 10px/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 10px/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 11.5px/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;gap:10px;text-align:left;
  padding:15px 2px 13px;border-bottom:1px solid var(--line);
}
.grp-t{font:500 10.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint);flex:none}
/* The summary is a value, so it is Archivo. The label to its left is mono
   because it is a label about a value. */
.grp-s{font:400 12.5px/1.3 var(--sans);color:var(--muted);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. */
.bk-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:1px;
  background:var(--line);border:1px solid var(--line-strong);border-radius:var(--r-s);overflow:hidden;margin-bottom:16px}
.bk-d{background:var(--surface);padding:8px 0 7px;display:flex;flex-direction:column;
  align-items:center;gap:5px}
.bk-d-w{font:500 9px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.bk-d-n{font:700 15px/1 var(--sans);font-variant-numeric:tabular-nums}
.bk-d-c{font:500 9px/1 var(--mono);color:var(--faint)}
/* Same rule as the console strip: fading the whole cell also fades the
   inverted fill under the selected day, so a closed day you tapped went to
   42% of an already-quiet mark. Selected keeps full opacity and says "closed"
   with a dimmer number instead. */
.bk-d.off:not(.on){opacity:.42}
/* A general holiday, in the red every wall calendar in Hong Kong prints them
   in. The date takes the colour and a mark sits above it, so the day is legible
   as a holiday at a glance and still legible as a date. */
.bk-d.hol .bk-d-w,.bk-d.hol .bk-d-n{color:var(--bad)}
.bk-d.hol{position:relative}
.bk-d.hol::after{content:'';position:absolute;top:3px;right:3px;
  width:4px;height:4px;background:var(--bad)}
.bk-d.on{background:var(--ink)}
.bk-d.on .bk-d-w,.bk-d.on .bk-d-n,.bk-d.on .bk-d-c{color:var(--bg)}
.bk-d.on.hol::after{background:var(--bg)}
/* Band headings are rules across the list, not cards — they say where you are
   in the day and then get out of the way. */
.bk-band{display:flex;align-items:baseline;justify-content:space-between;
  padding:16px 2px 7px;
  font:500 9.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.bk-band .dim{letter-spacing:.06em;color:var(--fainter)}
.bk-list{display:flex;flex-direction:column;border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow)}
.bk-r{display:flex;align-items:center;justify-content:space-between;gap:12px;
  width:100%;text-align:left;padding:13px 14px;
  background:var(--surface);border:1px solid var(--line)}
/* Each band is its own closed box: the shared edge collapses between adjacent
   rows, but the first and last row of a group keep theirs, so a band heading
   never lands on an open edge. */
.bk-r + .bk-r{border-top:0}
.bk-t{font:500 13px/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--muted)}
.bk-s{font:500 10px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
.bk-r.open:active{background:var(--surface-2)}
.bk-r.open .bk-s{color:var(--accent)}
/* Booked is the only row that names a person, so it is the only one that gets
   the accent mark and the sans face. */
.bk-r.booked{border-left:2px solid var(--accent)}
.bk-r.booked .bk-t{color:var(--ink)}
.bk-r.booked .bk-s{font:600 13.5px/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}
/* 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 9.5px/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 11px/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 11px/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--muted)}
.step{display:flex;align-items:center;gap:2px}
.step-b{width:22px;height:24px;flex:none;border:1px solid var(--line);border-radius:8px;color:var(--faint);
  font:400 13px/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 12px/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 13px/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 19px/1.2 var(--sans);letter-spacing:-.015em;margin-top:9px}
.hrs-sum-s{font:400 10px/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 14.5px/1.3 var(--sans);letter-spacing:-.005em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Row subtitles are almost always meta — a prescription, a date, a count — so
   they take the mono treatment rather than shrunken prose. */
.row-sub{font:400 10.5px/1.45 var(--mono);letter-spacing:.03em;color:var(--faint);
  margin-top:4px;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 14px/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 9.5px/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. */
.session-item.attn{padding:0;position:relative}
.attn-body{display:block;width:100%;text-align:left;padding:12px 14px 13px}
/* Room for the action, or a long name runs under it. Uppercased because this
   slot holds a clock time on a session card and a phrase here — mixed case is
   the one thing in the mono set that reads as prose rather than as a label. */
.attn-reason{display:block;padding-right:104px;text-transform:uppercase}
/* The reason line wears the tone, not just the 3px edge: on a phone at arm's
   length the amber and pink bars read as "some colour", and the card's first
   words are what actually distinguish a renewal nudge from an unpaid chase. */
.session-item.tone-warn .attn-reason{color:var(--warn)}
.session-item.tone-bad .attn-reason{color:var(--bad)}
.attn-act{
  position:absolute;top:8px;right:10px;
  font:500 9.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink);border:1px solid var(--line-strong);border-radius:var(--r-pill);
  padding:6px 9px 5px;white-space:nowrap;
}
/* The visible chip is 9.5px type in a 22px box — half the 44px a thumb needs,
   and these two chips are the only controls in the app that take money. The
   hit area is grown with a pseudo-element instead of padding so the target
   gets bigger without the chip getting louder. Nothing else lives in this
   corner, so the overlap onto .attn-body costs nothing. */
.attn-act::after{content:'';position:absolute;top:-12px;right:-10px;bottom:-12px;left:-10px}
/* Tone in the text only. A red border round a small chip in the top corner
   turns into a second, louder status light next to the rule on the left. */
.attn.tone-warn .attn-act{color:var(--warn)}
.attn.tone-bad .attn-act{color:var(--bad)}

/* ---------- 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 1px 0 var(--hi),var(--shadow);
}
.stat{padding:12px 14px 14px}
.stat + .stat{border-left:1px solid var(--line)}
.stat .l{font:400 9.5px/1.3 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.stat .n{font-size:26px;font-weight:700;line-height:1.1;letter-spacing:-.02em;margin-top:7px}
.stat .n-warn{color:var(--warn)}

/* ---------- 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 .stat-grid{border:0}
.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. Signed is grey and unsigned is amber: one is a fact you can
   check, the other is a thing to do before the next session. */
.cli-waiver{font:400 10px/1.3 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.cli-waiver.miss{color:var(--warn)}

/* ---------- buttons ----------
   Every button label is a mono CTA in the handoff, uppercase in both themes. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  padding:13px 16px;
  font:700 11.5px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  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-line{background:transparent;border-color:var(--accent);color:var(--accent)}
.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:10.5px}
.btn-lg{padding:15px 18px;font-size:12px}
.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,.link-add{position:relative}
.me::after,.icon-btn::after,.seg button::after,.btn-sm::after,
.pkg-history-toggle::after,.link-add::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%)}
/* A text link acting as a button — "+ Add" beside a section title. 13px of type
   and nothing else, so it needs the widest overhang of any control here. */
.link-add::after{left:-10px;right:-10px}
.link-add{color:var(--accent);font:700 13px/1 var(--sans);letter-spacing:0;text-transform:none}

/* FAB */
.fab{
  position:fixed;z-index:25;right:0;left:0;margin:0 auto;
  max-width:var(--maxw);pointer-events:none;
  bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + 14px);
}
/* the wrapper is a zero-height anchor line; without bottom:0 the button hangs
   below it and disappears behind the tab bar */
.fab-inner{position:absolute;right:16px;bottom:0;pointer-events:auto}
.fab-btn svg{width:24px;height:24px;stroke-width:2.2}

/* ---------- forms ---------- */
.field{margin-bottom:14px}
.field > label{display:block;font:500 9.5px/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint);margin:0 0 7px}
.input,.textarea,.select{
  width:100%;padding:12px 13px;background:var(--surface);
  border:1px solid var(--line-strong);border-radius:var(--r-s);
  font-size:16px;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}
/* 16px keeps iOS from zooming the viewport on focus; the border alone carries
   the focus state now that there are no shadows to glow with. */
.input:focus,.textarea:focus,.select:focus{outline:none;border-color:var(--accent)}
.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: name across the top with its bin, three short numbers under
   it. Three number inputs side by side still clear 90px each at 360px wide,
   which is enough for four digits without the label wrapping. */
.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}
.tpl-nums > .field{margin-bottom:0;min-width:0}
.tpl-nums .input{padding-left:10px;padding-right:6px}
.tpl-nums > .field > label{letter-spacing:.08em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hint{font:400 10px/1.5 var(--mono);letter-spacing:.02em;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 13.5px/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 10.5px/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 10.5px/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 10.5px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase}
.seg button.on{background:var(--ink);border-color:var(--ink);color:var(--bg)}
.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:13.5px}

/* ---------- 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:20px;font-weight:700;margin:0;letter-spacing:-.02em}
.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)}
.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 ---------- */
.empty{
  text-align:center;padding:44px 22px;color:var(--muted);
}
.empty .ico{width:48px;height:48px;border:1px solid var(--line-strong);border-radius:var(--r-s);display:flex;align-items:center;justify-content:center;margin:0 auto 14px;color:var(--faint)}
.empty .ico svg{width:24px;height:24px}
.empty h3{margin:0 0 6px;color:var(--ink);font-size:17px;font-weight:700;letter-spacing:-.02em}
.empty p{margin:0 auto;font-size:13px;max-width:270px;color:var(--muted);line-height:1.5}
.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 10.5px/1.4 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
.kv .v{font-weight:600;text-align:right;font-size:13.5px}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
.mut{color:var(--muted)}
.big-amount{font-size:38px;font-weight:700;letter-spacing:-.03em;line-height:1.05}
.center{text-align:center}
.mt-1{margin-top:8px}.mt-2{margin-top:14px}.mt-3{margin-top:22px}

/* 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}
.self-center{align-self:center}
.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}
/* 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:12.5px}
.note{font-size:13.5px}
.body-sm{font-size:14px}
.mb-1{margin-bottom:8px}.mb-2{margin-bottom:14px}.mb-3{margin-bottom:22px}
.vspace{height:14px}
/* 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:13.5px;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:13px;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:0}
.mt-tight{margin-top:2px}
.mt-snug{margin-top:4px}
.bd-warn{border:1px solid var(--warn)}
/* Secondary line under an item name in list cards (packages, sessions). */
.sub-line{font-size:12.5px;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{
  margin:22px -10px 0;padding:2px 10px 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:14px;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 13.5px/1.2 var(--sans);letter-spacing:-.005em;white-space:nowrap;flex:none;max-width:52%;overflow:hidden;text-overflow:ellipsis}
.bday-meta{font:400 10.5px/1 var(--mono);letter-spacing:.03em;color:var(--faint);white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}
.bday-row .title-action{flex:none;padding:6px 10px}

/* 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. */
.set-ico{width:34px;height:34px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:var(--r-s)}
.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:14px;white-space:pre-wrap}
.preview-block{font-size:13.5px;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}
.list-empty{font-size:13px;padding:10px 2px}
.card-empty{padding:6px 2px}
.pkg-meta{display:flex;justify-content:space-between;margin-top:7px;font-size:12.5px}
.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)}
.shell.view-in{animation:view-in .24s var(--ease)}
@keyframes view-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform: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 10.5px/1.5 var(--mono);letter-spacing:.04em;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:22px;font-weight:700;letter-spacing:-.025em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bcard-role{font:400 10px/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 11px/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:13.5px;line-height:1.6}
.doc h4{
  font:500 9.5px/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 11px/1.55 var(--mono);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 10.5px/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;
}
.toast .x{margin-left:auto;opacity:.7}

.link-list{display:flex;flex-direction:column;gap:0}
.seg-day{display:flex;gap:6px;overflow-x:auto;padding:2px 0 6px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.seg-day::-webkit-scrollbar{display:none}
.day-chip{
  flex-shrink:0;min-width:50px;padding:8px 6px;text-align:center;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-s);
}
.day-chip .dow{font:500 9px/1 var(--mono);letter-spacing:.1em;color:var(--faint);text-transform:uppercase}
.day-chip .dnum{font-size:18px;font-weight:700;margin-top:5px;letter-spacing:-.02em}
.day-chip.on{background:var(--ink);border-color:var(--ink);color:var(--bg)}
.day-chip.on .dow{color:var(--bg);opacity:.7}
.day-chip .mk{width:4px;height:4px;background:var(--accent);margin:4px auto 0}
.day-chip.on .mk{background:var(--bg)}
.day-chip.today .dnum{color:var(--accent)}
.day-chip.on.today .dnum{color:var(--bg)}

/* ---------- 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 10.5px/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 9px/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:13px;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}
.mday.on{background:var(--ink);color:var(--bg)}
.mday.on.today{color:var(--bg)}
.mday .mdot{
  width:4px;height:4px;background:var(--accent);
  position:absolute;bottom:4px;left:50%;margin-left:-2px;
}
.mday.on .mdot{background:var(--bg)}
.mday.hol{color:var(--bad)}
.mday.hol.on{color:var(--bg)}
.mday.hol::after{
  content:'';position:absolute;top:4px;right:4px;
  width:4px;height:4px;background:var(--bad);
}
.mday.hol.on::after{background:var(--bg)}

/* 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 10px/1.3 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  margin-bottom:12px;
}
.notice-hol{color:var(--bad)}
.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)}
.tint-ok{background:var(--ok-bg);border:1px solid var(--ok)}

/* 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 9.5px/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}

/* mobile month toggle */
.month-toggle{
  display:flex;align-items:center;gap:6px;
  font:500 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  padding:4px 2px 12px;color:var(--ink);
}
.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}
.seg-day.hidden{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 10px/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 10px/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 9.5px/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:17px;font-weight:700;line-height:1.1;letter-spacing:-.02em;white-space:nowrap;font-variant-numeric:tabular-nums}
.mtile-v small{font-size:10.5px;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 10.5px/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 9.5px/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. */
.mrows{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
/* 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. */
.mrow:last-child:nth-child(odd){grid-column:1/-1;padding-right:calc(50% + 12px)}
/* 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. */
.mrow{background:var(--surface);background-image:var(--card-grad);padding:9px 13px 10px;min-width:0;
  border-radius:var(--r-s);box-shadow:inset 0 1px 0 var(--hi),var(--shadow);
  display:grid;grid-template-columns:1fr auto;row-gap:4px;column-gap:8px;align-items:baseline}
.mrow-k{grid-column:1/-1;font:400 9.5px/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mrow-v{font:700 15px/1.1 var(--sans);letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.mrow-v small{font-size:10px;font-weight:500;color:var(--faint);margin-left:2px;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 10px/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)}
.meas-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin-top:11px;padding:0 2px;font:400 9.5px/1.3 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--faint)}

/* ---------- 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 11px/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 */
.setrow{display:grid;grid-template-columns:22px 1fr 1fr 1fr 34px;gap:7px;align-items:center;margin-bottom:8px}
.setrow .input{padding:10px 9px;text-align:center}
.setn{font:500 11px/1 var(--mono);color:var(--faint);text-align:center}
.setrow-head{margin-bottom:6px}
.setrow-head span{font:500 9px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);text-align:center}

/* ---------- 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 9.5px/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 11px/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 9.5px/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 10.5px/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)}
.pl-t{color:var(--ink)}
.pl-b{color:var(--muted)}
.ctl-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}
.ctl-row > span{font:500 9.5px/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}
.disclose{font:400 9.5px/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 15px/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 9.5px/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 18px/1.35 var(--sans);letter-spacing:-.01em;text-wrap:pretty}
.report h4{margin:0 0 9px;font:500 9.5px/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint)}
.report p{margin:0;white-space:pre-wrap;font-size:13.5px;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 9.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.rstat-v{font:700 21px/1.1 var(--sans);letter-spacing:-.02em;margin-top:9px;font-variant-numeric:tabular-nums;white-space:nowrap}
.rstat-d{font:500 10.5px/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 11px/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 13px/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 11.5px/1.5 var(--mono);color:var(--muted)}
.report-goals{margin:0;padding-left:18px;font-size:13px}
.report-goals li{margin-bottom:4px}
.report-goals li.done{color:var(--faint)}
.report-foot{padding:12px 16px;border-top:1px solid var(--line);
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  font:400 9.5px/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}
  .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;gap:10px}
/* 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 3px 0 0 var(--ok),inset 0 1px 0 var(--hi),var(--shadow);
  padding:12px 14px 13px 17px;
}
.session-item.tone-warn{box-shadow:inset 3px 0 0 var(--warn),inset 0 1px 0 var(--hi),var(--shadow)}
.session-item.tone-bad{box-shadow:inset 3px 0 0 var(--bad),inset 0 1px 0 var(--hi),var(--shadow)}
.session-item.tone-done,.session-item.tone-off{box-shadow:inset 3px 0 0 var(--line-strong),inset 0 1px 0 var(--hi),var(--shadow)}
/* 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}
/* 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 12px/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:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--fainter)}
.si-flag{font:500 9.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--ok);flex:none}
.session-item.tone-warn .si-flag{color:var(--warn)}
.session-item.tone-bad .si-flag{color:var(--bad)}
.session-item.tone-done .si-flag,.session-item.tone-off .si-flag{color:var(--faint)}
/* 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 16px/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 9.5px/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)}
