/* ============================================================
   Hourroll — stylesheet.

   MOBILE FIRST. Every rule outside a media query describes the
   phone. The one breakpoint, `@media (min-width: 861px)`, adds
   the desktop back on top: the static sidebar, the real data
   tables, the denser controls. Nothing below that line is an
   override of a desktop value, because there is no desktop
   value to override — the phone is the default and the desktop
   is the enhancement. Adding a component means giving it a
   phone size first; if you never touch the 861px block, it
   still works on a laptop, just a little roomier.

   Colour and typeface are the HOURROLL BRAND SYSTEM v1 — step 4
   of the rollout, 14 August 2026, and the last surface to move.
   Indigo over warm paper, Newsreader for display, Plus Jakarta
   Sans for UI, JetBrains Mono for anything counted. The spec is
   ../../../brand/2026-08-14_Hourroll_Brand-System_v1.md and the
   values below are copied from brand/tokens/hourroll-tokens.css.
   NOTHING HERE INVENTS A COLOUR. Change it there and copy it
   down; a hex that appears in this file and nowhere in the token
   package is a bug.

   The BD personal palette and typefaces it replaced are gone,
   and so are their five font files, the same way the monogram
   images went in step 3. None of those names is written out
   anywhere in this file, not even here: QA/tests/test_brand.py
   greps this stylesheet for them literally, so a comment
   mentioning one is indistinguishable from a regression. The
   client portal never shared this file: it loads portal.css and
   nothing else, so that the one page a stranger sees could not
   be broken by this repaint. Keep it that way.

   THE ONE RULE THAT IS EASY TO BREAK: dark mode below is an INK
   GROUND, and on ink EVERY colour swaps. --brand becomes
   brand-lift, --alert/--warn become signal-bright, the primary
   button INVERTS to an on-ink fill with an ink label, and
   elevation is a 1px line rather than a shadow. The light-surface
   indigo on ink measures 1.9:1 and disappears.

   AMBER HAS ONE JOB: signal. Expiry, a used-up package, an
   error. It is never decoration and never a second button. There
   is no red anywhere in this file — errors are carried by the
   signal colour plus a border, an icon and the sentence itself.

   Contrast was measured, not eyeballed: 40 gated pairs across
   both grounds, 0 failures, in the step 4 build report. One
   departure from the spec — --ink-field-line #6B6483 measures
   2.57:1 as a control border on a raised ink card and fails the
   3:1 boundary rule, so dark form controls take --on-ink-muted
   (6.66:1) instead. --border-control is the ONLY border allowed
   on a form control, on either ground.

   Touch: every interactive element is at least 44x44 on the
   phone, with at least 8px between adjacent ones. That is the
   floor, not the target, and it is why the desktop block makes
   things SMALLER rather than the phone block making them bigger.
   ============================================================ */

/* ---- Typefaces (OFL; the licences travel with the files in brand/fonts/) ----
   Three faces, 106,748 B of a 163,840 B per-surface ceiling. Newsreader is the
   VARIABLE 400-700 file, not the portal's static 600: this surface has a dark
   mode, and the dark-surface rule sets display at 600 on paper and 650 on ink.
   No italic — the italic sets one word on the marketing site and nothing here.
   Every one of these is byte-identical to brand/fonts/; QA/tests/test_brand.py
   asserts it, because fontTools stamps head.modified and a casual rebuild
   silently stops matching what is deployed. */
@font-face { font-family:'Newsreader';        src:url('../fonts/newsreader-var.woff2')      format('woff2'); font-weight:400 700; font-style:normal; font-display:swap; }
@font-face { font-family:'Plus Jakarta Sans'; src:url('../fonts/jakarta-var.woff2')         format('woff2'); font-weight:400 700; font-style:normal; font-display:swap; }
@font-face { font-family:'JetBrains Mono';    src:url('../fonts/jetbrains-mono-500.woff2')  format('woff2'); font-weight:500;     font-style:normal; font-display:swap; }

:root {
  --font-sans: 'Plus Jakarta Sans', -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Newsreader', Georgia, 'Times New Roman', serif;
  /* Counted or copied, never emphasis: session numbers, eyebrows, table
     headers, the data-labels on a stacked row. */
  --font-mono: 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;

  /* rgba of --ink. On paper a shadow is legitimate elevation; the dark block
     sets all four to none, because on ink a shadow reads as a smudge and
     elevation is a 1px line instead. */
  --shadow-xs: 0 1px 2px rgba(34,30,51,0.06);
  --shadow-sm: 0 1px 3px rgba(34,30,51,0.08), 0 1px 2px rgba(34,30,51,0.04);
  --shadow-md: 0 4px 12px rgba(34,30,51,0.08);
  --shadow-lg: 0 12px 32px rgba(34,30,51,0.12);
  --shadow-up: 0 -1px 3px rgba(34,30,51,0.05);
  --scrim: rgba(34,30,51,0.5);

  /* The height of the bottom tab bar, excluding the safe area. Referenced by
     everything that has to sit clear of it — the sticky save bar, the page
     bottom padding, the slide-up dialog. Change it here only. */
  --tabbar-h: 58px;

  /* ---- Light: a warm paper ground -------------------------------------- */
  --pg:#FAF8F4;               /* paper */
  --sidebar:#FFFFFF;          /* surface */
  --card:#FFFFFF;             /* surface */
  --section:#E7E3DA;          /* line, used as the one neutral fill: hover, seg
                                 track, flash strip, a muted pill */
  --wash:#EDEBFB;             /* brand-soft — the only indigo tint */
  --border:#E7E3DA;           /* line */
  --border2:#D8D2C6;          /* line-mid — rules that carry weight */
  --border-control:#938B7A;   /* line-strong: the ONLY border on a form control,
                                 3.38:1 on surface. Nothing else clears 3:1. */
  --border-brand-soft:#4338CA;/* brand — the outline on a selected thing */
  --alert-border:#A34A06; --warn-border:#A34A06;   /* signal */
  --strong:#221E33; --body:#221E33; --muted:#57536B;
  --brand:#4338CA; --brand-dark:#372DA6;  /* brand, brand-press (hover only) */
  --bright:#4338CA;                       /* the focus ring is the brand colour */
  --alert:#A34A06;  --alertbg:#FFF3DF;    /* signal, signal-soft. There is no red. */
  --warn:#A34A06;   --warnbg:#FFF3DF;
  --focus-ring: rgba(67,56,202,0.30);
  --btn-pri-bg:#4338CA; --on-brand:#FAF8F4;   /* 7.45:1 */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2357536B' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

/* ---- Ink. EVERY colour swaps; none carries over. --------------------------
   brand -> brand-lift, signal -> signal-bright, the primary button inverts to
   an on-ink fill with an ink label, and every shadow becomes nothing because
   elevation on ink is the 1px --border hairline. */
body.dark-mode {
  --pg:#221E33;               /* ink */
  --sidebar:#2C2740; --card:#2C2740;      /* ink-raised: 1.13:1 against the page
                                 on its own, which is why the hairline below is
                                 not optional */
  --section:#3B3552;          /* ink-line, as the neutral fill */
  --wash:#3B3552;             /* there is no brand tint on ink: the fill is
                                 neutral and brand-lift carries the hue */
  --border:#3B3552;           /* ink-line — THE hairline */
  --border2:#6B6483;          /* ink-field-line, for rules that carry weight */
  --border-control:#B4AEC4;   /* on-ink-muted, 6.66:1. NOT the spec's
                                 --ink-field-line, which measures 2.57:1 here
                                 and fails the 3:1 control-boundary rule. */
  --border-brand-soft:#A9A2F7;
  --alert-border:#F59E0B; --warn-border:#F59E0B;
  --strong:#F6F4F0; --body:#F6F4F0; --muted:#B4AEC4;
  --brand:#A9A2F7; --brand-dark:#A9A2F7;
  --bright:#A9A2F7;
  --alert:#F59E0B; --alertbg:#3B3552;
  --warn:#F59E0B;  --warnbg:#3B3552;
  --focus-ring: rgba(169,162,247,0.35);
  --btn-pri-bg:#F6F4F0; --on-brand:#221E33;   /* the inversion, 14.69:1 */
  --shadow-xs:none; --shadow-sm:none; --shadow-md:none; --shadow-lg:none;
  --shadow-up:none;
  --scrim: rgba(34,30,51,0.72);   /* the same ink, more of it, over a dark page */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23B4AEC4' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

/* ---- Reset + base ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font-sans);
  background: var(--pg);
  color: var(--body);
  -webkit-font-smoothing: antialiased;
  /* A phone scrolls a list to its end and then drags the page itself, which on
     a logging screen reads as the app coming apart. Contain the bounce. */
  overscroll-behavior-y: contain;
}
a { color: var(--brand); text-decoration: none; font-weight: 500; }
a:hover { text-decoration: underline; }
img, svg, video, canvas { max-width: 100%; }
/* Newsreader, and --strong rather than --brand: emphasis inside a heading is a
   weight change, not a colour change. Serif never sets a sentence here — it is
   headings and the wordmark and nothing longer. */
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; color: var(--strong); letter-spacing: -0.018em; }
/* 600 on paper, 650 on ink: the serif loses a little weight to the darker
   ground. This is the whole reason the app ships the variable file. */
body.dark-mode h1, body.dark-mode h2, body.dark-mode h3 { font-weight: 650; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 8px; border: 2px solid transparent; background-clip: content-box; }

/* ---- Skip link (a11y) ---- */
.skip-link { position: absolute; left: -9999px; top: 0; background: var(--brand); color: var(--on-brand); padding: 0.75rem 1rem; border-radius: 0 0 9px 0; font-weight: 600; z-index: 1400; min-height: 44px; display: inline-flex; align-items: center; }
.skip-link:focus { left: 0; }

/* ---- Sidebar nav (desktop; on a phone the tab bar below replaces it) ---- */
.navlink { display:flex; align-items:center; gap:11px; min-height:44px; padding:9px 11px; border-radius:9px; font-size:15px; font-weight:500; cursor:pointer; color:var(--body); text-decoration:none; border:1px solid transparent; transition:background .12s,color .12s; }
.navlink:hover { background:var(--section); color:var(--strong); text-decoration:none; }
.navlink.active { background:var(--wash); color:var(--brand); border-color:var(--border-brand-soft); font-weight:600; }
.navlink.active svg { color:var(--brand); }

/* ---- Bottom tab bar — the phone's primary navigation ----
   Three destinations, four for an admin, which is inside the five a thumb can
   hold. It replaces the drawer rather than sitting beside it: two navigations
   for the same three links is how a phone screen fills up with chrome. The
   sidebar still exists and takes over above the breakpoint. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
  display: flex; align-items: stretch;
  background: var(--card); border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
  box-shadow: var(--shadow-up);
}
.tabbar a {
  flex: 1 1 0; min-width: 0; min-height: var(--tabbar-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  padding: 7px 4px; text-decoration: none;
  font-size: 11px; font-weight: 600; letter-spacing: .01em;
  color: var(--muted); position: relative;
}
.tabbar a:hover { text-decoration: none; }
.tabbar a.active { color: var(--brand); }
/* The active marker is a rule at the top edge, not a colour change alone —
   colour is never the only signal. */
.tabbar a.active::before { content:""; position:absolute; top:0; left:22%; right:22%; height:2px; border-radius:0 0 2px 2px; background:var(--brand); }
.tabbar a svg { flex: none; }
.tabbar .tab-label { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tabbar .tab-badge { position:absolute; top:6px; left:50%; margin-left:4px; min-width:16px; height:16px; padding:0 4px; border-radius:999px; background:var(--warn); color:var(--warnbg); font-size:10px; font-weight:700; line-height:16px; text-align:center; }

/* ---- Tables ----
   The header row is an eyebrow — caps, tracked, tiny — so it is mono. The cells
   below stay in Jakarta: mono is wider, and putting a client's name in it would
   move every column, which step 4 does not do. */
.th { font-family:var(--font-mono); font-size:10.5px; font-weight:500; color:var(--muted); text-transform:uppercase; letter-spacing:.07em; text-align:left; padding:0 16px 11px; white-space:nowrap; }
.td { font-size:13px; color:var(--body); padding:13px 16px; border-top:1px solid var(--border); vertical-align:middle; }
.row-hover:hover { background:var(--section); }

/* ---- Buttons ----
   44px is the phone floor. .btn-sm is the in-row button that shrinks on a
   desktop; it does not shrink here, which is the whole point of putting the
   size in the stylesheet instead of a style attribute in the template. */
.btn { font-family:inherit; border:none; cursor:pointer; border-radius:9px; font-weight:600; display:inline-flex; align-items:center; justify-content:center; gap:7px; min-height:44px; padding:0 16px; font-size:14px; transition:filter .12s,background .12s,transform .06s; white-space:nowrap; text-decoration:none; }
.btn:hover { text-decoration:none; }
.btn:active { transform:translateY(1px); }
/* The primary button INVERTS on ink — an on-ink fill with an ink label, not a
   lavender one. That is dark-surface rule 2, and it is why the fill is its own
   token rather than --brand. */
.btn-pri { background:var(--btn-pri-bg); color:var(--on-brand); }
.btn-pri:hover { background:var(--brand-dark); color:var(--on-brand); }
.btn-out { background:var(--card); color:var(--strong); border:1px solid var(--border-control); }
.btn-out:hover { border-color:var(--brand); color:var(--brand); }
.btn-ghost { background:transparent; color:var(--body); }
.btn-ghost:hover { background:var(--section); color:var(--strong); }
.btn-danger { background:var(--card); color:var(--alert); border:1px solid var(--alert-border); }
.btn-danger:hover { background:var(--alertbg); }
.btn-warn { background:var(--card); color:var(--warn); border:1px solid var(--warn-border); }
.btn-warn:hover { background:var(--warnbg); }
.btn-lg { min-height:48px; padding:0 20px; font-size:15px; }
.btn-sm { min-height:44px; padding:0 14px; font-size:14px; }
.btn[disabled], .btn:disabled { opacity:.55; cursor:default; }
.btn[disabled]:active, .btn:disabled:active { transform:none; }

/* ---- Fields ----
   16px is not a taste decision: iOS Safari zooms the whole page in when a
   focused input is smaller than that, and the coach then has to pinch back out
   between every field. The desktop block takes it down to 13.5px. */
.field { width:100%; min-height:44px; padding:0 12px; border-radius:9px; border:1px solid var(--border-control); background:var(--card); color:var(--strong); font-family:inherit; font-size:16px; outline:none; }
.field:focus { border-color:var(--bright); box-shadow:0 0 0 3px var(--focus-ring); }
/* A placeholder is text and axe-core cannot see its colour, so it gets the
   same --muted every other piece of secondary text gets: 7.36:1 on a light
   card, 6.66:1 on a raised ink one. It is never a lighter grey than that. */
.field::placeholder { color:var(--muted); opacity:1; }
textarea.field { height:auto; padding:11px 12px; resize:vertical; line-height:1.5; }
/* The chevron is a token because a data: URI cannot read a custom property —
   the whole SVG has to swap on ink, or the arrow stays at 2.5:1. */
select.field { appearance:none; -webkit-appearance:none; background-image:var(--chevron); background-repeat:no-repeat; background-position:right 12px center; padding-right:32px; }
label { display:block; }
.lbl { font-size:12px; color:var(--muted); font-weight:500; margin-bottom:4px; }
.field-error { color:var(--alert); font-size:12px; margin-top:4px; }
/* A checkbox cannot grow past its native box on every platform, so the row that
   carries it is the target instead. */
/* accent-color, or the one control in the app still wears the operating
   system's blue. On ink that blue is the only saturated thing on the screen and
   it belongs to no palette at all. */
input[type="checkbox"] { width:20px; height:20px; flex:none; accent-color:var(--brand); }
.check-row { display:flex; align-items:center; gap:11px; min-height:44px; cursor:pointer; }

/* ---- Chips ---- */
.chip { display:inline-flex; align-items:center; gap:7px; min-height:44px; padding:0 14px; border-radius:10px; font-size:14px; font-weight:500; cursor:pointer; border:1px solid var(--border); background:var(--card); color:var(--body); transition:border-color .12s,background .12s; text-decoration:none; }
.chip:hover { border-color:var(--brand); text-decoration:none; }
.chip.on { background:var(--wash); border-color:var(--brand); color:var(--brand); font-weight:600; }

/* ---- Card ---- */
.card { background:var(--card); border:1px solid var(--border); border-radius:13px; }
/* The card that matters gets an indigo edge. It used to be copper; amber is
   not a second brand colour and this is not a signal. */
.card-accent { border-left:3px solid var(--brand); }

/* ---- The headline number ----
   Mono, and --strong: every number is mono, and the number is not amber just
   for being important. The portal's count follows exactly this rule — ink
   normally, signal only when there is nothing left. */
.figure { font-family:var(--font-mono); font-weight:500; font-size:34px; line-height:1; color:var(--strong); letter-spacing:-0.02em; }
.figure-sm { font-family:var(--font-mono); font-weight:500; font-size:19px; line-height:1; color:var(--strong); }
.figure-label { font-family:var(--font-mono); font-size:11px; font-weight:500; color:var(--muted); text-transform:uppercase; letter-spacing:.07em; }
/* Zero left. The one state on this component that spends the signal colour. */
.figure.is-out, .figure-sm.is-out { color:var(--alert); }

/* ---- Segmented control ---- */
.seg { display:inline-flex; background:var(--section); border:1px solid var(--border); border-radius:11px; padding:3px; gap:3px; }
.seg button, .seg a { font-family:inherit; border:none; background:transparent; cursor:pointer; font-size:14px; font-weight:600; color:var(--muted); min-height:44px; padding:0 14px; border-radius:8px; text-decoration:none; display:inline-flex; align-items:center; justify-content:center; }
.seg button:hover, .seg a:hover { text-decoration:none; }
.seg button.on, .seg a.on { background:var(--card); color:var(--brand); box-shadow:var(--shadow-xs); }
/* On ink the shadows are all none, and this is the one lifted thing with no
   border of its own — so it gets the 1px line instead, per dark-surface rule 3.
   A ring rather than a border, so nothing moves. */
body.dark-mode .seg button.on, body.dark-mode .seg a.on { box-shadow:0 0 0 1px var(--border2); }

/* ---- Focus visible ---- */
a:focus-visible, .btn:focus-visible, button:focus-visible, .navlink:focus-visible, .field:focus-visible, .tabbar a:focus-visible { outline:2px solid var(--bright); outline-offset:2px; }

/* ---- Confirmation dialog ----
   A sheet from the bottom edge on a phone: it arrives under the thumb rather
   than in the middle of the screen, and it clears the tab bar. */
.confirm-overlay { position:fixed; inset:0; z-index:1300; background:var(--scrim); display:flex; align-items:flex-end; justify-content:center; padding:0; }
.confirm-overlay[hidden] { display:none; }
.confirm-dialog { background:var(--card); color:var(--body); border:1px solid var(--border); border-radius:16px 16px 0 0; box-shadow:var(--shadow-lg); max-width:100%; width:100%; padding:20px 18px calc(20px + env(safe-area-inset-bottom)); }
.confirm-msg { margin:0 0 18px; font-size:15px; color:var(--strong); }
.confirm-actions { display:flex; justify-content:flex-end; gap:10px; }

/* ---- App shell ----
   On a phone this is one column that scrolls with the document. The fixed
   drawer, the backdrop and the hamburger are all gone: the tab bar is the
   navigation, and there is nothing left for a drawer to hold. */
.shell-row { display:flex; flex-direction:column; min-height:100vh; background:var(--pg); color:var(--body); }
.app-sidebar { display:none; }
.nav-backdrop { display:none; }
.nav-toggle { display:none; }
.brand-lockup { display:flex; align-items:center; gap:10px; padding:4px 8px 18px; text-decoration:none; }
.brand-lockup:hover { text-decoration:none; }
.brand-mark { width:28px; height:28px; flex:none; }
/* The wordmark: Newsreader 600, tracked in, on --strong. The mark beside it is
   inline SVG filled with currentColor and inherits --brand from .brand-lockup,
   so it turned indigo from the token swap alone — it was never redrawn. */
.brand-name { font-family:var(--font-display); font-weight:600; font-size:17px; color:var(--strong); letter-spacing:-0.018em; }
body.dark-mode .brand-name { font-weight:650; }
/* An eyebrow. */
.nav-section-label { margin:16px 8px 8px; font-family:var(--font-mono); font-size:10px; font-weight:500; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
.sidebar-user { margin-top:auto; display:flex; align-items:center; gap:10px; padding:10px 8px 4px; border-top:1px solid var(--border); }
/* Initials are neither a heading nor a count, so they are interface type. */
.avatar { border-radius:999px; background:var(--brand); color:var(--on-brand); display:flex; align-items:center; justify-content:center; font-weight:600; flex:none; font-family:var(--font-sans); }

.topbar { flex:none; background:var(--card); border-bottom:1px solid var(--border); display:flex; align-items:center; gap:8px; padding:8px 12px; position:sticky; top:0; z-index:800; }
/* Interface, not display: this is chrome sitting between a search box and three
   icon buttons, and the serif belongs to the page's own h1 below it. */
.topbar-title { font-family:var(--font-sans); font-size:17px; font-weight:600; color:var(--strong); line-height:1.15; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.topbar-sub { font-size:11.5px; color:var(--muted); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.topbar-actions { margin-left:auto; display:flex; align-items:center; gap:6px; }
.topbar-search { display:none; }
.topbar-avatar, .topbar-divider { display:none; }
.icon-btn { width:44px; min-width:44px; height:44px; min-height:44px; padding:0; }
.logout-btn { width:44px; min-width:44px; height:44px; min-height:44px; padding:0; }
.logout-btn span { display:none; }
#darkToggle .icon-sun { display:none; }
body.dark-mode #darkToggle .icon-moon { display:none; }
body.dark-mode #darkToggle .icon-sun { display:inline; }

/* Flash strip */
.flashbar { flex:none; display:flex; align-items:center; gap:11px; padding:11px 13px; border-bottom:1px solid var(--border); background:var(--section); }
.flashbar.success { background:var(--wash); border-bottom-color:var(--border-brand-soft); }
.flashbar.danger, .flashbar.error { background:var(--alertbg); border-bottom-color:var(--alert-border); }
.flashbar.warning { background:var(--warnbg); border-bottom-color:var(--warn-border); }
.flashbar .txt { font-size:14px; font-weight:500; color:var(--strong); }
.flash-dismiss { margin-left:auto; width:44px; min-width:44px; height:44px; min-height:44px; padding:0; font-size:19px; line-height:1; color:var(--muted); }

/* ---- Layout utilities ----
   Everything is one column. The desktop block is where the second column
   comes back. */
main#main-content { flex:1; }
.content-wrap { width:100%; margin:0 auto; padding:16px 13px calc(var(--tabbar-h) + 22px + env(safe-area-inset-bottom)); }
/* `minmax(0, 1fr)`, never a bare `1fr`. A bare 1fr track has an AUTO minimum,
   so it grows to its widest content instead of to its container — and one wide
   child then pushes the whole page sideways. Found by the v0.4.0 overflow gate:
   the Sessions table on a connected Google Calendar page made this track
   439.67px inside a 364px box and every connected instructor scrolled that
   screen sideways on a phone. It predates v0.4.0 (measured at 82px of overflow
   on the v0.3.0 template, 63px on this one) and it applies to every screen
   using these four classes, which is most of them. */
.grid2, .grid3, .detail-cols, .form-row { display:grid; grid-template-columns:minmax(0, 1fr); gap:12px; }
.stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.mobile-only { display:block; }
.desktop-only { display:none; }

/* A scope URL, a redirect URI, an environment variable name. Every one of them
   is a single unbreakable token 50-odd characters long, and on a 390px phone one
   of them is wider than the screen.

   `anywhere`, not `break-word`: only `anywhere` lets the element's MIN-CONTENT
   width shrink below the longest token, which is the measurement a grid or flex
   track uses to size itself. With `break-word` the box still reports a 406px
   minimum and the page still scrolls sideways — it just wraps once it gets
   there. Found by the v0.4.0 overflow gate on the Google Calendar page; the
   longest of the three, `calendar.app.created`, has been on that page since
   v0.3.0. */
code { overflow-wrap:anywhere; }

.page-header { display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:16px; }
.page-header h1 { font-size:26px; margin:0; }
/* Caps and tracked — that is an eyebrow, and eyebrows are mono. Newsreader at
   13px in all-caps with letter-spacing is the one thing a serif cannot do. */
.section-title { font-family:var(--font-mono); font-size:12px; font-weight:500; color:var(--strong); text-transform:uppercase; letter-spacing:.1em; margin:0 0 10px; }
.form-group { margin-bottom:14px; }
.form-actions { display:flex; gap:10px; margin-top:16px; flex-wrap:wrap; }
.form-narrow { max-width:720px; }
.table-wrapper { overflow-x:auto; }
.card table, .table-wrapper table { width:100%; border-collapse:collapse; }
.empty-state { padding:28px; text-align:center; color:var(--muted); font-size:14px; }
.muted { color:var(--muted); }
.strong { color:var(--strong); }
.hint { font-size:12.5px; color:var(--muted); }

/* ---- Pills, badges, markers ---- */
.pill, .marker { display:inline-block; padding:3px 10px; border-radius:999px; font-size:12px; font-weight:600; background:var(--section); color:var(--muted); }
.pill.is-ok { background:var(--wash); color:var(--brand); }
.pill.is-bad { background:var(--alertbg); color:var(--alert); }
.pill.is-wait { background:var(--warnbg); color:var(--warn); }
/* Scheduled reads the same as Done. The identity has ONE indigo tint, and
   inventing a second one to keep them apart is exactly what the token package
   forbids — the word is the channel. */
.pill.is-info { background:var(--wash); color:var(--brand); }
.pill.is-mute { background:var(--section); color:var(--muted); }
/* The sign-in card's own flash. .flashbar below is a full-width strip under the
   top bar, and the login screen has neither a top bar nor a shell to hang one
   on, so it had its own inline style — one style, painted alert, for every
   message. The redirect that says "please sign in" then looked exactly like
   "invalid username or password", which is how a reader learns to stop reading
   the strip. Neutral by default; the alert tone is spent on a refusal only. */
.login-flash { padding:10px 12px; border-radius:9px; margin-bottom:12px; font-size:12.5px;
               background:var(--wash); border:1px solid var(--border-brand-soft); color:var(--strong); }
.login-flash.danger, .login-flash.error { background:var(--alertbg); border-color:var(--alert-border); color:var(--alert); }
/* On ink every fill collapses towards the same neutral, so each pill gets the
   1px line that dark-surface rule 3 asks of any raised block. */
body.dark-mode .pill, body.dark-mode .marker, body.dark-mode .role-badge { box-shadow:inset 0 0 0 1px var(--border2); }
.role-badge { display:inline-block; padding:3px 10px; border-radius:999px; font-size:12px; font-weight:600; background:var(--wash); color:var(--brand); }

/* ---- Timeline ---- */
.timeline { list-style:none; margin:0; padding:0; }
.timeline li { font-size:13px; color:var(--muted); padding:5px 0 5px 14px; border-left:2px solid var(--border2); margin-left:2px; }
.timeline li.is-key { border-left-color:var(--brand); color:var(--body); }

/* ---- Data table as stacked cards ----
   This is the phone's default rendering of any table carrying class="rtable";
   the desktop block turns it back into a real table. The first cell is the row's
   identity — the client's name, or the time of the booking — so it is a heading
   line with no label in front of it, and everything after it is a compact
   label/value pair. Reading a day's schedule should not cost a scroll per
   booking. Every <td> needs data-label="Header"; an empty one suppresses it. */
.table-card { border:none; background:transparent; border-radius:0; padding:0 !important; }
.rtable { display:block; }
.rtable thead { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
.rtable tbody { display:block; }
/* Two columns: the identity and its action share the first line, and every
   remaining field runs full width underneath. Stacking the action on its own
   line instead costs about 90px per row, which is most of a booking. */
.rtable tr { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; column-gap:10px; border:1px solid var(--border); border-radius:12px; background:var(--card); margin-bottom:8px; padding:2px 0 5px; box-shadow:var(--shadow-xs); }
.rtable tr:last-child { margin-bottom:0; }
.rtable tr[onclick] { cursor:pointer; }
.rtable td { grid-column:1 / -1; border:none; padding:4px 13px; display:flex; gap:12px; align-items:center; justify-content:space-between; text-align:right; font-size:13.5px; }
.rtable td::before { content:attr(data-label); font-family:var(--font-mono); font-size:10.5px; font-weight:500; color:var(--muted); text-transform:uppercase; letter-spacing:.07em; text-align:left; flex:none; }
.rtable td:not([data-label])::before, .rtable td[data-label=""]::before { content:""; }
.rtable td:empty { display:none; }
/* The identity line — client's name, or the time of the booking. */
.rtable td:first-child { grid-column:1; grid-row:1; display:block; text-align:left; padding:8px 0 6px 13px; font-size:15.5px; font-weight:600; color:var(--strong); }
.rtable td:first-child::before { content:""; display:none; }
.rtable td:first-child a { font-weight:600; }
/* The action rides on that first line, hard against the right edge. */
.rtable td[data-label=""] { grid-column:2; grid-row:1; padding:6px 13px 4px 0; justify-content:flex-end; }
.rtable td[data-label=""] .btn { padding:0 15px; }
/* A link in a row is the row's real target — the client's name is how the coach
   opens the client. 20px of text is not a target, so the link carries the 44px
   itself rather than relying on the line it sits in. */
.rtable td a:not(.btn) { display:inline-flex; align-items:center; min-height:44px; }

/* ---- The exercise editor (the session form) ----
   A card per exercise, fields at 44px, three controls that are each a real
   target with real space between them. This screen is used one-handed on a
   phone between clients; a 32px delete button beside a 32px reorder button is
   how a set of numbers gets thrown away. Above the breakpoint the card opens
   out into one row. */
.ex-card { border:1px solid var(--border); border-radius:12px; padding:10px; background:var(--card); margin-bottom:10px; }
.ex-card-head { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.ex-card-num { font-family:var(--font-mono); font-size:13px; font-weight:500; color:var(--muted); min-width:18px; }
.ex-card .field { min-height:44px; font-size:16px; }
.ex-card textarea.field { height:auto; }
.ex-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.ex-note { margin-top:8px; }
.ex-mini { width:44px; min-width:44px; height:44px; min-height:44px; padding:0; border-radius:10px; color:var(--muted); }
.ex-mini:hover { color:var(--strong); }
.ex-mini svg { pointer-events:none; }
.ex-mini[data-remove]:hover { color:var(--alert); background:var(--alertbg); }
.ex-actions { margin-left:auto; display:flex; gap:8px; }
/* The save bar rides above the tab bar, not under it. */
.sticky-actions { position:sticky; bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom)); z-index:5; background:var(--pg); border-top:1px solid var(--border); padding:12px 0; margin-top:14px; display:flex; gap:10px; flex-wrap:wrap; }
.sticky-actions .btn { flex:1; }
.autosaved { font-size:12.5px; color:var(--muted); align-self:center; }

/* ---- Pagination ---- */
.pagination { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:14px; font-size:13px; }
.pagination-info { color:var(--muted); }
.pagination-links { display:flex; gap:8px; flex-wrap:wrap; }
.page-link { display:inline-flex; align-items:center; justify-content:center; min-width:44px; min-height:44px; padding:0 12px; border-radius:9px; border:1px solid var(--border-control); background:var(--card); color:var(--body); text-decoration:none; font-weight:500; }
.page-link:hover { border-color:var(--brand); color:var(--brand); text-decoration:none; }
.page-link.current { background:var(--brand); color:var(--on-brand); border-color:var(--brand); }
.page-link.disabled, .page-link.ellipsis { color:var(--muted); border-color:var(--border); background:transparent; cursor:default; }

/* ============================================================
   DESKTOP — the one breakpoint, and everything in it is an
   addition to the phone above, not a correction of it.
   ============================================================ */
/* ============================================================
   The week, and the availability screen (v0.4.0).

   Phone first, and here that is not a formality: seven columns
   on a 390px screen is 55px a column, which is not a calendar,
   it is seven slivers. So the phone gets an AGENDA — days
   stacked, each one a short list — and the seven-column grid
   arrives with the desktop, where the columns are ~150px and a
   week is legible at a glance.

   A busy block is the one thing on this screen that carries no
   information beyond its time, and it is drawn that way: the
   neutral fill, muted text, no link, no hover, no cursor
   change. It cannot be opened because there is nothing behind
   it — `gcal_busy` has two datetime columns and no third. The
   grey is --section, the one neutral fill in the token set, so
   it swaps with the ground like everything else.
   ============================================================ */
.week-bar { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px; flex-wrap:wrap; }
.week-nav { display:flex; gap:8px; }
.week-range { font-family:var(--font-mono); font-size:12.5px; color:var(--muted); }
.week-stale { margin:0 0 14px; }

.week-grid { display:flex; flex-direction:column; gap:10px; }
.week-day { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:12px 13px; }
.week-day.is-today { border-color:var(--border-brand-soft); }
.week-day.is-closed { background:var(--section); }
.week-day-head { display:flex; align-items:baseline; gap:8px; margin-bottom:8px; }
.week-day-name { font-size:12px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); }
.week-day-num { font-family:var(--font-mono); font-size:15px; color:var(--strong); }
.week-day.is-today .week-day-num { color:var(--brand); }

.week-list { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }
.week-entry { border-radius:9px; }
.week-entry a,
.week-entry.is-busy { display:flex; align-items:center; gap:10px; min-height:44px; padding:8px 10px; border-radius:9px; text-decoration:none; }
.week-entry.is-session a { background:var(--wash); color:var(--strong); }
.week-entry.is-session a:hover { background:var(--wash); box-shadow:var(--shadow-xs); }
.week-entry.is-session.is-done a { background:var(--section); }
.week-entry.is-busy { background:var(--section); color:var(--muted); cursor:default; }
.week-time { font-family:var(--font-mono); font-size:13px; flex:none; }
.week-who { font-size:14px; font-weight:500; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.week-tag { margin-left:auto; font-size:11px; font-weight:600; letter-spacing:.03em; color:var(--muted); flex:none; }
.week-tag.is-portal { color:var(--brand); }
.week-entry.is-busy .week-tag { font-family:var(--font-mono); font-weight:400; }
.week-empty { margin:0; font-size:13px; color:var(--muted); }

.avail-add { display:flex; flex-direction:column; gap:0; }
.avail-add .form-group { margin-bottom:12px; }
.avail-add-action { margin-bottom:0; }
.avail-add-action .btn { width:100%; }

.slot-preview { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:4px; }
.slot-preview li { display:flex; align-items:center; gap:10px; min-height:32px; padding:4px 0; border-bottom:1px solid var(--border); }
.slot-preview li:last-child { border-bottom:none; }

@media (min-width: 861px) {
  .shell-row { flex-direction:row; height:100vh; overflow:hidden; }
  main#main-content { overflow:auto; }

  .app-sidebar { display:flex; width:236px; flex:none; background:var(--sidebar); border-right:1px solid var(--border); flex-direction:column; padding:16px 13px; }
  .navlink { min-height:0; font-size:13.5px; }

  /* The tab bar is the phone's navigation and only the phone's. */
  .tabbar { display:none; }

  .topbar { height:60px; padding:0 22px; gap:16px; position:static; }
  .topbar-title { font-size:16px; }
  .topbar-search { display:flex; align-items:center; gap:8px; height:38px; width:220px; padding:0 12px; background:var(--section); border:1px solid var(--border); border-radius:9px; color:var(--muted); font-size:13px; }
  .topbar-search:focus-within { border-color:var(--brand); }
  .topbar-search-input { flex:1; min-width:0; height:100%; border:none; background:transparent; color:var(--strong); font-family:inherit; font-size:13px; outline:none; padding:0; }
  .topbar-search-input::placeholder { color:var(--muted); }
  .topbar-actions { gap:10px; }
  .topbar-avatar { display:flex; }
  .topbar-divider { display:block; }
  .icon-btn { width:38px; min-width:38px; height:38px; min-height:38px; }
  .logout-btn { width:auto; min-width:0; height:38px; min-height:38px; padding:0 12px; font-size:12.5px; }
  .logout-btn span { display:inline; }
  .logout-btn svg { display:none; }

  .flashbar { padding:11px 22px; }
  .flashbar .txt { font-size:13px; }
  .flash-dismiss { width:26px; min-width:26px; height:26px; min-height:26px; font-size:16px; }

  .btn { min-height:38px; font-size:13px; }
  .btn-lg { min-height:48px; font-size:14.5px; }
  .btn-sm { min-height:34px; padding:0 12px; font-size:12.5px; }
  .field { min-height:40px; font-size:13.5px; }
  .chip { min-height:0; padding:7px 13px; font-size:12.5px; }
  .seg button, .seg a { min-height:0; padding:5px 11px; font-size:12px; }
  .seg { border-radius:9px; padding:3px; gap:2px; }
  .check-row { min-height:0; }
  .page-link { min-width:32px; min-height:32px; height:32px; padding:0 9px; border-radius:8px; font-size:12.5px; }
  .pagination { font-size:12.5px; }
  .pagination-links { gap:5px; }
  .field-error { font-size:11.5px; }
  .hint { font-size:11.5px; }
  .empty-state { font-size:13px; }
  .pill, .marker, .role-badge { padding:2px 9px; font-size:11px; }
  .timeline li { font-size:12px; padding:4px 0 4px 14px; }

  .content-wrap { max-width:1100px; padding:24px; }
  /* Same rule as the phone block: every track takes a 0 minimum, so a long
     client name or an untruncatable scope URL cannot widen the page. */
  .grid2 { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .grid3 { grid-template-columns:repeat(3, minmax(0, 1fr)); }
  .detail-cols { grid-template-columns:minmax(0, 1fr) minmax(0, 1.4fr); align-items:start; }
  .form-row { grid-template-columns:1fr 1fr; }
  .stat-grid { grid-template-columns:repeat(3,1fr); gap:14px; }
  .mobile-only { display:none; }
  .desktop-only { display:block; }
  .page-header h1 { font-size:24px; }

  /* Back to a real table. */
  .table-card { border:1px solid var(--border); background:var(--card); border-radius:13px; padding:16px !important; }
  .rtable { display:table; }
  .rtable thead { position:static; width:auto; height:auto; margin:0; overflow:visible; clip:auto; }
  .rtable tbody { display:table-row-group; }
  .rtable tr { display:table-row; border:none; border-radius:0; background:transparent; margin:0; padding:0; box-shadow:none; }
  .rtable td { display:table-cell; border-top:1px solid var(--border); padding:13px 16px; text-align:left; font-size:13px; color:var(--body); font-weight:400; }
  .rtable td::before { content:""; display:none; }
  .rtable td:first-child { display:table-cell; padding:13px 16px; font-size:13px; font-weight:400; color:var(--body); }
  .rtable td[data-label=""] { padding:13px 16px; }
  .rtable td[data-label=""] .btn { width:auto; padding:0 12px; }
  .rtable td a:not(.btn) { display:inline; min-height:0; }
  .rtable td:first-child a { font-weight:500; }

  /* Every child of an exercise row is placed by an explicit grid-column, so the
     markup order — which is what the phone reads — never has to change to suit
     the desktop layout. */
  .ex-card { display:grid; grid-template-columns:22px minmax(0,2fr) 92px 92px minmax(0,2fr) auto; gap:8px; align-items:center; padding:8px 10px; margin-bottom:8px; }
  .ex-card-head, .ex-grid { display:contents; }
  .ex-card-num { grid-column:1; font-size:12px; }
  .ex-name   { grid-column:2; }
  .ex-weight { grid-column:3; }
  .ex-reps   { grid-column:4; }
  .ex-note   { grid-column:5; margin-top:0; }
  .ex-actions { grid-column:6; margin-left:0; gap:5px; }
  .ex-card .field { min-height:36px; font-size:13.5px; }
  .ex-mini { width:34px; min-width:34px; height:32px; min-height:32px; border-radius:8px; }
  .sticky-actions { position:static; background:transparent; border-top:none; padding:0; }
  .sticky-actions .btn { flex:0 0 auto; }

  .confirm-overlay { align-items:center; padding:16px; }
  .confirm-dialog { max-width:420px; border-radius:13px; padding:20px; }
  .confirm-msg { font-size:14px; }
  .confirm-actions { gap:8px; }

  /* The week becomes a week. Seven equal columns with `minmax(0, 1fr)` rather
     than `1fr`: a bare 1fr has an `auto` minimum, so one long client name in
     one day would push the whole grid wider than the page and give the app a
     horizontal scrollbar. */
  .week-grid { display:grid; grid-template-columns:repeat(7, minmax(0, 1fr)); gap:8px; align-items:start; }
  .week-day { padding:10px; min-height:120px; }
  .week-day-head { margin-bottom:6px; }
  .week-day-num { font-size:14px; }
  .week-entry a,
  .week-entry.is-busy { min-height:0; padding:6px 8px; gap:6px; flex-wrap:wrap; }
  .week-time { font-size:12px; }
  .week-who { font-size:12.5px; }
  .week-tag { margin-left:0; font-size:10.5px; flex-basis:100%; }

  /* The add-a-window form goes to one row: day, from, to, button. */
  .avail-add { flex-direction:row; align-items:flex-end; gap:10px; flex-wrap:wrap; }
  .avail-add .form-group { margin-bottom:0; flex:1 1 120px; min-width:0; }
  .avail-add-action { flex:0 0 auto; }
  .avail-add-action .btn { width:auto; }
}

/* ============================================================
   Reduced motion. The drawer slide, the button press and every
   transition above are decoration; the app is identical without
   them, so honour the system setting rather than animating over
   someone who asked us not to.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .btn:active { transform:none; }
}
