/* ── Self-hosted brand font (Libre Caslon Text, latin) ───────────────── */
@font-face {
  font-family: 'Libre Caslon Text'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/static/fonts/LibreCaslonText-Regular.woff2') format('woff2');
}
@font-face {
  font-family: 'Libre Caslon Text'; font-style: normal; font-weight: 700;
  font-display: swap; src: url('/static/fonts/LibreCaslonText-Bold.woff2') format('woff2');
}
@font-face {
  font-family: 'Libre Caslon Text'; font-style: italic; font-weight: 400;
  font-display: swap; src: url('/static/fonts/LibreCaslonText-Italic.woff2') format('woff2');
}

/* ══ THE SAFE-AREA INSETS, AS RESOLVED PIXELS ══════════════════════════════
   ⚠ THIS STYLESHEET CONTAINS NO env(safe-area-inset-*). Not one. The values
   arrive here from public/safe-area.js, which reads them off the device once
   and writes them into these two properties.

   THE REASON IS THE TAB BAR, which has come unpinned in the installed iOS app
   twice, and both times the change that did it put an env(safe-area-inset-*)
   term into the shell's layout:

     0e81410  .nav gained `padding: env(safe-area-inset-top) 16px 0`. No page
              declared viewport-fit=cover then, so the inset resolved to ZERO
              and that declaration computed identically to the `padding: 0 16px`
              it replaced. NOT ONE PIXEL MOVED. The fixed tab bar still came off
              its mooring on every page, parking mid-screen on the way down and
              healing on the way up. Named by bisect over three device passes.

     0c1164c  viewport-fit=cover went onto all sixty-five views. It changed no
              declaration whatsoever; it changed whether the ones already here
              resolved to something. Three went live on a phone dashboard, one
              of them inside the fixed bar's own padding. Same signature, on the
              non-calendar pages, reported in QC 5.

   The pixels are not what does it -- the first trigger moved none -- so the
   trigger is the DEPENDENCY. safe-area.js carries the full account and the
   mechanism it rests on. What matters here is the rule: a resolved length has
   no dependency, so these two properties are where the insets stop.

   0px IS A WORKING DEFAULT, and is the geometry the app shipped with before
   0c1164c. With the script blocked the status band collapses and the bar's pad
   is 14px: degraded, not broken.

   --sal and --sar exist for symmetry and a landscape rotation; nothing uses
   them yet. */
:root { --sat: 0px; --sab: 0px; --sal: 0px; --sar: 0px; }

/* ── Rhythm Tutoring brand tokens (green + gold) ──────────────────────── */
:root {
  --green-deep: #14372A;  /* headers, dark panels */
  --green-btn:  #1B4332;  /* buttons, links */
  --gold:       #C9A227;  /* borders, accents */
  --gold-light: #E3B53A;  /* accents on dark */
  --gold-dark:  #A8842A;  /* eyebrow text on light */
  /* The DARKER gold, for gold TEXT that has to be read rather than glanced at.
     --gold-dark is an eyebrow colour and sits at about 3.4:1 on paper, which is
     fine for a tracked-out label and not for a phrase somebody reads.

     DECLARED ONCE BECAUSE THE REQUIREMENT IS THAT THEY ARE EQUAL, the same
     argument --hair carries above: the timezone feature prints this colour in
     three places -- the roster's zone phrase, the Rolodex's, and the DEFAULT
     pill -- and the design's token table names one value for all three. Three
     literals would drift the first time one of them was nudged.

     The consent page declares its own --pc-gold-ink at the same value, scoped to
     .pc, for its validation text. Left alone: that block is a different feature
     with its own namespace, and aliasing it is a separate call from this one. */
  --gold-ink:   #7C6210;  /* gold text meant to be read; 5:1 on paper */
  --cream:      #FAF7F0;  /* page background */
  --off-white:  #FDFCF9;  /* cards on cream */
  --field-bg:   #F1ECE0;  /* tinted fill for anything the tutor fills in */
  --field:      #D6CDB8;  /* its border, on text inputs and tiles alike */
  --ink:        #1D2B23;  /* headings */
  --body:       #2C3A31;  /* body text */
  --muted:      #5C6B62;  /* secondary text */
  /* ⚠ RAISED FROM #8A948C, WHICH WAS BELOW THE CONTRAST FLOOR. The old value
     measures 3.06:1 on the form card, so every .optout__note and every
     placeholder on the admin forms was under 4.5:1 and had been since the token
     was written. #67736B is 4.9:1 and reads the same: both prototypes in
     design_handoff_edit_student draw it, and it is a FIX rather than a
     divergence, which is why it lands on the token and not on two screens.
     ⚠ IT REACHES EVERY SURFACE, deliberately. 130 declarations read this token
     and the alias --rt-muted points at it, so anything that was failing the
     floor on this colour passes now and nothing needs finding one screen at a
     time. Nothing else about the value changes: same hue, one step darker. */
  --faint:      #67736B;  /* tertiary text; 4.9:1 on the form card */
  --line:       #D4D9D3;  /* borders */
  /* THE LAST TWO FROM THE CALENDAR BUNDLE'S TOKEN TABLE, declared rather than
     written as literals at the two places that use them. Both already existed
     as values in this file: #A9B1AA is what the month's past days needed and
     #FDF7E6 is the gold tint the queue's validation blocks already paint.
     Naming them is what stops the calendar drifting away from those. */
  --disabled:   #A9B1AA;  /* taken slots, past days */
  /* FROM THE HEX INVENTORY IN handoff/design-tokens.md, where the chart tooltip
     bundle's token table names it "Sage · second tutor line". It had no
     declaration here because nothing drew a fourth tutor line until the cap on
     that panel came off. */
  --sage:       #8FA69B;  /* tutor lines past the first three */
  --gold-tint:  #FDF7E6;  /* validation and warning grounds */
  /* Hairlines INSIDE a card: row separators and card heads. --line edges the card
     itself; this is the lighter rule drawn between things within one.

     It comes from the four design mocks and was the single token of theirs that
     did not make it into this block when they were folded in. The seven
     declarations that reference it, across the admin, queue, sent and history
     sections, drew nothing at all until now: a var() that cannot resolve
     invalidates its whole shorthand, so `border-top: 1px solid var(--hair)` reset
     border-style to none and drew no line rather than a wrong-coloured one.

     .08 is deliberately the value .trust__row already hardcodes two hundred lines
     below, so defining the token changes nothing that was already painting; the
     only delta is those eight declarations coming alive at a weight that matches
     the one separator on this page that was never broken. The mocks disagree
     about the value (.08 in sent/queue/history, .1 in admin-tools) and the admin
     mock disagrees with itself, hardcoding .08 for its own trust rows. .08 is the
     value that keeps the live page self-consistent. */
  --hair:       rgba(27, 67, 50, .08);
  --danger:     #b42318;  /* destructive / errors */
  /* THE DASHBOARD'S FOOT RHYTHM. The gap above the escape-hatch line, the gap
     below it, and the gap under the notifications card above the mobile tab
     bar, all this one value.

     DECLARED ONCE BECAUSE THE REQUIREMENT IS THAT THEY ARE EQUAL. Two literals
     that happen to match today drift the first time one of them is nudged,
     which is exactly how the line came to sit off-centre: 16px above it and 0
     below, because the card it sits above never had a top margin at all. */
  --dash-foot-gap: 16px;
  --serif: 'Libre Caslon Text', Georgia, serif;
  --sans: Helvetica, Arial, sans-serif;

  /* ── handoff/rhythm-tokens.css, merged ─────────────────────────────────
     The design handoff ships its own --rt-* namespace. Rather than keep two
     parallel palettes, the ones that are the SAME COLOUR as a token this file
     already had are declared as aliases: one value, two names, and no way for
     them to drift apart. The rest are genuinely new and carry a literal.

     Read the aliases as a translation table between the two vocabularies. Note
     that --muted and --faint sit one step off the handoff's naming: what the
     handoff calls "secondary" this file calls --muted, and what it calls
     "muted" this file calls --faint. Only the words disagree, and renaming the
     existing ones would touch every page.
     ⚠ --faint HAS SINCE BEEN RAISED to #67736B for contrast, so it is no longer
     byte-identical to the handoff's "muted". The alias is kept pointing at the
     token rather than pinned to the old literal: one value, two names, and the
     fix reaches both. */
  --rt-green-deep: var(--green-deep);        /* #14372A */
  --rt-green-button: var(--green-btn);       /* #1B4332 */
  --rt-gold: var(--gold);                    /* #C9A227 */
  --rt-gold-bright: var(--gold-light);       /* #E3B53A */
  --rt-gold-dark: var(--gold-dark);          /* #A8842A */
  --rt-cream: var(--cream);                  /* #FAF7F0 */
  --rt-offwhite: var(--off-white);           /* #FDFCF9 */
  --rt-white: #FFFFFF;
  --rt-ink: var(--ink);                      /* #1D2B23 */
  /* NOT an alias of --body (#2C3A31). The handoff specifies a lighter colour
     for long-form paragraphs, and both hexes appear in its inventory. */
  --rt-body: #3D4A42;
  --rt-secondary: var(--muted);              /* #5C6B62 */
  --rt-muted: var(--faint);                  /* #67736B, raised for contrast */
  --rt-disabled: #B7BEB8;
  --rt-border-input: var(--line);            /* #D4D9D3 */
  --rt-rule: var(--hair);                    /* rgba(27,67,50,.08) */
  --rt-border-card: rgba(27,67,50,.12);
  --rt-border-panel: rgba(27,67,50,.14);
  --rt-border-control: rgba(27,67,50,.16);
  --rt-tint-neutral: rgba(27,67,50,.07);
  --rt-tint-selected: rgba(27,67,50,.05);
  --rt-tint-gold: rgba(201,162,39,.18);
  --rt-border-gold: rgba(168,132,42,.35);
  --rt-underline-gold: rgba(201,162,39,.55);
  --rt-font-display: var(--serif);
  --rt-font-ui: var(--sans);
  --rt-radius-button: 2px;
  --rt-radius-input: 5px;
  --rt-radius-card: 6px;
  --rt-radius-panel: 8px;
  --rt-radius-dialog: 10px;
  --rt-radius-pill: 100px;
  --rt-shadow-frame: 0 14px 36px rgba(20,55,42,.14);
  --rt-shadow-raised: 0 18px 44px rgba(20,55,42,.2);
  --rt-shadow-dialog: 0 22px 50px rgba(20,55,42,.22);
  --rt-shadow-active-gold: 0 8px 22px rgba(201,162,39,.12);

  /* The recommended implementation scale from the handoff, not the raw
     inventory. Every length in the training styles below comes from here. */
  --rt-s1: 4px;  --rt-s2: 8px;  --rt-s3: 12px; --rt-s4: 16px; --rt-s5: 20px;
  --rt-s6: 24px; --rt-s7: 28px; --rt-s8: 32px; --rt-s9: 40px; --rt-s10: 64px;
}

* { box-sizing: border-box; }

/* Reserve the scrollbar gutter for the whole portal, once, on the document root.
   Without this the viewport narrows the moment a page becomes tall enough to
   scroll, and every centered layout jumps sideways. Expanding a report on the
   history page is the obvious case, but the queue and the approval screen grow
   and shrink the same way, so this is global rather than per page.

   `stable` reserves the space whether or not a scrollbar is currently needed, and
   shows no track when it is not, so a short page keeps its clean edge. */
html { scrollbar-gutter: stable; }

/* Fallback for engines without scrollbar-gutter (Safari only shipped it in 18.2,
   and this repo declares no browserslist, so older Safari is assumed to be in
   scope). A permanently visible scrollbar is uglier than a reserved gutter, but
   it holds the width just as well, which is the whole point.
   Guarded by @supports so the two never both apply: unguarded, overflow-y would
   force a visible track on modern browsers too and cancel the nicer behaviour. */
@supports not (scrollbar-gutter: stable) {
  html { overflow-y: scroll; }
}

body {
  margin: 0;
  font: 16px/1.5 var(--sans);
  color: var(--body);
  background: var(--cream);
}
a { color: var(--green-btn); }
a:hover { color: var(--gold); }

/* ── Top bar / nav ───────────────────────────────────────────────────── */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; background: var(--green-deep); color: #fff;
}
.brand { font-weight: 700; letter-spacing: 0.14em; font-size: 15px; color: var(--off-white); }
.topnav { display: flex; align-items: center; gap: 14px; }
.who { color: rgba(255,255,255,.7); font-size: 14px; }
.navlink { color: rgba(255,255,255,.8); text-decoration: none; font-size: 14px; }
.navlink:hover { color: var(--gold-light); text-decoration: underline; }
.linkbtn {
  background: none; border: 0; color: rgba(255,255,255,.8); cursor: pointer;
  font: inherit; text-decoration: underline; padding: 0;
}
.linkbtn:hover { color: var(--gold-light); }
form.inline { display: inline; margin: 0; }

/* ── Layout / cards / headings ───────────────────────────────────────── */
.container { max-width: 760px; margin: 32px auto; padding: 0 20px; }
/* SHARED BY EVERY CARD IN THE PRODUCT, not just the deck: the admin screens,
   the error page and the survey pages all use this class. Padding went 28px to
   37px for the lesson reader, where the card is the substantial centre of the
   screen, and every other surface inherits the roomier box with it. */
.card {
  background: var(--off-white); border: 1px solid var(--line);
  border-radius: 12px; padding: 37px;
}
.card.narrow { max-width: 420px; margin: 0 auto; }

h1 { font-family: var(--serif); font-weight: 400; font-size: 24px; color: var(--ink); margin: 0 0 16px; }
h2 { font-family: var(--serif); font-weight: 400; font-size: 18px; color: var(--ink); margin: 24px 0 10px; }

/* ══ THE PAGE TITLE ═════════════════════════════════════════════════════════
   ONE treatment for the <h1> at the top of a top-level page, declared once
   here. Dashboard, Student work, Training, Competency and Admin tools each had
   their own: 30px, 28px, 32px, 32px and 28px, at two different line heights and
   three different margin sets. Nothing chose those numbers; they arrived one
   page at a time, and side by side they read as sloppy rather than as anything.

   .page-title IS THE NAME TO USE ON A NEW PAGE. The other three are the classes
   the existing titles already carry, aliased here rather than renamed across
   twenty-five views: the point is one declaration, not one spelling. A future
   page that writes any of them gets the same title, which is what stops the
   next one being restyled locally.

   THE BASE IS .h1's TREATMENT, because it was already the most common by a
   distance -- two of the five top-level pages and around twenty overall, while
   .t-h1 had four and .greet had one. This is a consolidation onto the existing
   majority, not a new look: 28px serif at 400, which is also what the bare <h1>
   element above resolves to at a larger size.

   THE SPACE BELOW IS 16px, and it is the one value that could make the gap
   uniform. Intro paragraphs are out of scope and keep their own margins, so the
   effective gap is the LARGER of this and the paragraph's margin-top: Student
   work's intro is an unstyled <p> carrying the user agent's 16px, so anything
   less than 16 would have left that page alone and moved only the others. It is
   also what the bare <h1> already uses.

   THE SPACE ABOVE IS NONE, from the title itself. Three of the five had exactly
   that and took their air from the page container's own 28px of padding. The
   exception is a title under an eyebrow, immediately below. */
.page-title,
.h1,
.t-h1,
.greet {
  font-family: var(--serif);
  font-size: 28px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--ink);
  margin: 0 0 16px;
}

/* A TITLE THAT FOLLOWS AN EYEBROW keeps the 8px that used to live on .t-h1's
   own margin-top. The eyebrow carries no margin of its own and is out of scope
   here, so with a flat margin-top of 0 the two would sit welded together on the
   training page. The gap belongs to the pair, so the rule is about the pair. */
.t-eyebrow + .page-title,
.t-eyebrow + .h1,
.t-eyebrow + .t-h1,
.t-eyebrow + .greet { margin-top: 8px; }

/* ── Forms ───────────────────────────────────────────────────────────── */
.stack { display: flex; flex-direction: column; gap: 14px; }
.stack label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 14px; }

input, textarea, select {
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 6px;
  font-size: 16px; font-family: inherit; width: 100%; background: #fff; color: var(--body);
}
textarea { resize: vertical; }
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(201,162,39,.15);
}
form.submitting { opacity: .65; pointer-events: none; }

.btn {
  background: var(--green-btn); color: #fff; border: 1.5px solid var(--gold);
  border-radius: 6px; padding: 11px 16px; font-size: 15px; font-weight: 600;
  font-family: var(--sans); cursor: pointer; transition: background .15s;
}
.btn:hover { background: var(--green-deep); }
.btn:disabled { opacity: .6; cursor: default; }
.btn.small { padding: 6px 12px; font-size: 13px; }
.btn.danger { background: var(--danger); border-color: #7a1a12; }
.btn.danger:hover { background: #912018; }
.btn.secondary { background: var(--muted); border-color: var(--muted); }
.btn.secondary:hover { background: #4a574f; }

/* ── Messages ────────────────────────────────────────────────────────── */
.alert {
  background: #fff; border: 1px solid #fecdca; border-left: 3px solid var(--danger);
  color: var(--danger); border-radius: 6px; padding: 10px 12px; font-size: 14px;
}
.notice {
  background: #ecfdf3; color: #08694a; border: 1px solid #a6f4c5;
  border-radius: 8px; padding: 10px 12px; font-size: 14px;
}
.facts { color: var(--muted); }
.muted { color: var(--muted); font-size: 14px; }

/* ── Tables / misc ───────────────────────────────────────────────────── */
table.grid { width: 100%; border-collapse: collapse; margin-top: 12px; }
table.grid th, table.grid td {
  text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line);
  font-size: 14px; vertical-align: top;
}
table.grid th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
code { background: #efeee7; padding: 1px 5px; border-radius: 4px; font-size: 13px; color: var(--green-btn); }

.flags { background: #fbf6e6; border: 1px solid #e6cf8a; border-radius: 8px; padding: 8px 14px; margin: 12px 0; font-size: 14px; }
.survey-detail { margin: 12px 0; font-size: 14px; }
.survey-detail summary { cursor: pointer; color: var(--green-btn); font-weight: 600; }
.private { color: var(--danger); }

.rowlist { list-style: none; padding: 0; margin: 0; }
.rowlist li { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.pill { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #efeee7; color: var(--muted); }
.pill.on { background: #ecfdf3; color: #08694a; }
.status-tag { font-size: 12px; padding: 2px 8px; border-radius: 6px; background: #efeee7; color: var(--muted); font-weight: 600; }

.history-item { padding: 12px 0; border-bottom: 1px solid var(--line); }
.history-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.history-item details { margin-top: 8px; }
.history-item summary { cursor: pointer; color: var(--green-btn); font-weight: 600; font-size: 14px; }

.reject-banner {
  background: #fbf6e6; border: 1px solid #e6cf8a; border-radius: 8px;
  padding: 12px 16px; margin-bottom: 16px; font-size: 14px; color: #7a4d05;
}
.reject-banner p { margin: 6px 0 0; }

/* ══ Dashboard (standalone brand layout — design/dashboard.html) ═══════════
   .nav* / .page / .table* / .eyebrow / .sectionhead / .empty are shared with the
   survey, approval, and confirmation screens. */
/* ⚠ THE STATUS BAR BAND. A sibling ABOVE .nav carrying the top inset as its
   height, in the nav's green, so the band the iOS clock sits in reads as part
   of the bar.

   IT IS NOT PADDING ON .nav, and that is the whole point. Padding on .nav is
   the change that unpinned the fixed tab bar on every page in 0e81410;
   test/training-mobile-layout refuses it by name and that refusal stands. This
   element is not .nav, does not touch .nav's box, and is position: static.

   THE HEIGHT IS --sat, NOT THE INSET FUNCTION ITSELF. See the top of this file:
   this stylesheet no longer names the inset function anywhere, because putting
   it in shell layout is the one thing both tab-bar detachments have in common.
   The value is the same; the dependency is gone.

   Zero height until a page declares viewport-fit=cover, which every portal view
   now does. Before that the insets all resolved to 0, which is why the 0e81410
   pad could not have fixed the complaint even as it broke the bar. */
.navtop { height: var(--sat); background: var(--green-deep); }

/* ⚠ NO OVERFLOW ON THE NAV, AND THIS IS A REGRESSION FIX WITH A RULING BEHIND
   IT. design_handoff_error_pages lists `nav overflow-x: auto` as its third
   layout rule, and taking it literally BROKE THE ADMIN DROPDOWN COMPLETELY.

   overflow-x: auto with overflow-y: visible is not a combination CSS allows:
   the visible axis computes to auto, so the nav becomes a scroll container on
   BOTH axes. .nav__panel is absolutely positioned at top: 100%, which is
   outside the nav's own 60px box, so it was clipped away entirely. Measured in
   a real browser: on click the panel un-hid, took display:block and laid out at
   y=60 to 328 -- and elementFromPoint over that whole area returned the PAGE.
   The trigger was visible, the script worked, aria-expanded flipped, and there
   was no way to reach /admin from the nav. That is what a tutor reported as
   "no Admin dropdown", and a server-side render could never have shown it.
 
   ⚠ AN OVERFLOW CONTAINER MUST NEVER BE AN ANCESTOR OF THE PANEL. That rules
   out .nav AND .nav__links, which is also where the bundle's warning against
   overflow:hidden on the links row points, for a different reason.
 
   The bundle's INTENT -- no item is hidden and the bar does not wrap -- is what
   its other two rules actually buy: nowrap and flex-shrink:0 on the wordmark
   and the links, and the viewer's own name as the only thing that yields. Those
   are in place below and they are what keeps the bar on one line. */
.nav { background: var(--green-deep); height: 60px; display: flex; align-items: center; padding: 0 26px; gap: 26px; }
.nav a { text-decoration: none; }
.nav__lockup { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.nav__shield { width: 24px; height: 27px; object-fit: contain; display: block; }
.nav__wordmark { font-family: var(--serif); font-size: 14px; font-weight: 700; color: var(--off-white); white-space: nowrap; }
.nav__wordmark em { font-weight: 400; color: var(--gold-light); }
/* ── THE FIVE NAV ITEMS ARE ONE BOX ──────────────────────────────────────
   Four are <a> and the fifth is a <button>, and every property that differs
   between those two element types is stated here rather than left to a user
   agent. Two rounds of this were spent moving the trigger up and down by two
   pixels; the cause was never an offset, it was that the boxes were not the
   same shape.

   TWO INEQUALITIES, both removed rather than compensated for:

   1. THE ACTIVE ITEM WAS TALLER. padding-bottom: 3px plus a 1.5px border made
      the selected item 22.75px against everyone else's 18.75px, so whichever
      cross-axis alignment the row used, the active item and the rest could not
      all agree. The underline is a pseudo-element now, outside the box, so the
      box never changes size and align-items has nothing left to disagree about.

   2. THE TRIGGER WAS CENTRED IN A DIFFERENT BOX from the links. It sits inside
      .nav__menu; both that wrapper and the row now stretch to the bar's full
      60px and centre their contents in it, so every one of the five is centred
      against the same 60px and the panel's top: 100% is the bar's own bottom
      edge rather than a distance guessed from the trigger.

   line-height as a RATIO, not the computed 18.75px: the number would be right
   only while the font size is 12.5px. */
/* ⚠ nowrap AND flex-shrink: 0. Without both, the bar wraps inside its own
   60px height and you get "Rhythm / Tutoring" and "Log / out" stacked. */
.nav__links {
  align-self: stretch; flex-shrink: 0; white-space: nowrap;
  display: flex; align-items: center; gap: 22px;
  font-size: 12.5px; font-weight: 500;
}
.nav__link, .nav__menubtn {
  display: inline-flex; align-items: center;
  position: relative;
  margin: 0; padding: 0; border: 0; background: none;
  font-family: inherit; font-size: 12.5px; font-weight: 500;
  line-height: 1.5; vertical-align: baseline;
  color: rgba(255,255,255,.6); text-decoration: none;
}
.nav__link:hover, .nav__menubtn:hover { color: #fff; }
.nav__link--on, .nav__menubtn--on { color: var(--off-white); }
.nav__link--on:hover { color: var(--off-white); }
/* THE UNDERLINE IS DRAWN OUTSIDE THE BOX. As a border with padding it changed
   the item's height, which is what made the active item impossible to align
   with the inactive ones. */
.nav__link--on::after,
.nav__menubtn--on::after,
.nav__menu--open .nav__menubtn::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -4px;
  border-bottom: 1.5px solid var(--gold);
}
/* ⚠ justify-content: flex-end, NEVER margin-left: auto, and this is a real fix
   rather than a restatement. An auto margin absorbs positive free space BEFORE
   flex-grow runs, so a `flex: 1` on the same box never grows: the name
   collapses towards a single character while the bar still has room. The auto
   margin was here and is gone.

   ⚠ AND THE VIEWER'S OWN NAME IS WHAT YIELDS UNDER PRESSURE, not a nav item.
   Your own name is less load-bearing than the section you are in, so the name
   shrinks and truncates while the wordmark, the links, the avatar and Log out
   all refuse to. */
.nav__user { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
.nav__name {
  flex-shrink: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: 12px; color: rgba(255,255,255,.6);
}
.nav__avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--gold); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: var(--green-deep); }

/* ══ THE OUTSTANDING-COUNT BADGE ═══════════════════════════════════════════
   The numeral on the avatar: surveys a tutor owes, or reports an admin has to
   approve. Painted by public/badge.js, never printed by the server.

   THE WRAPPER EXISTS ONLY TO POSITION IT. position:relative is what the badge's
   absolute offsets are measured from, and flex-shrink:0 stops the nav's flex row
   squeezing the avatar when a long name shares the bar.

   TWO DETAILS CARRY THIS AND NEITHER IS DECORATION. The 2px deep-green RING is
   what separates gold-light from the gold avatar directly beneath it: they are
   close in value and without the ring they merge into one blob. And the numeral
   is the SERIF, not the nav's Helvetica, which is the single choice that stops
   this reading as a stock notification dot.

   HIDDEN AT ZERO, by the [hidden] attribute the script sets. display:flex is
   declared on the element, and an author-origin display would override the user
   agent's [hidden]{display:none} exactly as it did on the roster rows in
   c65e5a5, so the hidden rule below restores it explicitly. That is not
   belt-and-braces: without it a tutor with nothing outstanding gets an empty
   gold pill on their avatar. */
.nav__avatarwrap { position: relative; flex-shrink: 0; display: flex; }
.nav__badge {
  position: absolute; top: -5px; right: -6px;
  min-width: 19px; height: 19px; padding: 0 5px; box-sizing: border-box;
  border-radius: 100px;
  background: var(--gold-light);
  border: 2px solid var(--green-deep);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 12px; line-height: 1;
  color: var(--green-deep);
  /* min-width plus horizontal padding is what makes 7, 11 and 99+ all render
     from one set of measurements. */
  white-space: nowrap;
}
.nav__badge[hidden] { display: none; }
/* logout is a POST form button styled as the mock's link */
.nav__logout { flex-shrink: 0; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: 12px; color: rgba(255,255,255,.6); border-bottom: 1px solid rgba(255,255,255,.35); }
.nav__logout:hover { color: #fff; border-color: #fff; }

/* ══ THE ADMIN MENU ════════════════════════════════════════════════════════
   A dropdown attached to the bar, not a card landing on the page: deep green,
   square at the top, flush under the 60px bar. */
/* Stretched to the bar's height for the same reason the row is: it makes the
   trigger's centre the bar's centre, and it makes top: 100% on the panel the
   bar's bottom edge rather than a distance measured from the label. */
.nav__menu { position: relative; align-self: stretch; display: flex; align-items: center; }
/* Everything the trigger shares with a link is declared with the links above.
   What is left here is the two things only a button needs. */
.nav__menubtn { cursor: pointer; gap: 6px; }
/* line-height: 1 so the caret's own line box (9px) can never be the thing that
   sets the trigger's height. Without it a change to the caret's size would move
   the label, which is the one thing the caret must not do. */
.nav__caret { font-size: 9px; line-height: 1; opacity: .75; }
.nav__menu--open .nav__menubtn { color: var(--off-white); }
.nav__menu--open .nav__caret { color: var(--gold-light); opacity: 1; transform: rotate(180deg); }

.nav__panel {
  position: absolute; left: 0; top: 100%; z-index: 40;
  width: 214px; padding: 8px 0;
  background: var(--green-deep);
  /* Square at the top because it is ATTACHED to the bar, rounded below. */
  border-radius: 0 0 6px 6px;
  box-shadow: 0 18px 40px rgba(3,20,10,.34);
}
/* [hidden] is a user-agent rule and loses to any author display. Nothing here
   sets display on .nav__panel, but the guard is written anyway: this is the
   roster incident's exact shape and it is one line. */
.nav__panel[hidden] { display: none !important; }

.nav__item, .nav__item--soon {
  min-height: 42px; padding: 0 20px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 13.5px; color: rgba(255,255,255,.78);
}
.nav__item:hover, .nav__item:focus-visible, .nav__item--on {
  background: rgba(253,252,249,.09);
  /* 20 - 3, so the label does not shift when the bar appears. */
  border-left: 3px solid var(--gold); padding-left: 17px;
  color: var(--off-white); font-weight: 600;
}
.nav__item:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.nav__item--soon { color: rgba(255,255,255,.34); }
.nav__soon {
  font-size: 9px; font-weight: 600; letter-spacing: .1em; color: var(--gold-light);
  border: 1px solid rgba(227,181,58,.45); border-radius: 100px; padding: 2px 8px;
}
/* A DOT, NOT A COUNT. It says only that something is waiting, which is the
   decision it supports; a number invites reading it as a workload and is stale
   the moment somebody else approves one. */
.nav__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--gold-light); flex: none; }

/* ══ THE MOBILE TAB BAR ════════════════════════════════════════════════════
   Hidden above the breakpoint; the desktop link row is hidden below it. */
.tabbar { display: none; }
.sheet { display: none; }

/* ══ THE BREAKPOINT IS A SHAPE, NOT A WIDTH ════════════════════════════════
   ⚠ RULED IN QC 8: A PHONE IS MOBILE IN BOTH ORIENTATIONS.

   Rotated to landscape an iPhone is about 844 x 390. On width alone that
   crosses 761 and the whole app flipped to the desktop layout mid-gesture:
   desktop nav, no tab bar, the create sheet in a scrim as a centred modal. The
   desktop layout is for desktop-CLASS devices, and width alone does not say
   which class a screen belongs to.

   THE MECHANISM, and why it is height rather than a capability query. The
   obvious rekey is `(hover: hover) and (pointer: fine)`, and it gets the iPad
   WRONG: an iPad with no trackpad reports `hover: none` and `pointer: coarse`
   exactly as a phone does, so it would be handed the phone layout on a
   1024-wide screen. The ruling says an iPad at that width still gets desktop.

   SHORT SIDE IT IS. Every phone in landscape is under 500px tall -- 390 for a
   14/15/16, 402 for a Pro, 430 for a Pro Max -- and every desktop-class screen
   in any orientation is well over it: an iPad mini is 744 tall in landscape and
   an 11 inch iPad is 834. One number separates the two classes cleanly and it
   needs no capability query, no script, and no engine newer than Media Queries
   Level 3.

   WHICH WAY THE MARGINS FALL:
     iPhone landscape, 844x390 ......... MOBILE   (the defect this fixes)
     iPhone portrait, 390x844 .......... MOBILE   (unchanged)
     iPad mini portrait, 744x1133 ...... MOBILE   (unchanged; under 761 wide)
     iPad landscape, 1133x744 .......... desktop
     iPad portrait, 820x1180 ........... desktop
     laptop, any normal window ......... desktop
     a laptop window under 500px tall .. MOBILE

   That last row is the one thing this trades away, and it is the right way
   round: the mobile layout works at any width, and a 1440x400 window is a
   shape the desktop layout was never drawn for either.

   PHONE SIDE IS A COMMA, WHICH IS OR: max-width 760 OR max-height 499.
   DESKTOP SIDE IS AND: min-width 761 AND min-height 500. The two are exact
   complements, so no shape can match both or neither. */

@media (max-width: 760px), (max-height: 499px) {
  /* The top bar keeps the lockup and the user block only. */
  .nav__links { display: none; }

  .tabbar {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    background: var(--green-deep);
    border-top: 1px solid rgba(227,181,58,.3);
    /* THE BOTTOM PAD IS NOT A SPACING CHOICE. The bar must reach the physical
       bottom so iOS can draw the home indicator inside it; without the inset the
       icons collide with the home bar. 14px, not the 8px this shipped with: in
       the installed app the labels sat tight against the bottom edge.

       ⚠ THE COMMENT THAT USED TO BE HERE CALLED THIS PAD SUSPECT AND SAID: "if
       the bar detaches again with only this back, the bisect was wrong and this
       is the second trigger." It detached again in QC 5, and the note was half
       right. The pad was never actually "back" in any sense that could be
       tested: with no page declaring viewport-fit=cover the inset resolved to 0
       and the term was inert, so the device pass that followed adjudicated
       nothing. 0c1164c is what armed it, and the detach came with it.

       So the pad is not the second trigger and the nav pad was not the first.
       BOTH ARE THE SAME TRIGGER: an env(safe-area-inset-*) term in the shell's
       layout. It is written --sab here for exactly that reason; the top of this
       file and public/safe-area.js carry the account.

       ⚠ max(), NOT +, AND THAT IS QC 7's SECOND DEFECT. This read
       `calc(14px + var(--sab))`, so a device with a home indicator got
       14 + 34 = 48px of padding under a 52px row: the thick band of dead space
       that was reported. The 14px was tuned in 0e81410 when the inset resolved
       to 0 and the term was inert; 0c1164c armed it and nobody re-did the
       arithmetic. The two are alternatives, not addends -- 14px is the manual
       clearance for a device with no indicator, --sab is the real one for a
       device that has one -- so the larger wins and neither stacks.

       THE BAR'S TOTAL HEIGHT IS 8 + 52 + max(14, --sab): 74px with no inset,
       94px with a 34px one, never 108. THREE OTHER RULES DEPEND ON IT:
       .sheet__scrim, .sheet__panel and the mobile .page padding. Change this
       and all three move with it, or the last row of every page slides under
       the bar. mobile-overflow.test.js derives the other three from this
       declaration so they cannot silently disagree. */
    padding: 8px 6px max(14px, var(--sab));
  }
  .tabbar__tab {
    flex: 1; min-height: 52px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 5px; background: none; border: 0; cursor: pointer; font: inherit;
    color: rgba(255,255,255,.6); text-decoration: none;
  }
  .tabbar__label { font-size: 10px; font-weight: 600; }
  /* The icons inherit currentColor, so active is a colour change and not a
     second asset with a filled variant to keep in step. */
  .tabbar__tab--on { color: var(--gold-light); }
  .tabbar__tab--on .tabbar__label { font-weight: 700; }
  .tabbar__tab:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

  /* BOTH BARS ARE FIVE TABS NOW, so there is no room to spend and the tutor
     variant no longer buys any.

     It used to: a tutor had four tabs to an admin's five, and the spare width
     went on a larger label and a looser gap. Calendar took that spare width.
     Five tabs at 390px is 78px each and at 360px is 72px, and "Student work"
     at 11px no longer sets on one line inside either.

     The class is kept rather than deleted. It is the hook the sheet rules and
     the layout tests already reference, and removing it would be a second
     change to the tab bar in a diff that should carry exactly one. Nothing in
     the bar's geometry moves: the padding, the position and the 74px total are
     untouched, which matters because those three are what the mobile overflow
     test derives from and what the pinning incident turned on. */
  .tabbar--tutor .tabbar__tab { gap: 5px; }

  /* While the sheet is up the previously active tab dims: nothing has navigated
     yet and the bar must not claim otherwise. */
  .tabbar--sheeting .tabbar__tab--on:not(.tabbar__tab--admin) { color: rgba(255,255,255,.35); }
  .tabbar--sheeting .tabbar__tab--on:not(.tabbar__tab--admin) .tabbar__label { font-weight: 600; }

  .sheet { display: block; position: fixed; inset: 0; z-index: 49; }
  .sheet__scrim {
    position: absolute; inset: 0 0 calc(60px + max(14px, var(--sab)));
    background: rgba(20,55,42,.38);
  }
  .sheet__panel {
    position: absolute; left: 0; right: 0;
    bottom: calc(60px + max(14px, var(--sab)));
    background: var(--off-white);
    border-radius: 12px 12px 0 0;
    box-shadow: 0 -20px 44px rgba(20,55,42,.24);
    padding: 12px 0 0;
    display: flex; flex-direction: column;
  }
  .sheet__grab {
    width: 38px; height: 4px; border-radius: 100px;
    background: rgba(27,67,50,.2); align-self: center; margin-bottom: 14px;
  }
  .sheet__eyebrow {
    font-size: 9.5px; font-weight: 600; letter-spacing: .16em;
    color: var(--gold-dark); padding: 0 20px 8px;
  }
  .sheet__row {
    min-height: 52px; padding: 0 20px;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    font-size: 15.5px; color: var(--ink); text-decoration: none;
    border-top: 1px solid rgba(27,67,50,.09);
  }
  .sheet__row--on { font-weight: 700; }
  .sheet__row--soon { color: #A9B1AA; }
  .sheet__soon {
    font-size: 9px; font-weight: 600; letter-spacing: .1em; color: var(--gold-dark);
    border: 1px solid rgba(168,132,42,.4); border-radius: 100px; padding: 2px 8px;
  }
  .sheet__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--gold-dark); flex: none; }
  .sheet__close {
    min-height: 44px; border: 0; border-top: 1px solid rgba(27,67,50,.09);
    background: none; cursor: pointer; font: inherit; font-size: 14px;
    color: var(--green-btn);
  }
}
.sheet[hidden] { display: none !important; }
.tabbar[hidden] { display: none !important; }

.page { max-width: 1180px; margin: 0 auto; padding: 28px 26px 44px; }
.page a { text-decoration: none; }
/* .greet's own type is gone: it takes the shared page-title rule near the top
   of this file with every other top-level title. It was 30px, which was 30px
   for no reason anybody could name. */

/* ── counters ── */
.counters { display: flex; gap: 14px; margin-top: 20px; flex-wrap: wrap; }
.counter { flex: 0 1 300px; min-width: 250px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 18px 20px; }
.counters .counter--accent { background: var(--green-deep); border-color: var(--green-deep); }
.counter--queued { border-left: 3px solid var(--gold); }
.counter__label { font-size: 9.5px; font-weight: 600; color: var(--faint); letter-spacing: .12em; }
.counter--accent .counter__label { color: rgba(255,255,255,.7); }
.counter__row { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
.counter__value { font-family: var(--serif); font-size: 34px; line-height: 1; color: var(--green-deep); margin-top: 6px; }
.counter--accent .counter__value { color: var(--gold-light); }
.counter__jump { font-size: 11.5px; font-weight: 600; color: var(--green-btn); white-space: nowrap; padding-bottom: 1px; border-bottom: 1px solid rgba(201,162,39,.55); }
.counter__jump:hover { color: var(--gold-dark); }
.counter--accent .counter__jump { color: var(--gold-light); border-bottom-color: rgba(227,181,58,.5); }
.counter--accent .counter__jump:hover { color: #fff; border-bottom-color: #fff; }
/* zero state: the number becomes a phrase */
.counter__done { font-family: var(--serif); font-size: 22px; line-height: 1.2; color: var(--gold); margin-top: 8px; }
.counter--accent .counter__done { color: var(--gold-light); }
/* solo counter (tutor view) — one wide card */
.counter--solo { flex: 1 1 100%; max-width: 620px; }
.counter--solo .counter__row { align-items: center; }
.counter--solo .counter__value { font-size: 38px; }

/* ── after every session ──
   The three-step strip, from the Design revision of design/dashboard.html. In
   the markup it sits below the announcements block and above the student list;
   the rules live here, with the other cards that stack under the counters, and
   the mobile rules are in the 760px block below.
   Static and permanent: no dismiss, no expiry, no per-user state, no JS.
   Consumes five tokens, all already in :root: --green-deep, --gold, --gold-light,
   --off-white and --serif. It references --line nowhere, so the mock's divergent
   --line value is not needed and was not imported. */
/* ── AFTER EVERY SESSION ──────────────────────────────────────────────────
   design_handoff_attendance_card. Three columns on a phone too, which is the
   whole change: .flow__steps was already a flex row and .flow__step already
   flex: 1, and ONE phone rule stacked them.

   ⚠ THE SENTENCES HAD TO GO BEFORE THE COLUMNS COULD COME BACK. .flow__note
   was 11.5px prose in a column about 103px wide at 390, which wraps to eight or
   nine lines three times over. It is DELETED rather than restyled, and a two
   word sub label takes its place. Deleting the phone override alone would have
   reproduced exactly the layout that override was written to escape.

   ⚠ THE STEPPER IS THE PORTAL'S OWN VOCABULARY, from .tsteps/.tstep in the
   training banner: 26px discs, gold fill for current, a hairline for pending, a
   1px connector that turns gold once the step behind it is done. That component
   already ships three across on this exact green.

   CONTRAST FLOOR ON THIS CARD, and two values here broke it before this:
   .flow__label was 9.5px against a 10.5px floor, and .flow__sub was
   rgba(255,255,255,.5) against a .6 floor. Both corrected. --faint is a white
   ground token at 3.06:1 and is not reachable for on this card at all. */
.flow { background: var(--green-deep); border-radius: 6px; padding: 18px 22px; margin-top: 14px; }
.flow__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.flow__label { font-size: 10.5px; font-weight: 600; color: var(--gold-light); letter-spacing: .14em; }
.flow__sub { font-size: 11.5px; color: rgba(255,255,255,.62); }
/* The gold rule under the head, which the mock draws in place of the old
   per-step gold border. One line for the card rather than three for the steps. */
.flow__rule { height: 1px; background: var(--gold); margin: 11px 0 20px; }

/* gap: 0 because the connectors ARE the spacing; a gap as well would push the
   columns apart and leave the hairlines short of both discs. */
.flow__steps { display: flex; align-items: flex-start; gap: 0; margin-top: 0; }
.flow__step {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center;
  border-top: 0; padding-top: 0;
}
.flow__no {
  width: 26px; height: 26px; border-radius: 50%; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 13px;
  border: 1px solid rgba(255,255,255,.35); color: rgba(255,255,255,.62);
}
/* CURRENT: gold fill, deep green numeral. DONE: gold numeral on a gold
   hairline. PENDING keeps the base above. */
.flow__step--on .flow__no { background: var(--gold); border-color: var(--gold); color: var(--green-deep); }
.flow__step--done .flow__no { border-color: var(--gold-light); color: var(--gold-light); }
.flow__title {
  margin-top: 9px;
  font-size: 12px; font-weight: 700; line-height: 1.3;
  text-align: center; color: #fff;
}
.flow__substep {
  margin-top: 3px;
  font-size: 10.5px; line-height: 1.35;
  text-align: center; color: rgba(255,255,255,.62);
}
/* 13px is half the disc, so the hairline meets it on its centre line. */
.flow__bar {
  flex: 0 0 20px; height: 1px; margin-top: 13px;
  background: rgba(255,255,255,.22);
}
.flow__bar--done { background: var(--gold); }

/* ── THE WAITING ROW ──────────────────────────────────────────────────────
   The only part of the card that changes from night to night, and the reason it
   is not a poster. It ALWAYS renders: with nothing outstanding it says so. */
.flow__waiting {
  margin-top: 20px; padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,.14);
  display: flex; align-items: center; gap: 12px;
}
.flow__waittext { flex: 1; min-width: 0; font-size: 12.5px; line-height: 1.5; color: rgba(255,255,255,.72); }
.flow__start {
  flex: none; padding: 10px 15px;
  background: var(--gold-light); border-radius: 4px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  color: var(--green-deep); white-space: nowrap;
  border: 0; text-decoration: none;
}
.flow__start:hover { background: var(--gold); color: var(--green-deep); }

/* ── STEP 3'S TICK ────────────────────────────────────────────────────────
   ⚠ A BUTTON, NOT AN <input type="checkbox">. It submits on the tap, because
   there is no JavaScript on this card and a checkbox that needed a second
   control to save it would be a step nobody finishes. It carries aria-pressed
   so the state is not colour alone. */
.flow__tick { margin-top: 12px; }
.flow__tickbtn {
  display: flex; align-items: center; gap: 9px;
  min-height: 44px; padding: 0; margin: 0;
  background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left;
}
.flow__box {
  flex: none; width: 18px; height: 18px; border-radius: 3px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255,255,255,.45);
  font-size: 11px; line-height: 1; color: var(--green-deep);
}
.flow__box--on { background: var(--gold-light); border-color: var(--gold-light); }
.flow__ticklabel { font-size: 12px; color: rgba(255,255,255,.72); }

/* ── section headers ── */
.sectionhead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 26px 0 10px; }
.eyebrow { font-size: 10.5px; font-weight: 600; color: var(--gold-dark); letter-spacing: .14em; }
.sectionhead a { font-size: 12px; border-bottom: 1px solid rgba(201,162,39,.55); white-space: nowrap; }

/* ── tables ── */
.table { background: #fff; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.table__head, .table__row { display: flex; align-items: center; }
.table__head { padding: 10px 18px; background: rgba(27,67,50,.04); border-bottom: 1px solid rgba(27,67,50,.1); font-size: 9.5px; font-weight: 600; color: var(--faint); letter-spacing: .12em; }
.table__head--queue { background: rgba(201,162,39,.07); }
.table__row { padding: 15px 18px; border-bottom: 1px solid rgba(27,67,50,.09); }
.table__row:last-child { border-bottom: 0; }
/* gold left edge means the same thing portal-wide: this row needs you */
.table__row--action { border-left: 3px solid var(--gold); }

.name { font-size: 14px; font-weight: 600; color: var(--ink); }
/* THE STUDENT NAME AS THE HISTORY LINK. On a phone it replaces the History
   anchor beside the button, and there it is underlined like one.

   HERE IT IS DELIBERATELY INVISIBLE AS A LINK. There is one template for both
   widths, and the desktop row is out of scope for that change: it still carries
   History as its own word in the action cell, so a second, differently styled
   route to the same page would be a change to a screen nobody asked to change.
   Every property an anchor would otherwise bring -- the link green, the gold
   hover, an underline -- is put back to what .name renders. */
.link-name, .link-name:hover { color: var(--ink); text-decoration: none; border-bottom: 0; }
.sub { display: block; font-size: 11.5px; font-weight: 400; color: var(--faint); margin-top: 2px; }
.link-history { font-size: 11.5px; color: var(--faint); border-bottom: 1px solid rgba(27,67,50,.2); white-space: nowrap; }
.link-history:hover { color: var(--green-btn); }

/* queue columns */
.qc-report { flex: 1.5; min-width: 0; }
.qc-tutor { flex: 1; min-width: 0; font-size: 13px; color: var(--muted); }
.qc-reason { flex: 1.3; min-width: 0; }
.qc-action { flex: 1.8; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
/* THE PHONE CARD'S TWO EXTRA PIECES, declared here only to be switched OFF.
   The desktop queue row is out of scope and stays a four-column table with its
   HELD BECAUSE pill; both of these are rendered into the markup at every width
   and are given a shape only inside the phone media query. Declared as their
   own rule rather than left to the media query so that a stylesheet read from
   the top says what they are for. */
.qc-band, .qc-concern { display: none; }
/* ⚠ NO BOX ON THE DESKTOP. The wrapper exists only so the phone can place the
   two buttons as one thing; here History, Archive and Review stay three direct
   children of the action cell and the row is the flex layout it always was. */
.qc-buttons { display: contents; }
/* student columns */
.sc-student { flex: 1.5; min-width: 0; }
.sc-parent { flex: 1.2; min-width: 0; font-size: 13px; color: var(--muted); }
.sc-status { flex: 1.8; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
/* Status for a row the viewer cannot act on. Plain text on purpose: no pill and
   no button, so it never reads as something clickable. */
.status-plain { font-size: 12.5px; color: var(--muted); }
/* The one red thing on the roster. Reserved for "we could not tell the tutor",
   which is the only state on this table that no other label makes visible. */
.status-warn {
  font-size: 9.5px; font-weight: 600; letter-spacing: .08em; white-space: nowrap;
  padding: 4px 9px; border-radius: 100px;
  color: var(--danger); border: 1px solid #fecdca; background: rgba(180,35,24,.06);
}

/* ── buttons + chips ── (anchors styled as buttons; no inline JS) */
.btn-review, .btn-survey { display: inline-block; padding: 10px 18px; background: var(--green-btn); border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; flex-shrink: 0; cursor: pointer; transition: background .15s; text-align: center; }
.btn-review { border: 1.5px solid var(--gold); }
.btn-survey { border: 0; }
.btn-review:hover, .btn-survey:hover { background: var(--green-deep); color: #fff; }
/* The short label is the phone's; see the media query at the foot of this block. */
.btn-review__short { display: none; }

/* ARCHIVE. Deliberately quieter than Review: it is the exception, and the two
   sitting side by side must not read as an even choice. Outline rather than
   fill, and it never borrows the danger red -- archiving loses nothing and is
   undoable from the toast. */
.btn-archive {
  display: inline-block; padding: 10px 16px; background: transparent;
  border: 1px solid rgba(27,67,50,.28); border-radius: 4px;
  font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: var(--green-btn); white-space: nowrap; flex-shrink: 0; cursor: pointer;
  transition: background .15s, border-color .15s; text-align: center;
}
.btn-archive:hover { background: rgba(27,67,50,.06); border-color: var(--green-btn); }
.btn-archive[disabled] { opacity: .55; cursor: default; }
/* The form is only there so Archive still works with no script. It has to
   participate in the action row as one flex item, or the inline default leaves
   the button sitting on its own baseline out of line with its neighbours.
   display:contents would also work and is not used: it has a history of
   dropping form semantics from the accessibility tree, and a flex item costs
   nothing. */
.qc-action form.inline { display: flex; margin: 0; }
.chip { padding: 7px 13px; border-radius: 100px; font-size: 10.5px; font-weight: 600; letter-spacing: .04em; white-space: nowrap; }
.chip--reason { background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); color: var(--green-deep); font-size: 9.5px; letter-spacing: .06em; padding: 5px 11px; }
/* PARENT OPTED OUT. Informational, so it is deliberately the quietest chip on
   the row: no gold, no red. Gold would compete with the routing reason beside
   it, which is the chip that actually says why the report is being held, and red
   would read as a fault. It says why an admin would archive rather than approve,
   and nothing else in the system reads it. */
/* The Parent-opted-out checkbox on the Edit student form. Scoped to .page--admin
   in the same spirit as the rest of that page's names. */
.page--admin .optout {
  display: flex; align-items: flex-start; gap: 11px; margin-top: 20px;
  padding: 14px 15px; border: 1px solid var(--line); border-radius: 5px;
  background: rgba(27,67,50,.03); cursor: pointer;
}
.page--admin .optout__box { margin: 2px 0 0; width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--green-btn); cursor: pointer; }
.page--admin .optout__text { display: block; min-width: 0; }
.page--admin .optout__title { display: block; font-size: 13px; font-weight: 600; color: var(--ink); }
.page--admin .optout__note { display: block; font-size: 11.5px; line-height: 1.6; color: var(--faint); margin-top: 4px; }

.chip--optout {
  background: rgba(27,67,50,.06); border: 1px solid rgba(27,67,50,.18);
  color: var(--muted); font-size: 9.5px; letter-spacing: .06em; padding: 5px 11px;
}
/* Two chips on one line, wrapping only when there is genuinely no room. */
.qc-reason { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.chip--review { background: rgba(27,67,50,.08); color: var(--green-btn); }
.chip--sent { background: rgba(27,67,50,.08); color: var(--faint); }

/* overflow row under a capped queue table */
.overflow { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 18px; background: rgba(201,162,39,.07); border-top: 1px solid rgba(27,67,50,.1); font-size: 12.5px; color: var(--muted); }
.overflow a { font-weight: 600; white-space: nowrap; border-bottom: 1px solid rgba(201,162,39,.55); }

/* zero state for a section */
.empty { background: var(--off-white); border: 1px solid var(--line); border-left: 3px solid rgba(27,67,50,.2); border-radius: 6px; padding: 18px 20px; font-size: 13px; line-height: 1.65; color: var(--muted); }

@media (max-width: 760px), (max-height: 499px) {
  /* MOBILE NAV, TWO ROWS.
     The links used to be display:none here with nothing put in their place, so
     Dashboard, Queue, Sent and Admin were simply unreachable on a phone. They now
     move to a second row inside the same green bar instead of disappearing.

     Row order comes from `order`, not from moving anything in the markup: the DOM
     is lockup, links, user, and the links are sent last so they wrap onto their
     own line. Log out therefore stays exactly where it is, on the top row at the
     right, on all 14 pages that carry this header.

     No JavaScript and none needed: this is flex-wrap plus order. */
  /* `gap: 0 12px`, not `gap: 12px`. A single value sets the ROW gap too, which
     put 12px of green between the top row and the links row on top of everything
     else below. The columns still need their 12px; the rows do not, because the
     seam under the top row is what separates them. */
  /* THE TOP PAD IS THE STATUS BAR, and it is the mirror of the tab bar's bottom
     one. Reported from the installed app: the first line of page content sat
     under the clock, which is what it looks like when the web view runs edge to
     edge and nothing reserves the band iOS draws the status bar in.

     ON THE NAV RATHER THAN ON THE PAGE, because the nav is the top of every
     signed-in screen and a per-page pad would have to be repeated on fifty
     views and would still leave the green bar itself under the clock. Padding
     it puts the inset INSIDE the green, so the status bar reads as part of the
     bar rather than as a strip above it, and every page below it moves down
     with the bar for free.

     env() IS ZERO WHERE THERE IS NO INSET -- a browser tab, a desktop window,
     any device without a reserved band.

     REVERTED WITH THE TAB BAR, while a shell regression is bisected. The two
     min-height rules that subtracted this same term (.page--deck .lessonwrap and
     .fwwrap) come back with it, or they reserve a band the nav no longer has.
     Note that page content riding up under the status bar is one of the reported
     symptoms, so this pad was aimed at something real; it is being taken out to
     find out whether it is also implicated, not because it was wrong. */
  .nav { padding: 0 16px; gap: 0 12px; height: auto; flex-wrap: wrap; }
  .nav__name { display: none; }
  /* 32px on mobile, per the handoff's mobile strip: the badge grows to 20px
     with it, so the ring stays the same weight against a slightly larger
     avatar. The 44px min-height on this row is measured from the avatar, and
     32px still sits inside it. */
  .nav__avatar { width: 32px; height: 32px; }
  .nav__badge { min-width: 20px; height: 20px; font-size: 12.5px; }
  .nav__lockup { order: 1; }
  .nav__user { order: 2; }
  /* min-height on the two top-row items, not on .nav, so the bar itself grows to
     fit the second row.
     44px, down from 60px. The tallest thing on this row is the 30px avatar, so at
     60px there were 15px of dead green above and below it, and that padding read
     as a gap between the two rows rather than as breathing room around the row's
     own content. 44px is the tap-target floor, so this is as tight as the row
     goes without making the lockup harder to hit. */
  .nav__lockup, .nav__user { min-height: 44px; }
  /* Log out is the other top-row target, and it was never actually 44px: the
     min-height sat on the div around it, while the button's own box was only as
     tall as its text. Padding gives it the full 44px.
     The underline then has to leave the box edge, for the same reason the nav
     links below take a pseudo-element: at 44px tall a border-bottom sits at the
     bottom of the tap target, well clear of the word. Same .72em offset as the
     links, so both underlines are drawn by one rule of thumb. */
  .nav__logout { padding: 13px 0; border-bottom: 0; position: relative; }
  .nav__logout::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    margin-top: .72em;
    border-bottom: 1px solid rgba(255,255,255,.35);
  }
  .nav__logout:hover::after { border-bottom-color: #fff; }

  /* THE SECOND ROW IS GONE, and with it the wrapping, the seam and the
     pseudo-element underline that made a 44px tap target look like a link.
     Below the breakpoint there is no link row at all: the tab bar at the bottom
     of the screen is the navigation, and .nav__links is display:none.

     Left in place these rules would have WON. They live in a later media block
     than the one that hides the row, at equal specificity, so the row kept
     rendering under the top bar while every source-level test said it was
     hidden. Deleted rather than overridden. */

  .page {
    padding: 22px 16px 36px;
    /* THE BAR'S HEIGHT PLUS THE HOME INDICATOR'S BAND. 8 + 52 + 14 is the bar
       itself; the inset is what iOS draws the indicator in. Without this the
       last row of every page sits under the bar and cannot be tapped.
       Declared here, in the media block that comes last, because a shorthand
       further down would otherwise reset it. */
    padding-bottom: calc(60px + max(14px, var(--sab)));
  }
  /* .greet's mobile 24px is gone with its desktop 30px. It was the ONLY page
     title with a phone size of its own: Student work, Training, Competency and
     Admin tools have never had one and render at 28px on a phone today, so a
     dashboard at 24 was the same inconsistency as the desktop one, one
     breakpoint down. The shared rule governs at both widths. */
  .counters { gap: 10px; }
  .counter { flex: 1 1 100%; min-width: 0; padding: 15px 16px; }
  .counter__value { font-size: 28px; }
  /* The three steps stack. Above 760px each step is a column with a gold rule
     across its top; stacked, that rule on every step would read as three
     unrelated cards, so only the joins between steps keep a hairline and the
     gold rule stays on the first step alone. */
  .flow { padding: 16px; }
  /* ⚠ THE STACK IS GONE, AND THAT IS THE CHANGE. This used to be
     `flex-direction: column` with a hairline between steps, because three
     .flow__note paragraphs could not survive a 103px column. The notes are
     deleted, so the columns come back -- the same three the desktop has always
     rendered, and the same shape .tstep already holds at this width. */
  .flow__steps { flex-direction: row; }
  /* A 20px connector is too wide once the columns are ~103px; 12 keeps three
     labels readable and still reaches both discs. */
  .flow__bar { flex-basis: 12px; }
  .table__head { display: none; }
  .table__row { flex-direction: column; align-items: stretch; padding: 14px 16px; }
  .qc-tutor, .sc-parent { font-size: 12px; margin-top: 4px; }
  .qc-reason { margin-top: 8px; }
  .qc-action, .sc-status { justify-content: flex-start; margin-top: 12px; gap: 12px; flex-wrap: wrap; }
  .btn-review, .btn-survey { flex: 1 1 auto; text-align: center; }

  /* ── THE STUDENT ROW ON A PHONE ───────────────────────────────────────
     NAME LEFT, ACTION RIGHT, ONE LINE, where it was four stacked ones.
     .table__row goes to a column above, so .sc-status took a whole line to
     itself and the rule right above this one stretched the button edge to edge
     under the names. A hundred and twenty-seven pixels a card, and one and a
     half students above the fold. The queue row fixed the same fault for itself
     below; this table never got one.

     KEYED ON --student RATHER THAN :not(--queue). .table__row is shared with
     the library's collection and topic tables, which set their own display:grid
     at a lower specificity than a :not() would carry, so that selector would
     relay both of them on a phone. An opt-in class cannot reach a row that did
     not ask for it. */
  .table__row--student {
    display: flex; flex-direction: row; flex-wrap: wrap; align-items: center;
    /* COLUMN GAP ONLY. The wrap is for the admin's parent line, which sits 3px
       under the name rather than 14, and a row gap here would be that space. */
    gap: 0 14px;
  }
  .table__row--student > .sc-student { flex: 1; min-width: 0; }
  .table__row--student > .sc-status {
    flex: none; justify-content: flex-end; margin-top: 0; flex-wrap: nowrap;
  }
  /* The name IS the History link here, so the separate word for it goes. Same
     href and same gate; see the note on .link-name above. */
  .table__row--student > .sc-status .link-history { display: none; }
  .table__row--student .link-name { border-bottom: 1px solid rgba(27,67,50,.25); }
  .table__row--student .btn-survey { flex: 0 0 auto; white-space: nowrap; }
  /* THE PARENT NAME IS AN ADMIN'S ON THIS SCREEN. A tutor's list is entirely
     their own students, so it never decided which row they tap. An admin is
     scanning across families, so it keeps its place, on its own line under the
     name rather than squeezing the one the action sits on. */
  .table__row--student > .sc-parent { display: none; }
  /* ⚠ THE GAP IS THE NAME'S LINE BOX, NOT THIS MARGIN, which is why 3px read as
     far more than 3px. .sc-student is a flex ITEM of a wrapped row, so it takes
     the row's line-height (1.5 from body) around a 13.5px name: about 7px of
     leading under the name before this margin is added at all. Pinning the name
     to its own text kills that dead space, and then 2px here is the same rhythm
     the Filed line uses under a queue card's name. */
  .table__row--student > .sc-student { line-height: 1.25; }
  .table__row--student > .sc-parent--admin {
    display: block; flex: 1 0 100%; order: 1; margin-top: 2px; line-height: 1.3;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* THE LINK-FAILED ROW is the one exception, and CSS alone cannot fix it:
     .status-warn renders INSIDE .sc-status, beside the button, so it cannot be
     lifted onto a line of its own from outside that cell. At 169px it would
     squeeze the name to nothing. On a row carrying one, the whole action cell
     drops to its own full-width line instead, badge first, then the button,
     left aligned. Rare row, and the full wording is kept.

     ORDER 2 puts it below the admin's parent line rather than between that and
     the name. The base rule already wraps, so this adds no flex-wrap. */
  .table__row--student:has(.status-warn) > .sc-status {
    flex: 1 0 100%; order: 2; justify-content: flex-start; flex-wrap: wrap;
    margin-top: 12px;
  }
  .table__row--student .status-warn { order: -1; flex: 0 0 100%; }

  /* ⚠ THE BUTTON IS CENTRED IN EVERY WAITING CARD, INCLUDING THE FAILED ONE.
     On an ordinary row it already sat on the single line and read as centred.
     On the link-email-failed row the whole action cell drops to a full-width
     line (above), so the button landed at the bottom-left under the badge and
     was the one card in the section whose action was somewhere else.
   *
   * Absolute for the same reason as the queue card's pair: align-self would
   * centre it within its own wrapped line rather than against the card. The row
   * reserves the width, so the name and the badge cannot run under it. */
  .table__row--student:has(.btn-survey) { position: relative; padding-right: 150px; }
  .table__row--student .btn-survey {
    position: absolute; right: 15px; top: 50%; transform: translateY(-50%);
    flex: 0 0 auto; white-space: nowrap;
  }
  /* With the button out of flow the failed row's cell holds only the badge, so
     its top margin is the badge's own and the 12px above would double it. */
  .table__row--student:has(.status-warn):has(.btn-survey) > .sc-status {
    margin-top: 8px;
  }

  /* ── THE QUEUE ROW ON A PHONE ─────────────────────────────────────────
     The routing pill, History, Archive and Review are ONE line at the foot of
     the card. They were four stacked blocks: .table__row goes column above, so
     .qc-reason and .qc-action each took a full line and the buttons inside
     .qc-action wrapped again under their own flex-wrap. Four rows of chrome
     under two lines of content, and the card no longer read as one thing.

     Only the QUEUE row is changed. The student table below shares .table__row
     and is right to stack: its cells are content, not a set of controls that
     belong together.

     The long Review label is swapped for the short one here. Measured at 360px,
     the narrowest width worth designing for: the four controls fit with room to
     spare, where the long label alone would not. Below that the pill ellipses
     rather than breaking the line, which keeps the buttons where the thumb
     expects them at any width. */
  .table__row--queue { flex-direction: row; flex-wrap: wrap; align-items: center; }
  .table__row--queue > .qc-report { flex: 1 1 100%; }
  /* LINE THREE: who filed it, then History, then the two buttons at the right.
     A time and a full tutor name will not share the left column, which is why
     the tutor is here with History rather than on the filed line. */
  /* ⚠ 4px, THE CARD'S OWN LINE RHYTHM. This was 10px, which read as a break
     between two halves of a card rather than as the next line of one. The name
     and its Filed line sit 2px apart (.sub), and the admin's parent line 3px;
     this is the same rhythm, not a gap. */
  .table__row--queue > .qc-tutor {
    flex: 0 1 auto; min-width: 0; margin-top: 4px; margin-right: 8px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .table__row--queue > .qc-action {
    flex: 1 1 auto; justify-content: flex-start; margin-top: 4px;
    gap: 8px; flex-wrap: nowrap; min-width: 0;
  }

  /* ⚠ THE BUTTONS ARE CENTRED AGAINST THE WHOLE CARD, not parked at the foot of
     its last line. They were bottom-right: the card is three lines of content
     and the buttons rode the third, so they read as an afterthought under the
     text rather than as the card's action.
   *
   * ABSOLUTE, BECAUSE FLEX CANNOT DO IT. align-self centres an item within its
   * flex LINE, and these are on a wrapped line of their own, so it would centre
   * them against themselves. Taking them out of flow is the only way to centre
   * against the card's full height.
   *
   * The row reserves the width they occupy, so nothing can run underneath them.
   * History stays in the flow with the tutor, which is the line it belongs to. */
  .table__row--queue { position: relative; padding-right: 128px; }
  .table__row--queue > .qc-action > .qc-buttons {
    display: flex; gap: 8px; align-items: center;
    position: absolute; right: 15px; top: 50%; transform: translateY(-50%);
  }
  /* The separator between the tutor and History. A flex item of the action
     cell rather than markup, and outside the anchor, so the underline on
     History does not run under it. */
  .table__row--queue > .qc-action::before {
    content: "\00B7"; flex: none; color: var(--faint); font-size: 11px;
  }
  /* Archive and Review right, whatever the tutor name did to the left of them.
     The form is the first of the two; see the note on .qc-action form.inline. */
  .table__row--queue > .qc-action form.inline { margin-left: auto; }

  /* THE HELD BECAUSE PILL IS THE DESKTOP'S. It carries route_reason, which is
     the router's FIRST match: with auto-send off, rule 1 catches every report
     before any other can fire, so the pill reads AUTO-SEND OFF on every card in
     the list. That is true of every report in an approval queue -- it is there
     because it did not auto-send -- so on a card it is a line that says
     nothing, and it was costing the width the two buttons needed.

     The concern line below is what a phone card carries instead, and it is
     read off the report rather than off route_reason. See
     services/reasons.concernFor.

     PARENT OPTED OUT IS NOT A ROUTING REASON and does not go with it. It says
     why an admin would archive this rather than approve it, which is worth the
     line, so the cell comes back for the rows that carry one. */
  .table__row--queue > .qc-reason { display: none; }
  /* ORDER 1 puts it BELOW the action line rather than between the tutor and it,
     which is where the markup would otherwise place a full-width cell: that
     split the last line in three and left History stranded under the chip. It
     is a footnote to the row, and it reads as one at the foot of it. */
  .table__row--queue > .qc-reason--optout {
    display: flex; flex: 1 0 100%; order: 1; margin-top: 10px; flex-wrap: nowrap;
  }
  .table__row--queue > .qc-reason--optout .chip--reason { display: none; }
  .table__row--queue > .qc-reason--optout .chip {
    min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }

  /* ── THE CONCERN, AND THE TWO FAILURES ────────────────────────────────
     FOUR REASONS ARE WORTH A LINE on this card and the rest are not. A dot and
     a label, at the eyebrow's own metrics, above the name. The label appears
     ALONE: a label plus its value runs about 175px at 10.5px against a left
     column of 146px once the buttons are placed, so the confidence score, the
     flag type and the grade comparison stay on the review screen, which is
     where somebody acts on them. */
  .table__row--queue > .qc-concern {
    display: flex; align-items: center; gap: 7px; min-width: 0;
    flex: 1 0 100%; order: -1; margin-bottom: 5px;
  }
  /* ⚠ CENTRED ON THE CAP, NOT ON THE LINE BOX. align-items:center centres the
     dot against the full line box, and a line box reserves descender space that
     an ALL-CAPS label never uses, so the dot sat visibly below the middle of the
     letters. Lifting it by half that unused depth puts it on the optical centre
     of the caps.

     THE em IS THE LABEL'S, which is why the font-size is restated: without it
     the em would be the row's 13px and the correction would be a third too big.
     Same pattern for any status dot beside an eyebrow. */
  .qc-concern__dot {
    width: 5px; height: 5px; border-radius: 50%; background: var(--gold); flex: none;
    font-size: 10.5px;
    transform: translateY(-0.1em);
  }
  .table__row--queue > .qc-concern .eyebrow {
    line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* A BAND, NOT A PILL, and it takes the full width of the card. These are the
     longest labels in the vocabulary and a pill would have to truncate them;
     clipping the loudest row on the page is the one outcome worth spending
     height on. The inversion is design/queue.html's, unchanged: deep green
     with gold-light text for a send that failed, and the two swapped for the
     report that went out with no human in the loop and THEN failed. The
     negative margin is the row's own padding, so the band reaches the card's
     edges and stops at the coloured rule. */
  .table__row--queue > .qc-band {
    display: block; flex: 1 0 100%; order: -2;
    margin: -14px -16px 12px; padding: 6px 16px;
    background: var(--green-deep); color: var(--gold-light);
    font-size: 10.5px; font-weight: 600; letter-spacing: .14em; line-height: 1.2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .table__row--queue > .qc-band--critical { background: var(--gold-light); color: var(--green-deep); }
  /* The row behind the band. --action's 3px gold edge is what these replace:
     the row does not need you the way an ordinary held report does, it needs
     you differently. */
  .table__row--fail {
    background: rgba(20,55,42,.05);
    border-left-width: 5px; border-left-color: var(--green-deep);
  }
  .table__row--critical {
    background: rgba(227,181,58,.13);
    border-left-width: 5px; border-left-color: var(--gold-light);
  }
  .table__row--queue .btn-review__long { display: none; }
  .table__row--queue .btn-review__short { display: inline; }
  .table__row--queue .btn-review,
  .table__row--queue .btn-archive {
    flex: 0 0 auto; padding: 9px 13px; font-size: 11.5px;
  }
  .table__row--queue .link-history { flex: 0 0 auto; font-size: 11px; }
}

/* ── Sign-in page (self-contained two-panel brand layout) ────────────── */
.login { display: flex; min-height: 100vh; }

.login__brandpanel {
  flex: 1; background: var(--green-deep); color: #fff;
  padding: 44px 42px; display: flex; flex-direction: column;
  position: relative; overflow: hidden;
}
.login__brandpanel::before,
.login__brandpanel::after { content: ""; position: absolute; border-radius: 50%; pointer-events: none; }
.login__brandpanel::before { right: -40px; bottom: -40px; width: 170px; height: 170px; border: 1.5px solid rgba(227,181,58,.11); }
.login__brandpanel::after { right: -90px; bottom: -90px; width: 280px; height: 280px; border: 1.5px solid rgba(227,181,58,.16); }

.login__lockup { position: absolute; top: 44px; left: 42px; display: flex; align-items: center; z-index: 1; }
.login__wordmark { font-family: var(--serif); font-size: 17px; font-weight: 700; color: var(--off-white); }
.login__wordmark em { font-weight: 400; color: var(--gold-light); }
.login__wordmark-div { color: rgba(255,255,255,.35); font-weight: 400; margin: 0 2px; }
.login__wordmark-sub { font-family: var(--sans); font-size: 12px; font-weight: 600; color: rgba(255,255,255,.7); letter-spacing: .04em; }

.login__pitch { margin: auto 0; position: relative; z-index: 1; }
.login__eyebrow { font-size: 10.5px; font-weight: 600; color: var(--gold-light); letter-spacing: .18em; }
.login__headline { font-family: var(--serif); font-size: 30px; line-height: 1.25; color: var(--off-white); margin: 12px 0 0; max-width: 340px; font-weight: 400; }
.login__sub { font-size: 13.5px; line-height: 1.7; color: rgba(255,255,255,.7); margin-top: 12px; max-width: 330px; }

.login__stats { display: flex; gap: 32px; margin-top: 34px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.14); }
.login__statnum { font-family: var(--serif); font-size: 26px; color: var(--gold-light); }
.login__statlabel { font-size: 11px; line-height: 1.5; color: rgba(255,255,255,.6); margin-top: 2px; }

.login__formpanel { flex: 1; background: var(--cream); padding: 52px 46px; display: flex; flex-direction: column; justify-content: center; }
.login__form { width: 100%; max-width: 380px; margin: 0 auto; }
.login__title { font-family: var(--serif); font-size: 26px; color: var(--ink); margin: 0; font-weight: 400; }
.login__title em { color: var(--green-btn); font-style: normal; }
.login__welcome { font-size: 13px; color: var(--faint); margin-top: 6px; }

.login__label { display: block; font-size: 11.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .04em; margin: 26px 0 7px; }
.login__label + .login__label { margin-top: 16px; }
.login__input {
  width: 100%; border: 1px solid var(--line); border-radius: 4px;
  padding: 13px 14px; font-family: var(--sans); font-size: 16px; background: #fff; color: var(--body);
}
.login__input::placeholder { color: #A9B2AB; }
.login__input:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(201,162,39,.15); }

.login__button {
  width: 100%; margin-top: 24px; padding: 15px;
  background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 2px;
  font-family: var(--sans); font-size: 13.5px; font-weight: 600; color: #fff;
  letter-spacing: .06em; cursor: pointer; transition: background .15s;
}
.login__button:hover { background: var(--green-deep); }
.login__help { text-align: center; font-size: 12px; color: var(--faint); margin-top: 18px; }
.login__error {
  background: #fff; border: 1px solid rgba(168,132,42,.4); border-left: 3px solid var(--gold);
  border-radius: 4px; padding: 11px 13px; font-size: 12.5px; color: var(--body); margin-top: 18px;
}

@media (max-width: 760px), (max-height: 499px) {
  .login { flex-direction: column; min-height: 100vh; }
  .login__brandpanel { flex: 0 0 auto; padding: 26px 24px 30px; }
  .login__lockup { position: static; }
  .login__brandpanel::before, .login__brandpanel::after { display: none; }
  .login__pitch { margin: 24px 0 0; }
  .login__headline { font-size: 24px; max-width: none; }
  .login__sub { font-size: 12.5px; max-width: none; margin-top: 10px; }
  .login__stats { gap: 24px; margin-top: 22px; padding-top: 16px; }
  .login__statnum { font-size: 22px; }
  .login__formpanel { flex: 1; padding: 32px 24px 44px; }
}

/* ══ Survey (standalone brand layout — from the design mock) ═══════════ */
/* Reuses .nav / .page / .eyebrow from the dashboard section above.
   Rail cards use .scard (NOT .card) so they can't collide with the shared
   .card component used on the admin/error/queue pages. */
.crumb { font-size: 12px; color: var(--faint); }
.crumb b { color: var(--green-deep); font-weight: 600; }
.cols { display: flex; gap: 26px; margin-top: 18px; align-items: flex-start; }
.cols__form { flex: 1.9; min-width: 0; }
.cols__rail { flex: 1; min-width: 0; }

.survey-title { font-family: var(--serif); font-size: 27px; color: var(--ink); font-weight: 400; margin: 5px 0 0; }
.lede { font-size: 12.5px; color: var(--faint); margin: 3px 0 0; }

.q { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 20px 22px; margin-top: 12px; }
.q:first-of-type { margin-top: 18px; }
.q__head { display: flex; align-items: center; gap: 11px; }
.q__num { width: 26px; height: 26px; border-radius: 50%; background: var(--green-deep); color: var(--gold-light); font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.q__label { font-size: 15px; font-weight: 600; color: var(--green-deep); }
.q__help { font-size: 12px; line-height: 1.55; color: var(--faint); margin: 5px 0 11px 37px; }

/* Tinted fill + an explicit border so each input reads as an editable box on the
   white question card. Both come from tokens (--field-bg, --field) that the
   session length and grade range tiles share, because a tile is something the
   tutor fills in and has to read as the same kind of surface as a text box.
   One source for each value, so the two cannot drift apart again. */
.field { width: 100%; border: 1px solid var(--field); border-radius: 4px; background: var(--field-bg); padding: 12px 13px; font-family: var(--sans); font-size: 16px; line-height: 1.6; color: var(--body); }
textarea.field { min-height: 58px; resize: vertical; display: block; }
textarea.field--tall { min-height: 86px; }
.field:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(201,162,39,.15); }

/* The grid the grade range tiles sit in. The tiles themselves are buttons, styled
   further down with the rest of the range picker. */
/* NINE ACROSS, and the gap pays for the ninth: 8px to 6px keeps the card the
   same width and the cell at 46px with 15px serif type. A ninth of the ~415px
   card is about 40px usable, and "A+" is two glyphs like "A-" and "B+", so it
   needs no shrink and no special label. The bottom cell is already the short
   FLOOR_BUCKET_CELL_LABEL, chosen when an eighth was tight, and survives a
   ninth unchanged. */
.grades { display: grid; grid-template-columns: repeat(9, 1fr); gap: 6px; }

/* private note: staff-only, visually quarantined */
.q--private { background: var(--green-deep); border-color: var(--green-deep); }
.q--private .q__num { background: var(--gold-light); color: var(--green-deep); }
.q--private .q__label { color: var(--off-white); }
.q--private .q__help { color: var(--gold-light); }
.q__flag { margin-left: auto; padding: 5px 11px; border-radius: 100px; border: 1px solid rgba(227,181,58,.45); font-size: 9.5px; font-weight: 600; color: var(--gold-light); letter-spacing: .1em; white-space: nowrap; }
.field--private { background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.24); color: rgba(255,255,255,.9); }
.field--private::placeholder { color: rgba(255,255,255,.45); }
.field--private:focus { border-color: var(--gold-light); box-shadow: 0 0 0 3px rgba(227,181,58,.2); }

/* right-rail cards (.scard, not .card) */
.scard { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 18px 20px; }
.scard + .scard { margin-top: 12px; }
.scard__label { font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .13em; }
.meta { font-size: 12.5px; color: var(--body); margin-top: 10px; }
.meta__row { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid rgba(27,67,50,.1); }
.meta__row:last-child { border-bottom: 0; }
.meta__key { color: var(--faint); }
.meta__val { font-weight: 600; }
.steps { font-size: 12.5px; line-height: 1.65; color: var(--muted); margin-top: 8px; }
.steps__row { padding: 7px 0; border-bottom: 1px solid rgba(27,67,50,.1); }
.steps__row:last-child { border-bottom: 0; }
.steps__n { font-family: var(--serif); color: var(--gold); margin-right: 6px; }

.btn-submit { width: 100%; padding: 15px; background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px; font-family: var(--sans); font-size: 13.5px; font-weight: 600; color: #fff; letter-spacing: .06em; cursor: pointer; transition: background .15s; }
.btn-submit:hover { background: var(--green-deep); }
.submit__note { font-size: 11px; line-height: 1.55; color: var(--faint); text-align: center; margin: 8px 0 0; }

.flash { background: #fff; border: 1px solid rgba(168,132,42,.4); border-left: 3px solid var(--gold); border-radius: 4px; padding: 11px 13px; font-size: 12.5px; color: var(--body); margin-top: 16px; }
/* The admin's send-back note, set apart from the sentence carrying it. All three
   treatments are stated explicitly rather than relying on the element default,
   so the note reads the same wherever it is placed. */
.flash__note { font-weight: 700; font-style: italic; text-decoration: underline; }
.flash strong { color: var(--green-deep); }

@media (max-width: 980px) {
  .cols { flex-direction: column-reverse; }
  .cols__form, .cols__rail { flex: 1 1 auto; width: 100%; }
  /* THREE, NOT FOUR, AND THE ORPHAN IS THE REASON. Four columns gave eight
     cells two clean rows; nine gives four, four and ONE CELL ALONE on a third
     row, which reads as a bug and puts the bottom bucket where the eye does not
     go. Three is three rows of three exactly, and at 340px each cell is about
     108px, wider than the four-column layout ever was. Three at this breakpoint
     is already the pattern .mins uses. */
  .grades { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 760px), (max-height: 499px) {
  .survey-title { font-size: 22px; }
  .q { padding: 15px 16px; }
  .q__label { font-size: 14px; }
  .q__help { margin-left: 0; margin-top: 6px; }
  .q__flag { font-size: 9px; padding: 4px 9px; }
  .grades { gap: 7px; }
  .scard--submit { position: sticky; bottom: 12px; }
}

/* ══ Approval screen (standalone brand layout — from the design mock) ═══════
   Reuses .nav*, .page, .crumb, .cols, .cols__rail, .flash, .scard, .meta* from
   the sections above. Rail cards use .scard (NOT .card) so they cannot collide
   with the shared .card component on the admin/error/queue-list pages, and the
   two classes whose survey versions differ (.field, .grades) are scoped to
   .page--approve so each page keeps its own treatment. */
.head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-top: 16px; }
.head__title { font-family: var(--serif); font-size: 26px; color: var(--ink); font-weight: 400; margin: 0; }
.head__meta { font-size: 12.5px; color: var(--faint); margin: 3px 0 0; }
.chip-draft { padding: 6px 12px; border-radius: 100px; background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); font-size: 10.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; white-space: nowrap; }

.cols__draft { flex: 1.7; min-width: 0; }

/* 1 · why this needs review — the single rule that held the report */
.reason { background: var(--green-deep); border-radius: 6px; padding: 16px 20px; }
.reason__label { font-size: 10px; font-weight: 600; color: var(--gold-light); letter-spacing: .13em; }
.reason__row { display: flex; align-items: flex-start; gap: 12px; margin-top: 10px; }
.reason__rule { flex-shrink: 0; padding: 5px 11px; border-radius: 100px; background: rgba(227,181,58,.16); border: 1px solid rgba(227,181,58,.45); font-size: 9.5px; font-weight: 600; color: var(--gold-light); letter-spacing: .08em; white-space: nowrap; }
.reason__text { font-size: 13px; line-height: 1.6; color: rgba(255,255,255,.85); min-width: 0; }
.reason__text b { color: #fff; font-weight: 600; }

/* 2 · flags */
.flags { background: #fff; border: 1px solid rgba(168,132,42,.4); border-left: 3px solid var(--gold); border-radius: 6px; padding: 16px 18px; margin-top: 12px; }
.flags__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.flags__label { font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .13em; }
.flags__count { font-size: 11px; color: var(--faint); white-space: nowrap; }
.flag { display: flex; align-items: flex-start; gap: 12px; padding: 11px 0; border-bottom: 1px solid rgba(27,67,50,.1); }
.flag:last-child { border-bottom: 0; padding-bottom: 0; }
.flag__type { flex-shrink: 0; padding: 5px 11px; border-radius: 100px; background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); font-size: 9.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .08em; white-space: nowrap; }
.flag__note { font-size: 12.5px; line-height: 1.6; color: var(--body); min-width: 0; }
.flags--clear { border-color: var(--line); border-left-color: rgba(27,67,50,.2); background: var(--off-white); }
.flags--clear .flags__label { color: var(--faint); }
.flags__clear-note { font-size: 12.5px; line-height: 1.6; color: var(--muted); margin: 8px 0 0; }

/* 3 · tutor's original answers (<details> — no JS, draft stays on screen) */
.compare { background: #fff; border: 1px solid var(--line); border-radius: 6px; margin-top: 12px; }
.compare > summary { list-style: none; cursor: pointer; padding: 14px 18px; display: flex; align-items: center; gap: 10px; font-size: 12.5px; font-weight: 600; color: var(--green-btn); }
.compare > summary::-webkit-details-marker { display: none; }
.compare > summary::before { content: "+"; font-family: var(--serif); font-size: 16px; color: var(--gold); width: 18px; text-align: center; flex-shrink: 0; }
.compare[open] > summary::before { content: "\2212"; }
.compare > summary:hover { color: var(--gold-dark); }
.compare__hint { font-weight: 400; color: var(--faint); font-size: 11.5px; margin-left: auto; text-align: right; }
.compare__body { padding: 0 18px 16px; border-top: 1px solid rgba(27,67,50,.1); }
.answer { padding: 12px 0; border-bottom: 1px solid rgba(27,67,50,.09); }
.answer:last-child { border-bottom: 0; padding-bottom: 0; }
.answer__q { font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .12em; }
.answer__a { font-size: 12.5px; line-height: 1.65; color: var(--body); margin: 5px 0 0; font-style: italic; }
.answer__a--grade { font-family: var(--serif); font-size: 18px; font-style: normal; color: var(--green-deep); }

/* editable draft */
.draft { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 22px 24px; margin-top: 12px; }
.draft__label { font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .14em; margin-top: 16px; }
.draft__label:first-child { margin-top: 0; }
.draft__pair { display: flex; gap: 16px; margin-top: 16px; }
.draft__pair > div:first-child { flex: 1.15; min-width: 0; }
.draft__pair > div:last-child { flex: 1; min-width: 0; }
.draft__pair .draft__label { margin-top: 0; }

/* The estimate and the homework, each on its own full-width row.
   They were a side-by-side pair, which left the estimate a little over half the
   container. Eight letter cells fit that; a four-across grid of score bands,
   whose labels are ranges like "1500-1600", does not. */
.draft__stack { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
.draft__stack > div { min-width: 0; }
.draft__stack .draft__label { margin-top: 0; }

/* Draft fields read as "editable copy" (dashed) rather than survey inputs. */
.page--approve .field { width: 100%; margin-top: 7px; border: 1px dashed rgba(27,67,50,.35); border-radius: 4px; background: var(--off-white); padding: 12px 13px; font-family: var(--sans); font-size: 16px; line-height: 1.65; color: var(--body); resize: vertical; display: block; }
.page--approve textarea.field { min-height: 70px; }
.page--approve .field:focus { outline: none; border-style: solid; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(201,162,39,.15); }

/* THE BOX IS AS TALL AS WHAT IT HOLDS.
   field-sizing does this with no script at all where it is supported, and
   public/autosize.js does the same everywhere else; the two agree, so a browser
   that has it never sees a second value written over the first. overflow-y
   hidden because a box sized to its content has nothing to scroll, and leaving
   it auto shows a scrollbar track for a pixel of rounding.
   min-height stays as the floor for an EMPTY box, which still has to look like
   somewhere to write. */
.page--approve textarea[data-autosize] {
  field-sizing: content;
  overflow-y: hidden;
  resize: vertical;
}

/* 4 · grade — same fixed scale as the survey, 3-up on this screen.
   ITS OWN COPY OF THE RULE, which is why it needs its own change: this screen
   shares the partial with the survey but not the stylesheet. Unconditional
   rather than phone-only, because nine cells at four across leave an orphan at
   EVERY width, not just a narrow one. */
.page--approve .grades { grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 7px; }
.grades__note { font-size: 11px; color: var(--faint); margin: 7px 0 0; }

/* send-back reason */
.returnbox { background: #fff; border: 1px solid var(--line); border-left: 3px solid var(--gold); border-radius: 6px; padding: 16px 18px; margin-top: 12px; }
.returnbox__label { display: block; font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .13em; }
.page--approve .returnbox .field { border-style: solid; border-color: var(--field); min-height: 64px; }
.returnbox__hint { font-size: 11px; color: var(--faint); margin: 7px 0 0; }

/* ── SCHEDULE SEND ────────────────────────────────────────────────────────
   Between the return box and the buttons. OFF is a white row that reads like
   any other setting; ON reverses the whole block to deep green, so the page
   says at a glance "this one is not going out yet".

   NO SCRIPT IS REQUIRED for any of it. The state is a real checkbox and the
   presets are real radios, and every rule below keys off :checked or off
   :has(:checked) on an ancestor. public/schedule-send.js updates one span.

   NO RED anywhere: the portal has no error colour, and a held send is not an
   error state. */
.sched { margin-top: 12px; border-radius: 6px; background: #fff; border: 1px solid var(--line); }
.sched__row { display: flex; align-items: center; gap: 14px; padding: 16px 20px; cursor: pointer; flex-wrap: wrap; }
/* The checkbox itself is the state; the pill beside it is what you see. Kept
   in the layout rather than display:none so it stays focusable and reachable
   by a keyboard and a screen reader. */
.sched__check { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.sched__switch { flex-shrink: 0; width: 44px; height: 26px; border-radius: 100px; background: rgba(27,67,50,.16); border: 1px solid rgba(27,67,50,.2); display: flex; align-items: center; padding: 0 3px; transition: background .15s; }
.sched__switch::before { content: ""; width: 20px; height: 20px; border-radius: 50%; background: var(--off-white); box-shadow: 0 1px 3px rgba(20,55,42,.3); transition: transform .15s, background .15s; }
.sched__label { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.sched__hint { font-size: 13px; color: var(--faint); }
.sched__now { margin-left: auto; font-size: 12px; font-weight: 600; letter-spacing: .1em; color: rgba(255,255,255,.55); display: none; }
.sched__check:focus-visible + .sched__switch { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Everything below the row exists only while the switch is on. */
.sched__body { display: none; padding: 4px 20px 20px; }

.sched:has(.sched__check:checked) { background: var(--green-deep); border-color: var(--green-deep); }
.sched:has(.sched__check:checked) .sched__label { color: var(--off-white); }
.sched:has(.sched__check:checked) .sched__hint { display: none; }
.sched:has(.sched__check:checked) .sched__now { display: inline; }
.sched:has(.sched__check:checked) .sched__body { display: block; }
.sched__check:checked + .sched__switch { background: var(--gold-light); border-color: var(--gold-light); justify-content: flex-end; }
.sched__check:checked + .sched__switch::before { background: var(--green-deep); box-shadow: none; }

.sched__eyebrow { font-size: 9.5px; font-weight: 600; letter-spacing: .14em; color: var(--gold-light); }
.sched__tiles { margin-top: 11px; display: flex; flex-wrap: wrap; gap: 9px; }

/* A tile is two lines: the time, and how far off it is. */
.tile { min-height: 44px; display: inline-flex; flex-direction: column; justify-content: center; padding: 0 18px; border-radius: 4px; border: 1px solid rgba(255,255,255,.28); cursor: pointer; }
.tile input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.tile__label { font-size: 13.5px; font-weight: 600; color: var(--off-white); }
.tile__rel { font-size: 11px; color: rgba(255,255,255,.55); }
.tile:has(input:checked) { background: var(--off-white); border: 1.5px solid var(--gold-light); }
.tile:has(input:checked) .tile__label { color: var(--ink); font-weight: 700; }
.tile:has(input:checked) .tile__rel { color: var(--muted); }
.tile:has(input:focus-visible) { outline: 2px solid var(--gold-light); outline-offset: 2px; }
.tile--custom { flex-direction: row; align-items: center; gap: 9px; }

/* The custom panel opens only when its own tile is the one selected. */
.sched__custom { display: none; margin-top: 14px; flex-wrap: wrap; gap: 12px; align-items: flex-end; padding: 16px; background: rgba(253,252,249,.07); border: 1px solid rgba(255,255,255,.16); border-radius: 5px; }
.sched__tiles:has(.tile--custom input:checked) + .sched__custom { display: flex; }
.sched__flabel { font-size: 9.5px; font-weight: 600; letter-spacing: .12em; color: rgba(255,255,255,.6); }
.sched__input { margin-top: 6px; min-height: 44px; padding: 0 14px; background: var(--off-white); border: 1px solid rgba(27,67,50,.2); border-radius: 4px; font-family: var(--sans); font-size: 16px; color: var(--ink); }
.sched__floor { flex: 1; min-width: 200px; margin: 0 0 12px; font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.68); }
.sched__floor b { color: var(--off-white); font-weight: 600; }
.sched__zone { margin: 14px 0 0; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.14); font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.68); }
.sched__zone b { color: var(--off-white); font-weight: 600; }

/* actions */
.actions { display: flex; align-items: center; gap: 14px; margin-top: 18px; flex-wrap: wrap; }
/* ONE PRIMARY BUTTON IS SHOWN AT A TIME, and the note beside it changes with
   it. Both exist in the markup because CSS cannot rewrite a label and the
   button must never claim to send something it is about to hold. */
.btn-approve--sched, .actions__note--sched { display: none; }
.cols__draft:has(.sched__check:checked) .btn-approve--now,
.cols__draft:has(.sched__check:checked) .actions__note--now { display: none; }
.cols__draft:has(.sched__check:checked) .btn-approve--sched { display: inline-block; }
.cols__draft:has(.sched__check:checked) .actions__note--sched { display: inline; font-weight: 600; color: var(--gold-dark); }
.btn-approve { padding: 14px 28px; background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px; font-family: var(--sans); font-size: 13px; font-weight: 600; color: #fff; letter-spacing: .06em; white-space: nowrap; flex-shrink: 0; cursor: pointer; transition: background .15s; }
.btn-approve:hover { background: var(--green-deep); }
.btn-return { padding: 14px 24px; background: #fff; border: 1px solid rgba(27,67,50,.25); border-radius: 4px; font-family: var(--sans); font-size: 13px; font-weight: 600; color: var(--green-btn); white-space: nowrap; flex-shrink: 0; cursor: pointer; transition: .15s; }
.btn-return:hover { border-color: var(--gold); color: var(--gold-dark); }
.actions__note { font-size: 12px; color: var(--faint); margin-left: auto; flex-shrink: 1; min-width: 0; }

/* The queue list's held band. Deliberately not a row in the table: these are
   decided, and mixing them into a list of things waiting on a person would put
   the WAITING count and the rows below it at odds. */
.sline { margin-top: 14px; background: var(--green-deep); border-radius: 6px; padding: 14px 18px 16px; }
.sline__head { font-size: 10px; font-weight: 600; letter-spacing: .13em; color: var(--gold-light); }
.sline__row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.12); color: var(--off-white); }
.sline__row:last-child { border-bottom: 0; padding-bottom: 0; }
.sline__row:hover .sline__name { color: var(--gold-light); }
.sline__name { font-size: 14px; font-weight: 600; }
.sline__tutor { font-size: 12.5px; color: rgba(255,255,255,.6); }
.sline__when { margin-left: auto; font-size: 12.5px; color: rgba(255,255,255,.78); }

/* ── A HELD REPORT (queue-held.ejs) ───────────────────────────────────────
   Approved, not sent. The chip and the panel both reverse to deep green, so
   the screen cannot be mistaken for the review screen it is not: there is no
   form here and nothing on it is editable. */
.chip-draft--held { background: var(--green-deep); border-color: var(--green-deep); color: var(--gold-light); }

.heldpanel { background: var(--green-deep); border-left: 4px solid var(--gold); border-radius: 0 6px 6px 0; padding: 22px 24px; }
.heldpanel__eyebrow { font-size: 10.5px; font-weight: 600; letter-spacing: .12em; color: var(--gold-light); }
.heldpanel__when { font-family: var(--serif); font-size: 27px; line-height: 1.25; color: var(--off-white); margin: 11px 0 0; }
.heldpanel__who { font-size: 13px; color: rgba(255,255,255,.68); margin: 13px 0 0; }
.heldpanel__acts { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 11px; align-items: flex-start; }

.heldbtn { min-height: 46px; display: inline-flex; align-items: center; padding: 0 22px; border-radius: 4px; border: 1px solid rgba(255,255,255,.3); background: transparent; font-family: var(--sans); font-size: 13.5px; font-weight: 600; color: var(--off-white); cursor: pointer; list-style: none; }
.heldbtn::-webkit-details-marker { display: none; }
.heldbtn:hover { border-color: var(--gold-light); color: var(--gold-light); }
.heldbtn--go { background: var(--gold-light); border-color: var(--gold-light); color: var(--green-deep); font-weight: 700; letter-spacing: .04em; }
.heldbtn--go:hover { background: var(--gold); border-color: var(--gold); color: var(--green-deep); }

/* Change the time opens in place: same tiles, same custom pair, no round trip
   through a second screen to move an hour. */
.heldtime { min-width: 0; }
.heldtime[open] { flex-basis: 100%; }
.heldtime__form { margin-top: 14px; }
.heldtime__go { margin-top: 14px; }

/* The words as approved. Same labels as the review screen, no boxes: a field
   an admin cannot type into should not be drawn as one. */
.draft--held { margin-top: 12px; }
.held__body { font-size: 14px; line-height: 1.65; color: var(--muted); margin: 9px 0 0; white-space: pre-wrap; }
.held__estimate { font-family: var(--serif); font-size: 18px; color: var(--green-deep); margin: 7px 0 0; }
.held__note { margin: 15px 0 0; padding-top: 15px; border-top: 1px solid var(--hair); font-size: 12.5px; color: var(--faint); }

/* rail: confidence + the tutor's private note */
.conf { display: flex; align-items: baseline; gap: 8px; margin-top: 8px; }
.conf__word { font-family: var(--serif); font-size: 28px; color: var(--green-deep); }
.conf__score { font-size: 12px; color: var(--faint); }
.conf__bar { height: 6px; border-radius: 3px; background: rgba(27,67,50,.1); margin-top: 10px; overflow: hidden; }
.conf__fill { display: block; height: 100%; background: var(--gold); }
.conf__note { font-size: 11.5px; line-height: 1.55; color: var(--faint); margin: 8px 0 0; }
.scard--private { background: var(--green-deep); border-color: var(--green-deep); }
.scard--private .scard__label { color: var(--gold-light); }
.scard--private .private__body { font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.8); margin-top: 8px; }

@media (max-width: 980px) {
  .page--approve .cols { flex-direction: column-reverse; }
  .cols__draft { flex: 1 1 auto; width: 100%; }
}
/* ══ Approval confirmation (design/admin-success.html) ═════════════════════
   Reuses the .done panel from the tutor confirmation below; only the values
   that differ in this design are scoped to .page--approval, so the tutor screen
   is unaffected. */
.page--approval { max-width: 560px; padding: 44px 26px 60px; }
.page--approval .done { padding: 48px 32px; }
.page--approval .done__title { font-size: 27px; }
.page--approval .done__btn { margin-top: 22px; padding: 14px 26px; }
.page--approval .done__next { font-size: 12px; }
.nav__logoutform { margin: 0; }
/* the panel mark: check (sent), return arrow, or the warning fill */
.done__mark {
  width: 56px; height: 56px; margin: 0 auto; border-radius: 50%;
  border: 1.5px solid var(--gold); background: rgba(227,181,58,.12);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 24px; color: var(--gold-light);
}
.done__mark--return { font-size: 22px; }
/* Held: the same quiet weight as the check. Nothing has gone wrong, and the
   warning fill is reserved for a parent who is owed a report and has not had
   one, which is the opposite of this. */
.done__mark--held { font-size: 26px; }
/* louder than the success check on purpose: a parent got nothing */
.done__mark--warn {
  border: 2px solid var(--gold-light); background: var(--gold-light);
  color: var(--green-deep); font-size: 30px; font-weight: 700;
  box-shadow: 0 0 0 6px rgba(227,181,58,.18);
}
@media (max-width: 760px), (max-height: 499px) {
  .page--approval { padding: 24px 16px 40px; }
  .page--approval .done { padding: 40px 22px; border-radius: 6px; }
  .page--approval .done__title { font-size: 24px; }
  .page--approval .done__btn { display: block; width: 100%; text-align: center; }
}

/* ══ Submission confirmation (standalone brand layout — design/submitted.html) ══
   Reuses .nav* and .page; the panel is its own narrow shell. */
.page--done { max-width: 560px; padding-top: 44px; padding-bottom: 60px; }
.done { background: var(--green-deep); border-radius: 8px; padding: 52px 34px; text-align: center; }
.done__check { width: 56px; height: 56px; margin: 0 auto; border-radius: 50%; border: 1.5px solid var(--gold); background: rgba(227,181,58,.12); display: flex; align-items: center; justify-content: center; font-family: var(--serif); font-size: 24px; color: var(--gold-light); }
/* Failure twin of the confirmation panel: same shell, no green check. */
.done__check--bad { border-color: rgba(255,255,255,.45); background: rgba(255,255,255,.08); color: #fff; font-family: var(--sans); font-weight: 700; }
.done__title { font-family: var(--serif); font-size: 28px; font-weight: 400; color: var(--off-white); margin: 22px 0 0; }
.done__title em { color: var(--gold-light); font-style: normal; }
.done__body { font-size: 13.5px; line-height: 1.7; color: rgba(255,255,255,.72); margin: 12px 0 0; }
.done__body b { color: #fff; font-weight: 600; }
.done__aside { font-size: 13px; line-height: 1.7; color: rgba(255,255,255,.6); margin: 14px 0 0; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.14); }
.done__btn { display: inline-block; margin-top: 24px; padding: 14px 28px; background: var(--gold-light); border: 0; border-radius: 4px; font-family: var(--sans); font-size: 13px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; white-space: nowrap; cursor: pointer; transition: background .15s; text-decoration: none; }
.done__btn:hover { background: var(--gold); color: var(--green-deep); }
.done__next { font-size: 11.5px; color: rgba(255,255,255,.5); margin: 16px 0 0; }
.done__next a { color: var(--gold-light); border-bottom: 1px solid rgba(227,181,58,.4); text-decoration: none; }
.done__next a:hover { color: #fff; border-color: #fff; }

@media (max-width: 760px), (max-height: 499px) {
  .head__title { font-size: 22px; }
  .page--done { padding-top: 24px; padding-bottom: 40px; }
  .done { padding: 40px 22px; border-radius: 6px; }
  .done__title { font-size: 24px; }
  .done__btn { display: block; width: 100%; text-align: center; }
  .reason__row, .flag { flex-direction: column; gap: 7px; }
  .draft { padding: 16px 18px; }
  .draft__pair { flex-direction: column; gap: 14px; }
  .compare__hint { display: none; }
  /* THE FLOOR, not the ceiling. The boxes size to their content (see
     [data-autosize] above); this is only what an EMPTY one looks like, and on a
     phone a 70px box reads as a single line rather than as somewhere to write a
     paragraph. Nothing here clamps a full one. */
  .page--approve textarea.field { min-height: 96px; }
  /* The tutor's original answers must not be clamped either. Stated rather than
     inherited: these are the answers the draft is being checked against, and a
     rule added elsewhere that capped them would be silent. */
  .compare__body, .answer, .answer__a { max-height: none; overflow: visible; }
  .actions { flex-direction: column; align-items: stretch; }
  .btn-approve, .btn-return { width: 100%; }
  .actions__note { margin: 2px 0 0; text-align: center; }
}


/* ══ Admin tools (design/admin-tools.html) ═════════════════════════════════
   Collision-prone names (.card, .badge, .empty, .submit, .page) are scoped to
   .page--admin so the queue/history/status pages keep their own treatment. */
.page--admin { max-width: 1080px; margin: 0 auto; padding: 28px 26px 60px; }
  /* ── shell ──────────────────────────────────────────────── */
  .page--admin .page{max-width:1080px;margin:0 auto;padding:28px 26px 60px}
  /* .h1's own type is gone: it is the shared page-title rule near the top of
     this file, and it was the treatment that rule is based on. */
  .page--admin .eyebrow{font-size:10.5px;font-weight:600;color:var(--gold-dark);letter-spacing:.14em}
  .page--admin .note{font-size:11.5px;line-height:1.6;color:var(--faint);margin:8px 0 0}

  /* ── kill switch status bar ─────────────────────────────── */
  .kill{
    background:var(--green-deep);border-left:4px solid var(--gold-light);border-radius:6px;
    padding:20px 24px;margin-top:18px;display:flex;align-items:center;gap:24px;
  }
  /* The banner's text column and its label row. Classes rather than the inline
     style attributes the older banners on this page still carry. */
  .kill__body{flex:1}
  .kill__row{display:flex;align-items:center;gap:10px}
  .kill__dot{width:8px;height:8px;border-radius:50%;background:var(--gold-light);display:inline-block}
  .kill__state{font-size:10.5px;font-weight:600;color:var(--gold-light);letter-spacing:.14em}
  .kill__headline{font-family:var(--serif);font-size:18px;color:var(--off-white);margin:8px 0 0}
  .kill__sub{font-size:12.5px;line-height:1.6;color:rgba(255,255,255,.6);margin:5px 0 0}
  .kill__btn{
    padding:14px 24px;background:var(--gold-light);border:0;border-radius:4px;
    font-family:var(--sans);font-size:12.5px;font-weight:600;color:var(--green-deep);
    letter-spacing:.06em;white-space:nowrap;cursor:pointer;transition:background .15s;
  }
  .kill__btn:hover{background:var(--gold)}
  /* ON state: cream bar, green rule, green dot, dark text */

  /* ── password confirm (replaces the bar's button row in place) ── */
  .confirm{
    background:var(--green-deep);border-left:4px solid var(--gold-light);
    border-radius:6px;padding:20px 24px;margin-top:14px;
  }
  .confirm__headline{font-family:var(--serif);font-size:17px;color:var(--off-white);margin:8px 0 0}
  .confirm__sub{font-size:12.5px;line-height:1.6;color:rgba(255,255,255,.65);margin:5px 0 0}
  .confirm__row{display:flex;gap:12px;margin-top:16px;align-items:center;flex-wrap:wrap}
  .confirm__input{
    flex:1;min-width:180px;max-width:300px;
    border:1px solid rgba(255,255,255,.24);border-radius:4px;background:rgba(255,255,255,.07);
    padding:12px 13px;font-family:var(--sans);font-size:16px;color:var(--off-white);
  }
  .confirm__input::placeholder{color:rgba(255,255,255,.45)}
  .confirm__input:focus{outline:0;border-color:var(--gold-light);background:rgba(255,255,255,.1)}
  .confirm__go{
    padding:13px 22px;background:var(--gold-light);border:0;border-radius:4px;
    font-family:var(--sans);font-size:12.5px;font-weight:600;color:var(--green-deep);
    letter-spacing:.06em;white-space:nowrap;cursor:pointer;transition:background .15s;
  }
  .confirm__go:hover{background:var(--gold)}
  .confirm__cancel{
    font-family:var(--sans);font-size:12.5px;color:rgba(255,255,255,.6);
    background:none;border:0;border-bottom:1px solid rgba(255,255,255,.3);padding:0;cursor:pointer;
  }
  .confirm__cancel:hover{color:#fff;border-color:#fff}
  .confirm__error{
    font-size:11.5px;color:var(--gold-light);margin:12px 0 0;
    padding-top:11px;border-top:1px solid rgba(255,255,255,.14);
  }

  /* ── cards ──────────────────────────────────────────────── */
  .page--admin .card{background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden}
  .page--admin .card--pad{padding:22px 24px 24px}
  .card__head{padding:18px 22px 14px;border-bottom:1px solid var(--hair)}
  .card__title{font-family:var(--serif);font-size:20px;font-weight:400;color:var(--ink);margin:0}
  .card__blurb{font-size:12.5px;line-height:1.6;color:var(--muted);margin:6px 0 0}

  /* ── trust table ────────────────────────────────────────── */
  .trust{margin-top:22px}
  .trust__row{
    display:grid;grid-template-columns:1.2fr 1.2fr 1.4fr .8fr .9fr;gap:12px;
    padding:14px 22px;border-top:1px solid rgba(27,67,50,.08);align-items:center;
    font-size:13.5px;color:var(--body);
  }
  .trust__row--head{
    padding:11px 22px;background:var(--cream);border:0;
    font-size:9.5px;font-weight:600;color:var(--faint);letter-spacing:.1em;
  }
  .trust__row--eligible{background:rgba(201,162,39,.06)}
  .trust__name{font-weight:600}
  .trust__streak{display:flex;align-items:center;gap:8px}
  .trust__bar{flex:1;max-width:80px;height:5px;border-radius:3px;background:var(--hair);overflow:hidden}
  .trust__bar span{display:block;height:100%;background:var(--gold)}
  .trust__bar--full span{background:var(--green-btn)}
  .trust__count{font-size:12px;color:var(--muted)}
  .trust__flag{font-size:10px;font-weight:600;color:var(--faint);letter-spacing:.06em}
  .trust__flag--yes{color:var(--green-btn)}
  .trust__togo{font-size:11.5px;color:var(--faint)}
  .trust__promote{
    padding:7px 12px;background:var(--green-btn);border:0;border-radius:4px;
    font-family:var(--sans);font-size:10.5px;font-weight:600;color:#fff;
    letter-spacing:.04em;text-align:center;cursor:pointer;
  }
  .trust__promote:hover{background:var(--green-deep)}
  .trust__revoke{
    justify-self:start;   /* keeps the underline on the word, not the column */
    font-family:var(--sans);font-size:11.5px;color:var(--muted);
    background:none;border:0;border-bottom:1px solid rgba(27,67,50,.2);padding:0;cursor:pointer;
  }
  .trust__revoke:hover{color:var(--danger);border-color:var(--danger)}

  /* ── two-column forms + rosters ─────────────────────────── */
  .cols{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:22px;align-items:start}
  .pair{display:flex;gap:12px;margin-top:14px}
  .pair>div{flex:1}
  .pair>div.wide{flex:1.3}
  .lbl{display:block;font-size:10.5px;font-weight:600;color:var(--green-deep);letter-spacing:.04em;margin:14px 0 6px}
  .lbl:first-of-type{margin-top:18px}
  .in,.sel{
    width:100%;border:1px solid #D4D9D3;border-radius:4px;background:var(--off-white);
    padding:11px 12px;font-family:var(--sans);font-size:16px;color:var(--body);
    /* AN UPPER BOUND, WHICH THESE HAVE NEVER HAD. `width:100%` is a preference
       and a form control is allowed to ignore it: the used width can come from
       the control's own intrinsic minimum, which is a user-agent decision and
       is the one thing a desktop engine will not reproduce for a native mobile
       control. Measured on Trey's iPhone 16 Pro Max (440px viewport, card
       content box 51.0 to 388.7):

         Choose a student   select, plain block   51.0..389.0   flush
         Student… / Tutor…  select in .tagform    51.0..397.7   over by  +9.0
         SESSION DATE       input[type=date]      51.0..clipped over by >+24.6

       Two different overflows, so two different inflations; +24.6 is the date's
       own 24px of padding plus its 2px of border, which is what the box would
       measure if `width` had been applied to the content box rather than the
       border box.

       max-width is the bound that holds whichever it is. It caps the BORDER box
       at the container, so it does not care what inflated the control or why,
       and it is inert wherever nothing does: verified byte-identical geometry
       in WebKit at 320, 390, 430 and 1200 before and after. min-width:0 is the
       matching floor, so a control that is also a flex item can still shrink.

       UNVERIFIED ON THE HANDSET. This is reasoned from a screenshot, not from a
       reading taken on the device, and it is the only change here that is. */
    max-width:100%;min-width:0;
  }

  /* THE DATE INPUTS, WHICH OVERFLOW WHEREVER THEY ARE. #ms-date sits in a plain
     block and .tagform__date in a flex column, and both ran past the card while
     the select beside them did not, so the context is not the cause: the
     control is. Stripping the native appearance sizes them like every other
     input on the page rather than like a platform widget.

     Prefixed AND unprefixed. iOS 26 needs neither prefix, so this is not the
     old Safari 15.4 problem; the prefix costs nothing and this stylesheet
     declares no browserslist. */
  input[type="date"].in{-webkit-appearance:none;appearance:none}
  .in::placeholder{color:#98A39B}
  .in:focus,.sel:focus{outline:0;border-color:var(--gold);background:#fff}
  .sel{appearance:none;background-image:none}
  .selwrap{position:relative}
  .selwrap::after{content:'▾';position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--faint);font-size:12px;pointer-events:none}
  .page--admin .submit{
    width:100%;margin-top:18px;padding:13px;background:var(--green-btn);
    border:1.5px solid var(--gold);border-radius:4px;
    font-family:var(--sans);font-size:12.5px;font-weight:600;color:#fff;
    letter-spacing:.06em;cursor:pointer;transition:background .15s;
  }
  .page--admin .submit:hover{background:var(--green-deep)}

  /* ── rosters ────────────────────────────────────────────── */
  .roster__head{font-size:10.5px;font-weight:600;color:var(--gold-dark);letter-spacing:.14em;margin:24px 0 4px}
  .roster__row{padding:12px 0;border-top:1px solid var(--hair);display:flex;align-items:flex-start;gap:10px}
  /* THE DISPLAY DECLARATION ABOVE DEFEATS [hidden], so both have to be honoured
     explicitly. An author-origin `display` beats the UA stylesheet's
     [hidden]{display:none} outright, whatever the specificity, which meant the
     hidden attribute on a roster row did nothing at all: deactivated and
     archived rows sat on the page, the name filter could not hide a row that
     did not match, and pages past the first never went away. Only small rosters
     hid how broken it was. The same trap is commented on at .fwgrading[hidden]
     further down this file. */
  .roster__row[hidden]{display:none}
  /* GROUP VISIBILITY FOLLOWS THE LIST'S OWN STATE, in CSS, so it applies before
     a single line of script runs and a slow or failed roster.js can never leave
     these rows on screen. data-archived-open is the ONE source of truth: the
     label reads it too. */
  .rlist:not([data-archived-open]) .roster__row--archived{display:none}
  .roster__row--tight{align-items:center}
  .roster__main{flex:1}
  .roster__name{font-size:13.5px;font-weight:600;color:var(--ink)}
  .roster__meta{font-size:11.5px;line-height:1.5;color:var(--faint);margin-top:2px}
  .roster__acts{display:flex;gap:12px;padding-top:2px;white-space:nowrap}
  .roster__link,.roster__archive{
    font-family:var(--sans);font-size:11.5px;color:var(--muted);
    background:none;border:0;border-bottom:1px solid rgba(27,67,50,.2);padding:0;cursor:pointer;
  }
  .roster__link:hover{color:var(--gold-dark);border-color:var(--gold)}
  .roster__archive:hover{color:var(--gold-dark);border-color:var(--gold)}
  /* the ONLY red control on this page. Archived rows only, never the live roster. */
  .roster__purge{
    font-family:var(--sans);font-size:11.5px;color:var(--danger);
    background:none;border:0;border-bottom:1px solid rgba(155,44,44,.4);padding:0;cursor:pointer;
  }
  .roster__purge:hover{color:#7A1F1F;border-color:#7A1F1F}
  /* Why a purge is not on offer, in place of the control. Muted and unlined: it
     is a sentence to read, not something to click. */
  .roster__blocked{
    font-family:var(--sans);font-size:11px;line-height:1.5;color:var(--faint);
    max-width:34ch;text-align:right;
  }
  /* archived rows stay visible but recede, and lose their actions except Restore */
  .roster__row--archived .roster__name,
  .roster__row--archived .roster__meta{color:var(--faint)}
  /* email failures: the address that was refused, and the provider's reason.
     The address is set in mono because a typo in one is what this panel most
     often exists to reveal, and a proportional face hides them. */
  .roster__addr{font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace);color:var(--ink)}
  .roster__why{color:var(--danger)}
  .roster__toggle{
    display:flex;align-items:center;gap:8px;margin:24px 0 4px;
  }
  .roster__toggle .roster__head{margin:0}

  /* ── the roster group: ONE CONTAINER OVER ALL THREE PARTS ──
     The header band, the filter and the rows. They share one left edge and one
     right edge because this box's content edges are the only edges any of them
     is allowed to have, not because three separate rules happen to agree.

     They used to be two siblings that happened to agree, and they stopped
     agreeing the moment an unrelated `.rlist` rule landed 20px of horizontal
     padding on the list (see the note at .rwrap > .rlist). The reset below is
     deliberately blunt for exactly that reason: a future rule that pads .rlist
     again, from anywhere in this file, cannot move these edges. */
  .rgroup{display:flow-root}
  .rgroup > *{margin-left:0;margin-right:0;padding-left:0;padding-right:0}

  /* THE GAP, PAID ONCE, BY THE LIST. The band's bottom margin goes to zero and
     the filter's top margin goes to zero, so the gap is one number in one place
     rather than three that add up. Padding rather than a margin because padding
     cannot collapse: the filter's top margin used to escape .rlist and meet the
     band's, which is why the same three declarations could read as 4px, 10px or
     34px depending on what else was set.

     Measured, in the broken build: 4px band margin + 20px list padding + 10px
     filter margin = 34px. Measured now: 0 + 10 + 0 = 10px, and no collapsing
     path exists that could make it anything else.

     10px AND NOT 16px, WHICH SUPERSEDES THE EARLIER BUILD. This gap shipped at
     16px, asked for as "the standard section rhythm". design_handoff_roster_spacing
     item 3 asks for the filter's own margin instead, which is the 10px in
     .rlist__filter's `margin: 10px 0 4px`, and its acceptance reads "one gap
     above the filter, about 10px, not 30". Trey's ruling is that the bundle
     wins. The NUMBER comes from the bundle; the MECHANISM stays as it is,
     because letting that margin collapse out of .rlist to produce the 10px is
     the exact ambiguity the paragraph above exists to remove. Same result, one
     declaration, no collapsing. */
  .rgroup > .roster__toggle{margin-bottom:0}
  .rgroup > .rlist{padding-top:10px}
  .rgroup .rlist__filter{margin-top:0}
  .roster__showarch{
    margin-left:auto;font-family:var(--sans);font-size:11px;color:var(--muted);
    background:none;border:0;border-bottom:1px solid rgba(27,67,50,.2);padding:0;cursor:pointer;
  }
  .roster__showarch:hover{color:var(--gold-dark);border-color:var(--gold)}
  .page--admin .badge{padding:4px 9px;border-radius:100px;background:rgba(27,67,50,.08);font-size:9.5px;font-weight:600;color:var(--muted);letter-spacing:.06em}
  .page--admin .badge--admin{background:rgba(201,162,39,.18);border:1px solid rgba(168,132,42,.35);color:var(--green-deep)}
  .roster__note{font-size:11px;line-height:1.6;color:var(--faint);margin-top:12px;padding-top:10px;border-top:1px solid var(--hair)}
  /* NO border of its own, deliberately. This rule used to override the top edge
     alone with a --hair hairline, which left the box with three heavy sides from
     the .empty at the top of this file and one pale one. Dropping the override
     lets that single rule own all four edges: 1px --line on three, and the 3px
     accent rail it draws down the left. One rule, one border.

     The 20px of horizontal padding comes from the same place and for the same
     reason, so both zero states share one horizontal rhythm.

     Worth knowing, because the padding looks over-specified next to the flush
     rows above it: 0 was not an omission here. This rule is a port of
     design/admin-tools.html, where .empty is a roster LIST ROW, flush left by
     design and separated by a hairline, exactly like .roster__row and
     .roster__note beside it. It only reads as a box at all because the global
     .empty supplies a background, a border and a radius that this override never
     reset. The box is the deliberate choice now, so the padding matches the box
     rather than the row. */
  .page--admin .empty{padding:16px 20px;font-size:12.5px;color:var(--faint)}

  @media (max-width:860px){
    .cols{grid-template-columns:1fr}
    .kill{flex-direction:column;align-items:flex-start;gap:16px}
    .kill__btn{width:100%}
    .trust__row{grid-template-columns:1fr 1fr;gap:8px 12px;padding:14px 16px}
    .trust__row--head{display:none}
    .trust__streak{grid-column:1 / -1}
  }
  /* ══ ⚠ THIS BLOCK IS THE ADMIN-FAMILY MOBILE STYLESHEET, AND IT IS SHARED ══
   *
   * ⚠ READ THIS BEFORE CHANGING ANYTHING ON THE ADMIN TOOLS PAGE. Thirty of the
   * thirty-one selectors below are admin-family, and the block carries no name
   * saying so. That is how six rounds of admin-page fixes were written without
   * anyone reconciling against it, and how `.roster__name { flex-direction:
   * column }` here silently beat a later rule that set every property EXCEPT
   * flex-direction. The cascade is per property; the rule that has to be beaten
   * is the one that sets the property you care about, not the one that shares
   * the class.
   *
   * ⚠ IT CANNOT BE MOVED, and that was checked rather than assumed.
   * `.roster__row`, `.roster__name` and `.roster__link` are used by FIVE other
   * templates -- admin-competency, admin-competency-framework,
   * admin-competency-tutor, admin-training-content and admin-tutorbird -- and
   * `.pair` by four more including two calendar sheets. Relocating the block
   * would change cascade order for all of them, and scoping it to .page--admin
   * would delete their mobile layout outright.
   *
   * ⚠ SO THE ADMIN TOOLS PAGE DISOWNS IT RATHER THAN MOVING IT. Its own
   * sections use their own names -- achip, atile, sstrip, pay, amodal, rmenu --
   * which nothing here can reach. Where it still shares a name, the override
   * lives in the page's own block and states every property it needs to win,
   * flex-direction included. Anything new on that page takes a new name.
   *
   * The one non-admin selector here is .pair, which stays for the four
   * templates above. */
  @media (max-width:760px){
    .page--admin .page{padding:20px 16px 40px}
    .pair{flex-direction:column;gap:0}
    .confirm__row{flex-direction:column;align-items:stretch}
    .confirm__input,.confirm__go{max-width:none;width:100%}

    /* ── TRUST ROWS: TWO COMPACT LINES PER PAIR ────────────────
       One fact per line, each with a caption above it, made every pair a
       full-height card: 216px measured, so three pairs filled a phone screen and
       the section could not be scanned at all.

       Two lines now:

         Abby Turk · Billy Neveldine
         AUTO-SEND NO   [====------]  3 / 10 · 7 to go

       GRID, NOT FLEX, and this is the whole reason. flex-wrap breaks lines by
       measuring hypothetical sizes, and flex-grow is applied AFTER that decision,
       so growing an item cannot push the next one onto a new line. The first
       attempt did exactly that and the auto-send state landed beside the student
       name, overlapping it. The only flex break is a 100% basis, which puts that
       item alone on its line and cannot express "these two share line two".

       Grid places by row and column, so the layout is stated rather than
       inferred, and the conditional children fall through to row 3 by
       auto-placement: PROMOTE, Revoke and the to-go note are each present only
       sometimes and none of them needs to be counted. */
    .trust__row:not(.trust__row--head){
      display:grid;grid-template-columns:auto minmax(0,1fr);
      /* THE DIVIDER IS THIS ROW'S OWN border-top, so it spans the row including
         its padding, and 16px of side padding put every line of text 16px to the
         right of the line above it. Dropping the side padding is what aligns the
         two; the card already supplies the margin from the screen edge.

         The row gap is the other half of the fix: at 5px the auto-send state sat
         directly under the student name and read as attached to it. 10px is a
         line's worth of separation, so the second line reads as its own fact. */
      gap:10px 14px;padding:12px 0;align-items:baseline;
    }
    /* THE TITLE AND BLURB SHARE THE ROWS' LEFT EDGE. The card head keeps the
       desktop's 22px side padding, which left the section title and its
       sentence indented from the dividers and every row below them once the
       rows dropped their own padding. Scoped to this card: every other card on
       the page still wants its inset. */
    .trust .card__head{padding-left:0;padding-right:0}
    .trust__row>*{min-width:0}
    .trust__name{grid-column:1;grid-row:1}
    .trust__student{grid-column:2;grid-row:1}
    .trust__student::before{content:'\00B7\00A0';color:var(--faint)}
    .trust__flag{grid-column:1;grid-row:2;font-size:12px;white-space:nowrap}
    /* Stays a box here, exactly as it is on desktop: the bar and its count are
       one thing and they share the second line's remaining width. */
    .trust__streak{grid-column:2;grid-row:2;display:flex;align-items:center;gap:10px}
    /* HALF THE CELL, NOT ALL OF IT. Growing to fill pushed the bar's left edge
       hard against the auto-send value, so "NO" read as the first word of the
       bar rather than as the answer to AUTO-SEND. Half leaves the value its own
       air and still shows the streak at a glance; the count keeps the right. */
    .trust__bar{flex:0 0 50%;max-width:none;margin:0}
    .trust__count{flex:0 0 auto}

    /* THE RIGHT EDGE IS A LINE, NOT A RAGGED MARGIN.

       Both lines of a pair ended wherever their own text happened to run out.
       Line one stopped at the end of the student's name and line two at the end
       of the count, and since neither string is a fixed length, every row ended
       at a different x: the panel had a clean left edge and no right edge at
       all. Measured at 390 before this, against a card whose inner right is
       326: the counts ended at 288.3, 293.8 and 277.1, and the names short of
       the same line by as much as 40px.

       Pushed to the end of the column rather than given widths. Column two is
       minmax(0,1fr) and already ends exactly where the card's padding ends, so
       aligning to it is what makes the two lines share one edge; a width would
       be a second number to keep in step with the padding.

       Written at two classes and after the rules above, because the single
       class versions of both sit ten lines up and at equal specificity the
       later rule would be the one that won by accident rather than by
       intent. */
    .trust__row .trust__student{text-align:right}
    .trust__row .trust__streak{justify-content:flex-end}
    /* Everything conditional spans the row and lands on line three. */
    .trust__togo,.trust__promoteform,.trust__revoke{grid-column:1 / -1;justify-self:start}
    .trust__togo{font-size:12px;color:var(--muted)}
    .trust__promoteform{justify-self:stretch}
    .trust__promote{width:100%;padding:11px}

    /* ONLY THE AUTO-SEND STATE IS CAPTIONED, and inline rather than above.
       "YES" and "NO" mean nothing alone; the tutor, the student and the bar all
       say what they are without help, and a caption block above each was most of
       the 216px. The text comes from the markup's data-label, so nothing above
       760px gains an element. */
    .trust__row .trust__name::before,
    .trust__row .trust__student::before,
    .trust__row .trust__bar::before{content:none}
    .trust__row .trust__student::before{content:'\00B7\00A0'}
    /* The caption and the value are two words, not one: a single non-breaking
       space put "NO" hard against "AUTO-SEND" and it read as part of the label. */
    .trust__row .trust__flag::before{
      content:attr(data-label);margin-right:7px;
      font-family:var(--sans);font-size:10.5px;font-weight:600;
      color:var(--green-deep);letter-spacing:.04em;
    }

    /* ── 4 · THE TUTOR ROW: STACKED, AND INSIDE THE VIEWPORT ───
       Measured before this: .roster__acts ended at x=459 in a 390px viewport,
       so Edit, Reset password and Deactivate were 69px off the right edge and
       unreachable. The row is a two-column flex (main | acts) and the actions
       simply did not fit beside the name.

       Stacked here, in the order the handoff asks for: name, then the students
       pill directly beneath it, then email and rate, then the actions on their
       own row. */
    .roster__row{flex-direction:column;align-items:stretch;gap:4px}

    /* THE PILL GOES UNDER THE NAME, LEFT-ALIGNED WITH IT. It was inline after
       the name text, which put it at x=77 against the name's x=71: six pixels
       of indent that read as a mistake. A column flex puts the name text, any
       badge and the pill each on their own line, all starting at the same x. */
    .roster__name{display:flex;flex-direction:column;align-items:flex-start;gap:6px}
    /* ⚠ THE TWO margin-left:0 OVERRIDES THAT SAT HERE ARE GONE WITH THE MARGINS
       THEY FOUGHT. They existed because the badge carried an inline
       `style="margin-left:6px"` and .spop__pill a 6px rule, and stacked here
       they became a 6px indent: measured, the name started at x=71 and the pill
       at x=77. Both margins are now removed at their source, so there is nothing
       left to override and the `!important` that beat the inline style has
       nothing to beat. The containers' own gap does the separating at every
       width, which is what it was always doing on the line. */

    /* The actions wrap inside the viewport rather than running past it, and each
       stays a 44px target. */
    /* The 44px targets are 44px tall, and the text inside them is centred, so
       each action carried roughly 14px of empty leading above its word. Against
       the row's own 8px gap that read as a band of dead space between the email
       line and the actions. The gap goes and the extra leading is pulled back,
       so the words land where a normal next line would, while the TARGET stays
       44px: the space is still tappable, it is simply not pushing anything. */
    .roster__acts{
      display:flex;flex-wrap:wrap;align-items:center;gap:0 14px;width:100%;
      margin-top:-10px;
    }

    /* 44px targets, with the underline kept AGAINST THE TEXT. As a border-bottom
       on a 44px-tall box the rule sits at the bottom of the tap target, a long
       way under the word, which reads as a stray line rather than as a link.
       Drawn by a pseudo-element instead, offset in em so it tracks the font
       size. Same treatment the mobile Log out already uses. */
    .roster__link,.roster__archive{
      min-height:44px;display:inline-flex;align-items:center;
      border-bottom:0;position:relative;
    }
    .roster__link::after,.roster__archive::after{
      content:'';position:absolute;left:0;right:0;top:50%;margin-top:.72em;
      border-bottom:1px solid rgba(27,67,50,.2);
    }
    .roster__link:hover::after,.roster__archive:hover::after{border-bottom-color:var(--gold)}

    /* ── 5 · THE AVAILABILITY PILLS ────────────────────────────
       They read Mo, Tu, W on a phone. The pills carry flex-shrink: 1 by
       default, so on a line that does not quite fit they were squeezed and the
       label was clipped inside them rather than the row wrapping. It does not
       reproduce in every engine, because how much a pill can give up depends on
       how wide that engine draws a checkbox, which is exactly why the fix is to
       stop them shrinking at all rather than to widen them by a guess.

       Wrapping is already allowed on .daypick; refusing to shrink is what makes
       it happen. */
  }

/* confirm dialog — native <dialog>, styled to match */
.dlg { border: 0; border-radius: 8px; padding: 0; max-width: 460px; width: calc(100% - 32px); background: #fff; box-shadow: 0 18px 50px rgba(20,55,42,.28); }
.dlg::backdrop { background: rgba(20,55,42,.45); }
.dlg__inner { padding: 24px 26px 22px; }
.dlg__title { font-family: var(--serif); font-size: 20px; color: var(--ink); margin: 0 0 10px; }
.dlg__body { font-size: 13px; line-height: 1.65; color: var(--muted); margin: 0; white-space: pre-line; }
.dlg__typewrap { margin-top: 16px; }
.dlg__typelabel { display: block; font-size: 10.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .04em; margin-bottom: 6px; }
.dlg__input { width: 100%; border: 1px solid #D4D9D3; border-radius: 4px; background: var(--off-white); padding: 11px 12px; font-family: var(--sans); font-size: 16px; color: var(--body); }
.dlg__input:focus { outline: 0; border-color: var(--gold); background: #fff; }
.dlg__acts { display: flex; align-items: center; gap: 14px; margin-top: 20px; }
.dlg__go { padding: 12px 20px; background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px; font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: #fff; letter-spacing: .06em; cursor: pointer; }
.dlg__go:hover { background: var(--green-deep); }
.dlg__go:disabled { opacity: .45; cursor: not-allowed; }
.dlg__go--danger { background: var(--danger); border-color: #7A1F1F; }
.dlg__go--danger:hover { background: #7A1F1F; }
.dlg__cancel { font-family: var(--sans); font-size: 12.5px; color: var(--muted); background: none; border: 0; border-bottom: 1px solid rgba(27,67,50,.2); padding: 0; cursor: pointer; }
.dlg__cancel:hover { color: var(--ink); }
.pw-reveal { background: var(--off-white); border: 1px solid var(--line); border-left: 4px solid var(--gold); border-radius: 6px; padding: 16px 20px; margin-top: 18px; }
.pw-reveal code { font-size: 15px; background: #fff; border: 1px solid var(--line); padding: 6px 10px; border-radius: 4px; display: inline-block; margin-top: 8px; }

/* ══ Approval queue (design/queue.html) ════════════════════════════════════
   EVERY selector is scoped under .page--queue. Five of these class names are
   already taken by other screens with different meanings: .head and .reason
   belong to the approval screen, .chip to the admin tools, .alert and .table
   to the shared primitives. Scoping is by hand, not by a regex, because a
   regex is what double-prefixed a previous restyle. */
.page--queue { max-width: 1080px; }
.page--queue .head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-top: 0; }
.page--queue .h1 { font-family: var(--serif); font-size: 28px; font-weight: 400; color: var(--ink); margin: 0; }
.page--queue .h1__sub { font-size: 13px; color: var(--muted); margin: 4px 0 0; }
.page--queue .h1__sub b { color: var(--green-deep); font-weight: 600; }

/* alert bar · auto_send_delivery_failed only */
.page--queue .alert { background: var(--green-deep); border: 0; border-left: 5px solid var(--gold-light); border-radius: 6px; padding: 18px 22px; margin-top: 18px; display: flex; align-items: center; gap: 22px; flex-wrap: wrap; color: inherit; }
.page--queue .alert__main { flex: 1; }
.page--queue .alert__state { font-size: 10.5px; font-weight: 600; color: var(--gold-light); letter-spacing: .14em; }
.page--queue .alert__title { font-family: var(--serif); font-size: 18px; color: var(--off-white); margin: 7px 0 0; }
.page--queue .alert__body { font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.7); margin: 5px 0 0; }
.page--queue .alert__btn { display: inline-block; padding: 13px 22px; background: var(--gold-light); border: 0; border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; white-space: nowrap; text-decoration: none; }
.page--queue .alert__btn:hover { background: var(--gold); color: var(--green-deep); }

/* filter chips */
.page--queue .filters { display: flex; gap: 8px; flex-wrap: wrap; }
.page--queue .chip { padding: 7px 13px; border-radius: 100px; background: #fff; border: 1px solid rgba(27,67,50,.2); font-size: 10.5px; font-weight: 600; color: var(--muted); letter-spacing: .06em; white-space: nowrap; text-decoration: none; }
.page--queue .chip:hover { border-color: var(--gold); color: var(--gold-dark); }
.page--queue .chip--on { background: var(--green-deep); border-color: var(--green-deep); color: var(--off-white); }
.page--queue .chip--on:hover { background: var(--green-deep); color: var(--off-white); border-color: var(--green-deep); }
.page--queue .chip--fail { background: var(--green-deep); border-color: var(--green-deep); color: var(--gold-light); }
.page--queue .chip--fail:hover { background: var(--green-btn); border-color: var(--green-btn); color: var(--gold-light); }

/* shown instead of .filters when kill_switch accounts for every ordinary row */
.page--queue .offnote { background: #fff; border: 1px solid var(--line); border-left: 3px solid var(--gold); border-radius: 6px; padding: 12px 16px; margin: 18px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.page--queue .offnote b { color: var(--green-deep); font-weight: 600; }
.page--queue .offnote a { font-weight: 600; border-bottom: 1px solid rgba(201,162,39,.55); }

/* table */
.page--queue .table { background: #fff; border: 1px solid var(--line); border-radius: 6px; margin-top: 18px; overflow: hidden; }
.page--queue .row { display: grid; grid-template-columns: 1.8fr 1fr 1.5fr .7fr 1.6fr; gap: 14px; padding: 15px 22px; border-top: 1px solid var(--hair); align-items: center; border-left: 3px solid var(--gold); }
.page--queue .row--head { padding: 11px 22px; background: var(--cream); border-top: 0; border-left: 3px solid transparent; font-size: 9.5px; font-weight: 600; color: var(--faint); letter-spacing: .1em; }
.page--queue .row__actionhead { text-align: right; }
.page--queue .rep__name { font-size: 14px; font-weight: 600; color: var(--ink); }
.page--queue .rep__meta { display: block; font-size: 12px; color: var(--faint); margin-top: 2px; }
.page--queue .tutor { font-size: 13.5px; color: var(--body); }
.page--queue .age { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.page--queue .age--old { color: var(--gold-dark); }          /* past 24 hours */
.page--queue .acts { display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
.page--queue .acts__history { font-size: 11.5px; color: var(--muted); border-bottom: 1px solid rgba(27,67,50,.2); }
/* ARCHIVE, quieter than Review on purpose. The two sit side by side and must not
   read as an even choice: approving is the ordinary path and archiving is the
   exception. Outline rather than fill, and never the danger red -- archiving
   loses nothing and the toast offers Undo. */
.acts__archive {
  display: inline-block; padding: 10px 14px; background: transparent;
  border: 1px solid rgba(27,67,50,.28); border-radius: 4px;
  font-family: var(--sans); font-size: 11.5px; font-weight: 600;
  color: var(--green-btn); letter-spacing: .04em; white-space: nowrap;
  cursor: pointer; transition: background .15s, border-color .15s;
}
.acts__archive:hover { background: rgba(27,67,50,.06); border-color: var(--green-btn); }
.acts__archive[disabled] { opacity: .55; cursor: default; }
/* The form exists so Archive works with no script. It has to be a flex item in
   the action row, or the inline default drops the button off its neighbours'
   baseline. */
.acts form.inline { display: flex; margin: 0; }
.page--queue .acts__history:hover { color: var(--gold-dark); border-color: var(--gold); }
.page--queue .acts__review { display: inline-block; padding: 10px 16px; background: var(--green-btn); border: 0; border-radius: 4px; font-family: var(--sans); font-size: 11.5px; font-weight: 600; color: #fff; letter-spacing: .04em; white-space: nowrap; cursor: pointer; transition: background .15s; text-decoration: none; }
.page--queue .acts__review:hover { background: var(--green-deep); color: #fff; }

/* TIER 1 · the six routing holds. One gold pill, all identical weight: none is
   more urgent than another, only older or newer. */
.page--queue .reason { display: inline-block; padding: 5px 11px; border-radius: 100px; background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); font-size: 9.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; }
/* MODEL FLAGGED's detail stays the same faint grey as every other detail line.
   The flag TYPE has no effect on routing (any flag queues the report), so it
   must never read as a severity signal. */
.page--queue .reason__detail { display: block; font-size: 11px; color: var(--faint); margin-top: 4px; }
/* PARENT OPTED OUT, on the queue list. Informational, so it is the quietest mark
   in this cell: the routing reason above it is the thing that says why the report
   is held, and gold or red here would compete with it or read as a fault. */
.page--queue .reason__optout {
  display: inline-block; margin-top: 6px; padding: 4px 9px; border-radius: 100px;
  background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.28);
  font-size: 9px; font-weight: 600; color: rgba(255,255,255,.82); letter-spacing: .08em;
  white-space: nowrap;
}

/* TIER 2 · the failures. Inverted rather than another gold pill: these are not
   "held for review", they are things that went wrong. Red stays reserved for
   irreversible destruction on /admin. */
.page--queue .reason--fail { background: var(--green-deep); border-color: var(--green-deep); color: var(--gold-light); letter-spacing: .08em; }
.page--queue .row--fail { background: rgba(20,55,42,.05); border-left-width: 5px; border-left-color: var(--green-deep); }
.page--queue .row--fail .reason__detail { color: var(--muted); }
.page--queue .acts__retry { padding: 10px 16px; background: #fff; border: 1.5px solid var(--green-btn); border-radius: 4px; font-family: var(--sans); font-size: 11.5px; font-weight: 600; color: var(--green-btn); letter-spacing: .04em; white-space: nowrap; cursor: pointer; }
.page--queue .acts__retry:hover { background: var(--green-btn); color: #fff; }
.page--queue .acts__retry[disabled] { border-color: rgba(27,67,50,.25); color: var(--faint); background: #fff; cursor: default; }

/* TIER 3 · sent with no human in the loop, then the send threw. */
.page--queue .reason--critical { background: var(--gold-light); border-color: var(--gold-light); color: var(--green-deep); letter-spacing: .08em; }
.page--queue .row--critical { background: rgba(227,181,58,.13); border-left-width: 5px; border-left-color: var(--gold-light); }
.page--queue .row--critical .rep__name, .page--queue .row--critical .age { color: var(--green-deep); }
.page--queue .row--critical .reason__detail { color: var(--green-btn); font-weight: 600; }

.page--queue .tablenote { font-size: 11.5px; line-height: 1.6; color: var(--faint); margin: 10px 0 0; }

/* empty state, both variants */
.page--queue .clear { background: var(--green-deep); border-radius: 8px; padding: 44px 32px; text-align: center; margin-top: 18px; }
.page--queue .clear__mark { width: 52px; height: 52px; margin: 0 auto; border-radius: 50%; border: 1.5px solid var(--gold); background: rgba(227,181,58,.12); display: flex; align-items: center; justify-content: center; font-family: var(--serif); font-size: 22px; color: var(--gold-light); }
.page--queue .clear__title { font-family: var(--serif); font-size: 26px; font-weight: 400; color: var(--off-white); margin: 18px 0 0; }
.page--queue .clear__title em { color: var(--gold-light); font-style: normal; }
.page--queue .clear__body { font-size: 13.5px; line-height: 1.7; color: rgba(255,255,255,.72); margin: 10px 0 0; }
.page--queue .clear__stats { display: flex; align-items: center; justify-content: center; gap: 20px; flex-wrap: wrap; margin-top: 22px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,.14); font-size: 12.5px; color: rgba(255,255,255,.6); }
.page--queue .clear__stats b { color: var(--gold-light); font-weight: 600; }
.page--queue .clear__btn { display: inline-block; margin-top: 20px; padding: 13px 24px; background: var(--gold-light); border: 0; border-radius: 4px; font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; text-decoration: none; }
.page--queue .clear__btn:hover { background: var(--gold); color: var(--green-deep); }

@media (max-width: 900px) {
  .page--queue .row { grid-template-columns: 1fr 1fr; gap: 10px 14px; padding: 16px; }
  .page--queue .row--head { display: none; }
  .page--queue .row > .rep { grid-column: 1 / -1; }
  /* Four controls now, so the row stays one line and tightens rather than
     wrapping each button onto its own. Same reasoning as the dashboard's queue
     row: these belong together at the foot of the card. */
  .page--queue .acts { grid-column: 1 / -1; justify-content: flex-start; margin-top: 4px; gap: 10px; flex-wrap: nowrap; }
  .page--queue .acts__review { flex: 1 1 auto; text-align: center; }
  .page--queue .acts__archive { flex: 0 0 auto; padding: 10px 12px; }
  .page--queue .acts__history { flex: 0 0 auto; }
  .page--queue .alert { flex-direction: column; align-items: flex-start; gap: 14px; }
  .page--queue .alert__btn { width: 100%; text-align: center; }
}
@media (max-width: 760px), (max-height: 499px) {
  .page--queue .head { align-items: flex-start; }
  .page--queue .clear { padding: 36px 22px; border-radius: 6px; }
  .page--queue .clear__btn { display: block; width: 100%; }
}

/* ══ Report history (design/history.html) ══════════════════════════════════
   Every selector is scoped under .page--history. Two of these class names are
   already taken with different meanings: .who belongs to the parent email
   template and .chip to the admin tools. Scoping is by hand, not by a regex,
   because a regex is what double-prefixed a previous restyle. */
.page--history { max-width: 860px; padding-top: 22px; }
.page--history .crumb { font-size: 11.5px; color: var(--faint); }
.page--history .crumb a { color: var(--faint); border-bottom: 1px solid rgba(27,67,50,.2); }
.page--history .crumb a:hover { color: var(--gold-dark); border-color: var(--gold); }
.page--history .crumb b { color: var(--green-deep); font-weight: 600; }

/* student header */
.page--history .who { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-top: 14px; padding-bottom: 16px; border-bottom: 1px solid var(--line); color: inherit; font-size: inherit; }
.page--history .who__name { font-family: var(--serif); font-size: 28px; font-weight: 400; color: var(--ink); margin: 0; }
.page--history .who__meta { font-size: 12.5px; color: var(--muted); margin: 4px 0 0; }
.page--history .who__meta span { color: var(--faint); }
.page--history .who__stats { display: flex; gap: 22px; text-align: right; }
.page--history .stat__n { font-family: var(--serif); font-size: 22px; color: var(--green-deep); }
.page--history .stat__l { font-size: 10.5px; color: var(--faint); letter-spacing: .06em; }

/* one report. position: relative only so the toggle can be placed onto the header
   line from here, without the markup moving. See the disclosure block below. */
.page--history .entry { position: relative; background: var(--off-white); border: 1px solid var(--line); border-left: 3px solid var(--gold); border-radius: 4px; padding: 16px 20px; margin-top: 10px; }
/* Nothing reached the parent, so the row recedes: grey rail, grey chip, muted
   date. A returned report is not a smaller version of a sent one, it is a
   non-event, and it should read as one without needing the chip. */
.page--history .entry--dead { border-left-color: rgba(27,67,50,.25); }
/* A failure is not a non-event. It keeps full weight and takes the dark rail. */
.page--history .entry--fail { border-left-color: var(--green-deep); border-left-width: 5px; background: rgba(20,55,42,.05); }

/* Always visible, in both states: it is a sibling of <details>, not a child, so
   nothing here can be collapsed away. No ordering needed now that the DOM order
   is the reading order.
   padding-right reserves the strip the toggle is positioned into, so a long meta
   line wraps before it reaches the toggle instead of running underneath it. */
.page--history .entry__line { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-right: 96px; }
.page--history .entry__date { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.page--history .entry--dead .entry__date { color: var(--muted); }
.page--history .entry__meta { font-size: 12.5px; color: var(--faint); }
.page--history .entry__meta b { color: var(--green-deep); font-weight: 600; }
.page--history .entry__meta a { border-bottom: 1px solid rgba(201,162,39,.55); }

/* status chip: plain English, never the raw enum */
.page--history .chip { display: inline-block; padding: 5px 11px; border-radius: 100px; background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); font-size: 9.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; white-space: nowrap; }
.page--history .chip--dead { background: rgba(27,67,50,.08); border-color: rgba(27,67,50,.18); color: var(--muted); }
.page--history .chip--fail { background: var(--green-deep); border-color: var(--green-deep); color: var(--gold-light); letter-spacing: .08em; }

/* MARKER chip. A secondary fact about a report whose status chip already told
   the main story, so it is an outline rather than a fill and sits smaller. Its
   own class on purpose: further markers can be added without touching the row. */
.page--history .chip--marker { display: inline-block; padding: 4px 9px; border-radius: 100px; background: transparent; border: 1px solid var(--gold); font-size: 8.5px; font-weight: 600; color: var(--gold-dark); letter-spacing: .08em; white-space: nowrap; margin-left: -6px; }

/* disclosure. <details>, so it works with no JavaScript under script-src 'self'.
   It holds ONLY the toggle and the body: the header line is a sibling above it.
   The details element keeps its own real box, in normal flow and full width, so
   the browser's native collapse is untouched and the body spans the whole row.
   No contents-style display, and the summary is never moved in the markup.

   The toggle is POSITIONED onto the header's line rather than living there: the
   summary is taken out of flow and pinned to the top right of the row. Two
   consequences, both wanted. A collapsed row is exactly one line tall, because
   the details element then contributes no height at all (its only in-flow child,
   the body, is hidden). And the toggle holds the same place whether the row is
   open or closed, because its position is fixed to the row rather than to the
   flow above it.

   margin: 0 on details is load-bearing for the one-line height. The body carries
   its own top spacing, so the gap under the header is unchanged when open.
   `right` matches the row's own horizontal padding. `top` is the row's 16px top
   padding plus 4px, which centres the 11.5px toggle against the taller chip on
   the header line; it is the one tuned value here. */
.page--history .entry details { margin: 0; display: flex; flex-direction: column; }
.page--history .entry summary { position: absolute; top: 20px; right: 20px; align-self: flex-end; list-style: none; cursor: pointer; font-size: 11.5px; color: var(--green-btn); border-bottom: 1px solid rgba(201,162,39,.55); }
.page--history .entry summary::-webkit-details-marker { display: none; }
.page--history .entry summary::after { content: ' \25BE'; }
.page--history .entry details[open] summary::after { content: ' \25B4'; }
.page--history .entry summary:hover { color: var(--gold-dark); }
.page--history .entry--dead summary { color: var(--muted); border-color: rgba(27,67,50,.2); }

/* 20px, absorbing the 10px that <details> used to carry plus its own 10px, so the
   gap between the header and the body is the same as before the toggle moved. */
.page--history .body { margin-top: 20px; padding-top: 14px; border-top: 1px solid rgba(27,67,50,.1); }
.page--history .body__note { font-family: var(--serif); font-style: italic; font-size: 13px; line-height: 1.7; color: var(--body); margin: 0; }
/* Why a report came back. Rendered only when a note was actually recorded. */
.page--history .body__returned { background: #fff; border: 1px solid var(--line); border-left: 3px solid rgba(27,67,50,.3); border-radius: 4px; padding: 10px 13px; margin: 0 0 12px; font-size: 12.5px; line-height: 1.6; color: var(--body); }
.page--history .body__returnedlabel { display: block; font-size: 9.5px; font-weight: 600; color: var(--muted); letter-spacing: .1em; margin-bottom: 3px; }
.page--history .sec { padding: 9px 0; border-top: 1px solid var(--hair); }
.page--history .sec__l { font-size: 10px; font-weight: 600; color: var(--gold-dark); letter-spacing: .1em; }
.page--history .sec__t { font-size: 12.5px; line-height: 1.65; color: var(--muted); margin-top: 3px; }

.page--history .foot { font-size: 11.5px; color: var(--faint); margin-top: 16px; }
.page--history .foot a { border-bottom: 1px solid rgba(201,162,39,.55); }

/* pagination: plain links, twenty per page, no infinite scroll */
.page--history .pager { display: flex; gap: 14px; margin-top: 12px; }
.page--history .pager__link { font-size: 12px; font-weight: 600; color: var(--green-btn); border-bottom: 1px solid rgba(201,162,39,.55); }
.page--history .pager__link:hover { color: var(--gold-dark); border-color: var(--gold); }

/* empty state */
.page--history .none { background: var(--green-deep); border-radius: 8px; padding: 40px 30px; text-align: center; margin-top: 16px; }
.page--history .none__title { font-family: var(--serif); font-size: 24px; font-weight: 400; color: var(--off-white); margin: 0; }
.page--history .none__title em { color: var(--gold-light); font-style: normal; }
.page--history .none__body { font-size: 13px; line-height: 1.7; color: rgba(255,255,255,.7); margin: 10px 0 0; }

@media (max-width: 760px), (max-height: 499px) {
  .page--history .who__stats { gap: 16px; text-align: left; }
  /* On a phone there is not enough width to hold a 96px strip aside for the
     toggle without squeezing the meta line, so the toggle returns to the flow and
     takes its own line under the header. The row is taller here, which is the
     right trade on a narrow screen: nothing overlaps and nothing is cramped.
     Structure is untouched either way, only where the summary is painted. */
  .page--history .entry__line { gap: 8px; padding-right: 0; }
  .page--history .entry summary { position: static; top: auto; right: auto; }
  .page--history .entry details { margin-top: 8px; }
  /* The details element carries the gap again on this breakpoint, so the body
     goes back to its own smaller margin. */
  .page--history .body { margin-top: 10px; }
}

/* ══ Sent reports (design/sent.html) ═══════════════════════════════════════
   Scoped under .page--sent by hand. .table, .row and .none are shared names
   used differently by the queue and the history page, so none of this may leak.
   Scoping is manual, not by a regex, because a regex is what double-prefixed a
   previous restyle. */
.page--sent { max-width: 1080px; }
.page--sent .h1 { font-family: var(--serif); font-size: 28px; font-weight: 400; color: var(--ink); margin: 0; }
.page--sent .h1__sub { font-size: 13px; line-height: 1.6; color: var(--muted); margin: 6px 0 0; max-width: 640px; }

/* table, same anatomy as the queue */
.page--sent .table { background: #fff; border: 1px solid var(--line); border-radius: 6px; margin-top: 18px; overflow: hidden; }
.page--sent .row { display: grid; grid-template-columns: 1.1fr 1.5fr 1.2fr 1.4fr .5fr 1.3fr .8fr; gap: 14px; padding: 14px 22px; border-top: 1px solid var(--hair); align-items: center; }
.page--sent .row--head { padding: 11px 22px; background: var(--cream); border-top: 0; font-size: 9.5px; font-weight: 600; color: var(--faint); letter-spacing: .1em; }
/* The mock put text-align:right inline on this spacer. It lives here instead. */
.page--sent .row__actionhead { text-align: right; }
.page--sent .sent__date { font-size: 13px; font-weight: 600; color: var(--ink); white-space: nowrap; }
.page--sent .sent__student { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.page--sent .sent__plain { font-size: 13px; color: var(--muted); }
.page--sent .sent__grade { font-family: var(--serif); font-size: 15px; color: var(--green-deep); }
.page--sent .sent__history { justify-self: end; font-size: 11.5px; color: var(--muted); border-bottom: 1px solid rgba(27,67,50,.2); white-space: nowrap; }
.page--sent .sent__history:hover { color: var(--gold-dark); border-color: var(--gold); }

/* HOW chips. Plain English, never the raw enum. The only distinction that matters
   on this page is whether a human read the report before the parent did. */
.page--sent .how { display: inline-block; padding: 5px 11px; border-radius: 100px; background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35); font-size: 9.5px; font-weight: 600; color: var(--green-deep); letter-spacing: .06em; white-space: nowrap; }
.page--sent .how--auto { background: rgba(27,67,50,.07); border-color: rgba(27,67,50,.18); color: var(--muted); }

.page--sent .tablenote { font-size: 11.5px; line-height: 1.6; color: var(--faint); margin: 10px 0 0; }
/* The mock coloured these two words with inline styles. Classes instead. */
.page--sent .tablenote__approved { color: var(--green-deep); font-weight: 600; }
.page--sent .tablenote__auto { color: var(--muted); font-weight: 600; }

/* pagination: plain links, no JavaScript */
.page--sent .pager { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 16px; }
.page--sent .pager__link { font-size: 12px; font-weight: 600; color: var(--green-btn); border-bottom: 1px solid rgba(201,162,39,.55); padding-bottom: 1px; }
.page--sent .pager__link:hover { color: var(--gold-dark); border-color: var(--gold); }
/* Rendered as a span, so it is genuinely not clickable or focusable rather than
   merely looking disabled. */
.page--sent .pager__link--off { color: var(--faint); border-color: transparent; pointer-events: none; }
.page--sent .pager__where { font-size: 11.5px; color: var(--faint); }

/* empty state */
.page--sent .none { background: var(--green-deep); border-radius: 8px; padding: 44px 32px; text-align: center; margin-top: 18px; }
.page--sent .none__title { font-family: var(--serif); font-size: 26px; font-weight: 400; color: var(--off-white); margin: 0; }
.page--sent .none__title em { color: var(--gold-light); font-style: normal; }
.page--sent .none__body { font-size: 13.5px; line-height: 1.7; color: rgba(255,255,255,.72); margin: 10px 0 0; }

@media (max-width: 900px) {
  .page--sent .row { grid-template-columns: 1fr 1fr; gap: 6px 14px; padding: 16px; }
  .page--sent .row--head { display: none; }
  .page--sent .sent__student { grid-column: 1; grid-row: 1; }
  .page--sent .sent__date { grid-column: 2; grid-row: 1; justify-self: end; color: var(--muted); font-weight: 400; font-size: 12px; }
  .page--sent .sent__history { grid-column: 2; justify-self: end; }
}
@media (max-width: 760px), (max-height: 499px) {
  /* Same 760px breakpoint the rest of the portal uses, NOT a new one, and this
     comment sits inside the block rather than above it so that stripping the
     file's media blocks leaves a byte-identical remainder.

     The 900px rules above still apply down here; this block only takes the row
     the rest of the way, from a two-column card to one fact per line.

     ONE FACT PER LINE, CAPTIONED.
     At 320px the 900px layout is still a desktop row folded in half: the student
     shares a line with the date, the grade and the HOW pill sit at different
     heights, and History hangs off the right edge. Here the row reads like the
     Add tutor and Add student forms instead, which is the only pattern on this
     site for "a value that needs saying what it is": a small green caption over
     the value it names, one per line, nothing side by side.

     Every child is reset to span the full width and to place itself in flow, so
     the grid-row / grid-column / justify-self placements from the 900px block
     stop applying. The grade and the HOW pill are the two exceptions, because
     those two share a line on purpose. */
  .page--sent .row {
    grid-template-columns: max-content 1fr;
    align-items: baseline;
    gap: 7px 10px;
    padding: 16px;
  }
  .page--sent .row > * {
    grid-column: 1 / -1;
    grid-row: auto;
    justify-self: start;
  }

  /* The student is the subject of the card, so it leads and takes the only size
     step. `order`, not a markup move: the DOM stays SENT-then-STUDENT for the
     desktop column order. */
  .page--sent .sent__student { order: -1; font-size: 15px; }

  /* The date is a value under a caption now, not the muted right-hand corner it
     became at 900px, so it goes back to reading like the other values. */
  .page--sent .sent__date { font-size: 13px; font-weight: 400; color: var(--body); }
  .page--sent .sent__plain { color: var(--body); }

  /* The grade keeps column 1 and the pill takes column 2, which is what puts
     them on one line, and `align-items: baseline` on the row is what levels the
     pill's text with the grade's rather than centring two boxes of different
     heights against each other. */
  .page--sent .sent__grade { grid-column: 1; }
  .page--sent .sent__how { grid-column: 2; }

  /* CAPTIONS. Drawn from the markup's data-label by a pseudo-element rather than
     added to the DOM, so nothing above 760px gains an element and the caption
     text still lives in the template where it can be read and changed.

     Taken OUT of flow, with the value reserving its height as padding, for one
     specific reason: a caption in flow would become the grade cell's first
     baseline, and the pill beside it would then line up with the word GRADE
     instead of with the grade. Out of flow, the value's own text is the cell's
     baseline and the pill lands where it should. */
  .page--sent .row [data-label] { position: relative; padding-top: 17px; }
  .page--sent .row [data-label]::before {
    content: attr(data-label);
    position: absolute;
    top: 0;
    left: 0;
    /* Stated, not inherited: the grade cell is serif, and without this its
       caption would be the one caption on the card set in Libre Caslon. */
    font-family: var(--sans);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--green-deep);
    letter-spacing: .04em;
    white-space: nowrap;
  }
}
/* ══ Session length tiles (design/survey.html) ═════════════════════════════
   Same tile language as the grade grid, six across. Nothing is pre-checked and
   there is no default: this value drives pay. */
.mins { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.mins input { position: absolute; opacity: 0; width: 0; height: 0; }
/* Same fill token as the text inputs, so a tile reads as a field the tutor fills
   in rather than as a lighter card. Hover deliberately leaves the background
   alone, so idle and hover stay identical to a text input. */
.mins label { height: 46px; border: 1px solid var(--field); border-radius: 4px; background: var(--field-bg); display: flex; align-items: center; justify-content: center; font-family: var(--sans); font-size: 13px; font-weight: 600; color: var(--faint); cursor: pointer; transition: .12s; }
.mins label:hover { border-color: var(--gold); color: var(--green-deep); }
.mins input:checked + label { background: var(--green-deep); border: 1.5px solid var(--gold); color: var(--gold-light); }
.mins input:focus-visible + label { box-shadow: 0 0 0 3px rgba(201,162,39,.3); }

/* ══ Grade RANGE picker (design/survey.html) ═══════════════════════════════
   The tiles are <button type="button"> now, not radios, because a range is two
   coupled choices rather than one. The submitted values live in two hidden
   inputs, and /static/survey.js drives the classes below. Every rule the script
   enforces is repeated server side; this is the interaction, not the guarantee.

   Used by BOTH the tutor survey and the approval screen, so an admin override is
   the same interaction under the same one or two step rule. */
/* Same fill token as the text inputs, matching the session length tiles above. */
.grades button { height: 46px; min-width: 0; border: 1px solid var(--field); border-radius: 4px; background: var(--field-bg); font-family: var(--serif); font-size: 15px; font-weight: 600; color: var(--faint); cursor: pointer; transition: .12s; padding: 0; }
.grades button:hover { border-color: var(--gold); color: var(--green-deep); }
.grades button:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(201,162,39,.3); }
/* Both ends of the range. */
.grades button.is-endpoint { background: var(--green-deep); border: 1.5px solid var(--gold); color: var(--gold-light); }
/* The middle grade of a three-grade span. It now fills IDENTICALLY to the two
   endpoints, so a selected span reads as one solid block rather than a bar with a
   lighter centre. The class stays distinct in the markup on purpose: endpoints are
   still the only tiles that clear the range when tapped, so the behaviour differs
   even though the fill no longer does. */
.grades button.is-between { background: var(--green-deep); border: 1.5px solid var(--gold); color: var(--gold-light); }
/* Out of reach while a floor is being picked, so an invalid span cannot be tapped. */
.grades button[disabled] { opacity: .35; cursor: default; }
.grades button.is-word { font-size: 10.5px; font-family: var(--sans); letter-spacing: .02em; line-height: 1.15; }

/* THE SAME GUARD c65e5a5 EARNED. .grades carries display:grid, and an
   author-origin display outranks the user agent's [hidden]{display:none}, so a
   hidden band grid would sit on screen in full. That is exactly how every
   deactivated roster row ended up visible in production. Scoped to this
   question and placed AHEAD of the declarations it protects. */
.q [hidden] { display: none !important; }

/* ── question 5's mode, section and reason chips ────────────────────────────
   ONE PILL CLASS, three users: the mode row, the section row and the skip
   reasons. They are the same control with different contents, and three copies
   of one pill is how they drift.

   Selected and unselected are the only two states. The gold-outlined SKIP panel
   is NOT here: it reuses .extra__panel and .extra__kicker from the + Parent
   block, which already carry these exact values. */
.pill {
  display: inline-flex; align-items: center;
  padding: 4px 11px; border-radius: 100px;
  border: 1px solid rgba(27, 67, 50, .2); background: var(--field-bg);
  font-family: var(--sans); font-size: 9.5px; font-weight: 600;
  letter-spacing: .08em; color: var(--muted);
  cursor: pointer; white-space: nowrap; transition: .12s;
}
.pill:hover { border-color: var(--gold); color: var(--green-deep); }
.pill:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(201,162,39,.3); }
.pill.is-on {
  background: var(--green-deep); border: 1.5px solid var(--gold); color: var(--gold-light);
}
/* The section and reason chips carry words rather than a shouted label, so they
   read at 10px without the wider tracking. */
.pill--section { font-size: 10px; letter-spacing: .02em; }

/* The mode row sits on the question's header line, pushed right. It wraps on a
   narrow card rather than squeezing the question title. */
.q__modes { display: flex; gap: 5px; margin-left: auto; flex-wrap: wrap; }

.q__sections { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0 0 11px; }
.q__sections--reason { margin: 11px 0 0; }
.q__sectionlbl {
  font-family: var(--sans); font-size: 9.5px; font-weight: 600;
  color: var(--gold-dark); letter-spacing: .12em; margin-right: 2px;
}

/* Inside the skip panel. The panel itself is .extra__panel. */
.q__skipbody { font-size: 12.5px; line-height: 1.6; color: var(--ink); margin: 7px 0 0; }
.q__skipnote { font-size: 11px; color: var(--faint); margin: 9px 0 0; }
/* Score bands are four across, not eight: the labels are ranges rather than
   single grades and will not fit an eighth of the card. */
.grades--bands { grid-template-columns: repeat(4, 1fr); }
/* min-width:0 is on .grades button already; this only drops the type so a
   "Reading & Writing" band label fits its cell. */
.grades button.is-band { font-size: 12px; font-family: var(--sans); letter-spacing: 0; }

/* Reads the range back low to high, whichever order it was tapped in. */
.grades-readout { display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 12.5px; min-height: 18px; }
.grades-readout__dash { display: inline-block; width: 22px; height: 2px; background: var(--gold); flex: none; }
.grades-readout__val { font-weight: 600; color: var(--green-deep); }
.grades-readout__hint { color: var(--faint); font-size: 11.5px; }

@media (max-width: 620px) {
  .mins { grid-template-columns: repeat(3, 1fr); }
}

/* --- hourly rate: dollars in, cents stored -------------------------------
   Admin-only payroll fields. The $ sits inside the field's frame so nobody
   wonders whether the input wants dollars or cents. From design/admin-tools.html,
   with the mock's inline style attributes folded into these classes. */
.money { position: relative; }
.money__sign {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: var(--faint);
  pointer-events: none;
}
.money .in { padding-left: 24px; }

/* A tutor with no rate set needs fixing, and must read that way. Every tutor
   who predates the column is in this state until an admin sets one. Gold, not
   red: red stays reserved for permanent destruction on the admin page. */
.norate {
  display: inline-block;
  margin-left: 6px;
  padding: 3px 9px;
  border-radius: 100px;
  background: rgba(201, 162, 39, .16);
  border: 1px solid rgba(168, 132, 42, .45);
  font-size: 9.5px;
  font-weight: 600;
  /* Stated rather than inherited, and the ONE line this change makes outside its
     own component. body sets 16px/1.5, so this badge computed a 14.25px line box
     against the caseload pill's 13.3px and sat a pixel taller than the thing it
     is meant to match exactly. Pinning both to 1.4 is what makes them the same
     size; the alternative, moving the pill to 1.5, would contradict the size the
     pill was specified at. */
  line-height: 1.4;
  color: var(--gold-dark);
  letter-spacing: .06em;
  white-space: nowrap;
}

/* --- the charge rate field ------------------------------------------------
   What Rhythm CHARGES a parent, admin only. Almost nothing new is styled: the
   field is .lbl + .money + .in + .note, the pill is .tzpill, the unset chip is
   .norate and the per-student toggle is .optout. What is here is the row that
   puts a label beside the input, and the gold backdate box.

   ── [hidden] MUST WIN, AND ON THIS CODEBASE THAT IS NOT AUTOMATIC ─────────

   Every block below that the script opens and closes carries an explicit
   [hidden] rule. The user agent's own `[hidden]{display:none}` is beaten by ANY
   author-origin display declaration at equal or higher specificity, which is
   exactly how commit c65e5a5 left every deactivated and archived roster row in
   plain sight on production: .roster__row had an author display:flex, the rows
   were marked hidden, and the attribute did nothing. These rules give the same
   attribute on the same kind of element an author-origin declaration of its own
   so it cannot lose the same way. */
.rate__line { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.rate__line[hidden] { display: none; }
.rate__line .money { flex: 0 1 130px; }
.rate__in { width: 100%; }
.rate__date { flex: 0 1 165px; }

/* "since 1 January 2026" and "was $85": the same quiet weight, because both are
   context for the number beside them rather than facts of their own. */
.rate__since,
.rate__was,
.rate__today {
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--faint);
}
.rate__since[hidden], .rate__was[hidden], .rate__today[hidden] { display: none; }
/* `was $85` is the one an admin is meant to catch a typo against, so it carries
   the ink the value has rather than the muted grey. */
.rate__was { font-weight: 600; color: var(--ink); }

.rate__when[hidden], .rate__student[hidden], .rate__past[hidden] { display: none; }
/* Indented, because it belongs to the field above it rather than standing beside
   it. The per-student block is the same shape one level in. */
.rate__when { margin-top: 4px; }
.rate__student {
  margin-top: 10px; padding-left: 14px;
  border-left: 2px solid rgba(27, 67, 50, .12);
}

/* The prior-rates line: one quiet line, never a table. Two or three fit inline,
   and a family with more has a story that wants a full view rather than a taller
   field. */
.rate__earlier { color: var(--faint); }

/* THE BACKDATE WARNING. Gold on #FDF7E6, the spec's own validation treatment.
   NO RED: a backdated rate is unusual, not wrong, and red on this page is
   reserved for permanent destruction. */
.rate__past {
  margin-top: 10px; padding: 11px 13px;
  background: #FDF7E6; border: 2px solid var(--gold);
  border-radius: 3px;
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.6;
  color: var(--gold-ink);
}

@media (max-width: 720px) {
  /* THE BASIS-BECOMES-HEIGHT TRAP, which this row walks into exactly as the
     tagform row does: `flex: 0 1 130px` in a column is 130px TALL. Returned to
     the content's own height, at the same specificity, after the rules it
     undoes. The row itself stays a row where it fits and stacks where it does
     not, so nothing here forces a column. */
  .rate__line .money { flex: 0 0 auto; width: 100%; }
  .rate__date { flex: 0 0 auto; width: 100%; }
}

/* --- tutor earnings card -------------------------------------------------
   From design/dashboard.html. Sits below the dashboard counters and above the
   student list, tutor facing only.

   Dollars are computed server side from filed surveys: the survey's session
   minutes times the session's STAMPED hourly rate. The rate itself never
   renders here and there is no per session breakdown. */
.earn {
  background: var(--green-deep);
  border-radius: 6px;
  padding: 20px 22px;
  margin-top: 14px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
}
.earn__label {
  font-size: 10px;
  font-weight: 600;
  color: var(--gold-light);
  letter-spacing: .14em;
}
.earn__value {
  font-family: var(--serif);
  font-size: 44px;
  line-height: 1;
  color: var(--off-white);
  margin-top: 8px;
}
/* The hours behind the amount above. Quiet: the dollar figure is the headline
   and this explains it, so it must not compete with it. */
.earn__hours {
  font-size: 12.5px;
  color: var(--gold-light);
  margin-top: 7px;
}
.earn__hint {
  font-size: 11px;
  line-height: 1.55;
  color: rgba(255, 255, 255, .6);
  margin-top: 10px;
  max-width: 340px;
}
.earn__hint b { color: rgba(255, 255, 255, .85); font-weight: 600; }
.earn__stats { display: flex; gap: 26px; text-align: right; }
.earn__n {
  font-family: var(--serif);
  font-size: 22px;
  color: var(--gold-light);
}
.earn__l {
  font-size: 9.5px;
  font-weight: 600;
  color: rgba(255, 255, 255, .55);
  letter-spacing: .1em;
  margin-top: 3px;
}
.earn__week {
  flex-basis: 100%;
  font-size: 10px;
  font-weight: 600;
  color: rgba(255, 255, 255, .45);
  letter-spacing: .1em;
  margin-top: 2px;
}

@media (max-width: 760px), (max-height: 499px) {
  /* The big number stacks above the month stats, which left align under it. */
  .earn {
    padding: 18px;
    align-items: flex-start;
    flex-direction: column;
    gap: 14px;
  }
  .earn__value { font-size: 40px; }
  .earn__stats { text-align: left; }
}

/* --- announcements -------------------------------------------------------
   A one way broadcast from admins to every active tutor's dashboard. From
   design/dashboard.html and design/admin-tools.html, with every inline style
   attribute in those mocks folded into these classes. Portal only: nothing in
   this feature is ever emailed or shown to a parent. */

/* Tutor side: between the counters and the earnings card. */
.news {
  background: #fff;
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: 6px;
  padding: 16px 20px;
  margin-top: 14px;
}
.news__head { display: flex; align-items: center; gap: 10px; }
.news__label {
  font-size: 9.5px;
  font-weight: 600;
  color: var(--gold-dark);
  letter-spacing: .14em;
}
/* A PUSH LANDS HERE. `/#announcement-12` jumps to a row, and a target flush
   against the viewport edge reads as the top of a list rather than as the thing
   that was tapped.
   MEASURED, NOT COPIED FROM THE ADMIN RAIL. That rail needs 57px because a
   sticky chip row covers its targets; the dashboard has no sticky or fixed
   element above the content (.nav is position: static and scrolls away, and the
   mobile chrome is the bottom .tabbar), so nothing is covering this and the
   value is breathing room rather than clearance.
   NO HIGHLIGHT ON ARRIVAL. It would be a second unread vocabulary competing
   with the button-against-chip one already working. */
.news__item { display: flex; align-items: center; gap: 16px; margin-top: 8px; scroll-margin-top: 16px; }
.news__item + .news__item {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(27, 67, 50, .1);
}
/* The mock used style="flex:1" here. */
.news__main { flex: 1; }
.news__title { font-size: 14px; font-weight: 600; color: var(--ink); }
.news__body { font-size: 12.5px; line-height: 1.6; color: var(--muted); margin-top: 3px; }
/* The age line sits under the message rather than in the header, because there
   is one line per announcement and the section holds several. */
.news__age { font-size: 11px; color: var(--faint); margin-top: 5px; }
.news__readform { margin: 0; }
.news__read {
  padding: 9px 16px;
  background: var(--green-btn);
  border: 0;
  border-radius: 4px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  color: #fff;
  letter-spacing: .04em;
  white-space: nowrap;
  cursor: pointer;
}
.news__read:hover { background: var(--green-deep); }
/* The quiet read state. Still visible: reading never hides an announcement. */
.news__done {
  padding: 8px 14px;
  border: 1px solid rgba(27, 67, 50, .25);
  border-radius: 4px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--faint);
  letter-spacing: .04em;
  white-space: nowrap;
}

/* Admin side: the composer and the live list, below the add cards. */
/* ⚠ THE CARD OWNS ITS OWN EDGES NOW. design_handoff_admin_tools draws this as
   a bordered box with a header band and a hairline between the two columns,
   two rows tall instead of eight, so the padding moves off the card and onto
   the band and the columns. */
.ann { margin-top: 22px; padding: 0; overflow: hidden; }
.ann__head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 14px 20px;
  border-bottom: 1px solid rgba(27, 67, 50, .09);
}
.ann__eyebrow { font-size: 10.5px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.ann__hint { font-size: 11.5px; color: var(--faint); }
/* Pushed to the right edge. margin-left:auto is correct HERE and wrong on the
   nav: nothing in this row is trying to flex-grow, so there is no growth for
   the auto margin to absorb. */
.ann__active { margin-left: auto; font-size: 11.5px; color: var(--muted); }
/* ⚠ 1.35fr TO 1fr WITH A HAIRLINE BETWEEN, not two equal columns with a gap.
   The composer is the wider half because it holds two fields and a button. */
.ann__cols {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  margin-top: 0;
  align-items: start;
}
.ann__cols > .ann__compose { border-right: 1px solid rgba(27, 67, 50, .09); }
.ann__compose, .ann__live { padding: 16px 20px 18px; }
/* A form carries a default margin in some engines, so it is zeroed here: the
   column has to begin exactly at the container edge for its first label to line
   up with the ACTIVE header opposite. */
.ann__compose { min-width: 0; margin: 0; }
.ann__live { min-width: 0; }
.ann__actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
  flex-wrap: wrap;
}
/* The two counters under the composer's fields. One line, wrapping to two on a
   narrow screen, with the verdict quieter than the number: the number is a fact
   and the verdict is advice. */
.ann__count {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 6px 0 0;
  font-size: 11.5px;
  color: var(--faint);
}
.ann__countnote { color: var(--muted); }
/* The divider between the count and its verdict. A drawn hairline rather than a
   pipe character, because the line is aria-live and a screen reader would read
   the glyph out on every keystroke. */
.ann__countrule {
  width: 1px;
  height: 11px;
  background: rgba(27, 67, 50, .2);
  align-self: center;
}

/* ── THE TIMING PANEL ──
   States when the push goes out. No border and no card of its own: it is a line
   of the form, not a box beside it, and the .optout block it may contain
   already carries the only frame this needs. */
.ann__timing { margin-top: 18px; }
.ann__timinghead { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.ann__timingtitle { font-size: 13px; font-weight: 600; color: var(--ink); }
.ann__timingnote {
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--faint);
  margin: 5px 0 0;
}
/* HOLDING. The gold pill the eyebrows already use, at chip weight. Gold rather
   than anything warmer because nothing has gone wrong: the push is waiting for
   a reasonable hour, which is the system working. There is no red in this
   design and this is not the place to introduce one. */
.ann__hold {
  display: inline-block;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .1em;
  color: var(--gold-dark);
  background: rgba(201,162,39,.18);
  border: 1px solid rgba(168,132,42,.4);
  border-radius: 3px;
  padding: 4px 9px;
}

.ann__livehead {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--gold-dark);
  letter-spacing: .14em;
  margin-bottom: 4px;
}
.ann__livehead--spaced { margin-top: 18px; }
/* baseline, not flex-start: it puts the row title, the Who's read it link and the
   Remove control on one text baseline. Under flex-start they merely started at the
   same y, which is not the same thing once their boxes differ in height. */
.ann__row {
  padding: 12px 0;
  border-top: 1px solid rgba(27, 67, 50, .1);
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.ann__rowmain { flex: 1; }
.ann__rowtitle { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.ann__rowmeta { font-size: 11.5px; color: var(--faint); margin-top: 2px; }
/* THE STATS LINE. Each phrase is its own box and does not wrap inside itself, so
   a narrow screen breaks BETWEEN "Pushed to 5" and "Portal only 2" and never
   mid-sentence. The row gap is what separates them; there is no glyph between,
   because a divider that has to wrap is a divider that ends up leading a line. */
.ann__rowstats {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 12px;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--faint);
}
.ann__stat { white-space: nowrap; }
.ann__stat--read { color: var(--green-deep); font-weight: 600; }
/* The one-line note under a group heading. Quieter than .ann__empty, which is a
   state; this is a caption. */
.ann__empty--quiet { padding: 8px 0 0; border-top: 0; }
.ann__rowmeta b { color: var(--green-deep); font-weight: 600; }
/* One is an anchor and one is a form button, which is why they used to sit at
   slightly different heights: a button does not inherit line-height the way an
   anchor does, so the two line boxes were different sizes and their underlines
   landed at different points. Declaring the same font, size and line-height for
   both makes the boxes identical, and the row's baseline alignment then puts them
   level with each other. Every shared property is set once, here, so the two
   cannot drift apart again. */
.ann__link,
.ann__remove {
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--muted);
  background: none;
  border: 0;
  border-bottom: 1px solid rgba(27, 67, 50, .2);
  padding: 0;
  white-space: nowrap;
}
.ann__remove { cursor: pointer; }
/* The form is only a POST wrapper around the button. Made a flex box so the form's
   baseline IS the button's baseline, rather than depending on line box rules. */
.ann__removeform { margin: 0; display: flex; }
.ann__empty {
  font-size: 12.5px;
  color: var(--faint);
  padding: 14px 0;
  border-top: 1px solid rgba(27, 67, 50, .1);
  margin: 0;
}
.ann__foot {
  font-size: 11px;
  line-height: 1.6;
  color: var(--faint);
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(27, 67, 50, .1);
}

/* ══ THE CONFIRMATION SCREEN ═══════════════════════════════════════════════
   The one genuinely new surface in this feature, and the only place anything
   new is styled. Everything else reuses .ann__, .news__ and .optout as built.

   THE PHONE MOCK IS WHY. The portal has never drawn a lock screen, and the
   protection this screen offers IS the picture: an admin has to see the
   truncated title before seven people do. A dialog taking strings could not do
   it, which is what ruled 2b out.

   NO RED ANYWHERE, and nothing below can produce any. */
.acon__title {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 400;
  line-height: 1.25;
  color: var(--ink);
  margin: 0;
}
.acon__lede { margin: 9px 0 0; font-size: 13.5px; line-height: 1.65; color: var(--muted); }

/* The override, restated here because the composer's checkbox is two clicks
   behind by now. Same gold pill as the HOLDING state it is skipping. */
.acon__override { margin-top: 18px; }
.acon__overridenow { margin-left: 9px; font-size: 12.5px; color: var(--body); }
.acon__overridenote { margin: 9px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }

.acon__previews {
  margin-top: 20px;
  padding: 20px;
  background: var(--cream);
  border: 1px solid rgba(27, 67, 50, .14);
  border-radius: 6px;
}
.acon__eyebrow {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  color: var(--gold-dark);
}
.acon__eyebrow--spaced { margin-top: 18px; }

/* THE PHONE. A dark slab standing in for the wallpaper, with the banner on it.
   Deliberately plain: no notch, no status bar, no device chrome. Every pixel
   that is not the notification is a pixel arguing about which handset this is. */
.acon__phone {
  margin-top: 12px;
  background: var(--ink);
  border-radius: 12px;
  padding: 16px 14px;
}
.acon__banner {
  background: rgba(250, 247, 240, .94);
  border-radius: 14px;
  padding: 12px 14px;
  display: flex;
  gap: 11px;
  align-items: flex-start;
}
.acon__shield {
  flex-shrink: 0;
  display: block;
  height: auto;
  border: 0;
  border-radius: 7px;
  background: var(--green-deep);
  padding: 5px;
  box-sizing: border-box;
}
.acon__bannermain { flex: 1; min-width: 0; }
.acon__bannerhead { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.acon__bannerapp { font-size: 12.5px; font-weight: 700; color: var(--ink); }
.acon__bannerwhen { flex-shrink: 0; font-size: 11px; color: var(--muted); }
/* THE TITLE AND BODY, AT THE WEIGHTS A LOCK SCREEN USES. Both wrap rather than
   ellipsing in CSS: the truncation is done on the server so the cut is a fact
   the sender can read, not a rendering that changes with the viewport. */
.acon__bannertitle { margin-top: 3px; font-size: 13.5px; font-weight: 600; line-height: 1.35; color: var(--ink); }
.acon__bannerbody { margin-top: 2px; font-size: 13.5px; line-height: 1.45; color: var(--body); }

/* The card preview is partials/announcement-card, unstyled here. Only the space
   above it belongs to this screen. */
.acon__previews .news { margin-top: 12px; }

/* WHO GETS WHAT. Two rows and a footnote, with the number given the serif so it
   reads as a quantity rather than as the start of the sentence. */
.acon__who {
  margin-top: 18px;
  background: var(--cream);
  border: 1px solid rgba(27, 67, 50, .14);
  border-radius: 6px;
  overflow: hidden;
}
.acon__wholabel {
  padding: 11px 17px;
  background: rgba(27, 67, 50, .04);
  border-bottom: 1px solid rgba(27, 67, 50, .12);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  color: var(--muted);
}
.acon__whorow {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 13px 17px;
  border-bottom: 1px solid rgba(27, 67, 50, .08);
}
.acon__whon {
  flex-shrink: 0;
  min-width: 34px;
  font-family: var(--serif);
  font-size: 20px;
  color: var(--green-deep);
}
/* THE PHRASE WRAPS AS A PHRASE. flex: 1 with normal wrapping keeps "get a push
   at 8:00 AM, and the dashboard card now" breaking between words at any width;
   the number beside it never wraps away from its own line. */
.acon__whotext { flex: 1; font-size: 13px; line-height: 1.55; color: var(--body); }
.acon__whotext b { color: var(--ink); font-weight: 600; }
.acon__whofoot { margin: 0; padding: 12px 17px; font-size: 12px; line-height: 1.6; color: var(--muted); }

.acon__send { margin: 0; }
.acon__actions { display: flex; align-items: center; gap: 16px; margin-top: 18px; flex-wrap: wrap; }
.acon__back {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--green-btn);
  text-decoration: none;
  border-bottom: 1px solid rgba(27, 67, 50, .3);
}
/* A PUSH CANNOT BE RECALLED. The one line worth keeping from the dialog this
   screen replaced. Quiet, because it is a fact about the medium rather than a
   warning about this particular send. */
.acon__warn { margin: 16px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--faint); }

@media (max-width: 620px) {
  /* FOUR NESTED PADDINGS REACH THE BANNER, and at 360px they left it about
     140px wide. The cut itself is decided on the server at a fixed character
     budget and rendered with its own ellipsis, so a narrow mock cannot make the
     preview lie about WHERE the phone stops -- but a banner squeezed to a third
     of a real lock screen stops looking like one, and this screen's whole job is
     that it looks like one.
     Measured at 360: 328 page, 280 card, 252 previews, 236 phone, 214 banner,
     171 for the text beside the shield. */
  .acon__previews { padding: 14px; }
  .acon__phone { padding: 10px 8px; }
  .acon__banner { padding: 10px 11px; }
  .acon__who .acon__whorow,
  .acon__who .acon__wholabel,
  .acon__who .acon__whofoot { padding-left: 13px; padding-right: 13px; }
  /* The action row stacks rather than wrapping a 48px button beside a link and
     leaving one of them orphaned mid-line. */
  .acon__actions { gap: 12px 16px; }
}

/* The who-has-read-it page: a plain two column list. */
.ann__reads { margin-top: 12px; }
.ann__readrow {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 9px 0;
  border-top: 1px solid rgba(27, 67, 50, .1);
}
.ann__readname { flex: 1; font-size: 13px; color: var(--body); }
.ann__readwhen { font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.ann__readwhen--none { color: var(--faint); font-style: italic; }

/* The composer's submit sits inline beside its hint rather than filling the
   card, unlike the full width .submit the add forms use. */
.submit--inline { margin-top: 0; width: auto; padding: 12px 22px; }
/* TITLE on the left and ACTIVE on the right must begin on the same line, and they
   did not. The cause was specificity, not spacing: .lbl:first-of-type sets an 18px
   top margin for the add cards and outranks a bare .lbl--first, 0,2,0 against
   0,1,0, so the left column started 18px lower than the right.
   Scoping to the composer makes this 0,3,0, which wins on specificity rather than
   on where the rule happens to sit in the file. The offset is removed at its
   source; no compensating margin is added to the other column, so the two stay
   level if either side's spacing changes later. */
.ann__compose .lbl--first,
.ann__compose .lbl:first-of-type { margin-top: 0; }

@media (max-width: 620px) {
  /* Below this the two announcement columns have already stacked, so a row has
     the full narrow width and the title plus both controls no longer fit on one
     line. The text takes its own line and the pair drops beneath it, still
     side by side, still on one baseline with each other, and still separated by
     the row's own 12px gap rather than a second spacing value. */
  .ann__row { flex-wrap: wrap; }
  .ann__rowmain { flex-basis: 100%; }
}


/* ══════════════════════════════════════════════════════════════════════════
   TRAINING PROGRAM
   Restyled to the portal mocks (screens 10 to 15). Every value below comes
   from handoff/design-tokens.md via the --rt-* tokens merged into :root, and
   every length comes from the recommended 4/8/12/16/20/24/28/32/40/64 scale
   rather than the raw inventory.

   Visual only. No selector here changes what a control does; the forms, field
   names and routes are exactly what they were.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── shared ─────────────────────────────────────────────────────────────── */
.page--training, .page--lesson, .page--mock, .page--roster { color: var(--rt-body); }
.t-eyebrow {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--rt-gold-dark);
}
/* .t-h1's own type is gone: it takes the shared page-title rule near the top of
   this file. It was 32px at line-height 1.2 with 8px above and 12px below; the
   8px above survives as the eyebrow adjacency rule up there, because on this
   page the title sits under TUTOR CERTIFICATION. */
/* 62ch broke too early against this page's 1028px container: the intro wrapped
   at roughly half the available width and stranded single words on their own
   line. 88ch keeps a readable measure while using the room that is there. */
.t-lead {
  font-family: var(--rt-font-ui); font-size: 14.5px; line-height: 1.7;
  color: var(--rt-body); max-width: 88ch; margin: 0 0 var(--rt-s6);
}
/* A lead that runs the full content width, for a page whose cards do too. The
   62ch measure is right where the paragraph is the widest thing on the screen
   and wrong where it sits above full-width cards, which reads as a stray narrow
   column rather than as a considered measure. */
.t-lead--full { max-width: none; }
.t-back {
  font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary);
  text-decoration: none; border-bottom: 1px solid var(--rt-underline-gold);
  padding-bottom: 1px;
}
.t-back:hover { color: var(--rt-ink); }
.t-btn {
  display: inline-block; font-family: var(--rt-font-ui); font-size: 13.5px;
  font-weight: 600; color: var(--rt-offwhite); background: var(--rt-green-deep);
  border: 0; border-radius: var(--rt-radius-button); padding: var(--rt-s3) var(--rt-s5);
  cursor: pointer; text-decoration: none; line-height: 1.3;
}
.t-btn:hover { background: var(--rt-green-button); }
.t-btn--gold { background: var(--rt-gold); color: var(--rt-green-deep); }
.t-btn--gold:hover { background: var(--rt-gold-bright); }
.t-note {
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.6;
  color: var(--rt-muted); margin: 0;
}
.t-foot {
  margin-top: var(--rt-s7); padding-top: var(--rt-s5);
  border-top: 1px solid var(--rt-rule);
}
.t-pill {
  display: inline-block; font-family: var(--rt-font-ui); font-size: 9.5px;
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--rt-secondary); background: var(--rt-tint-neutral);
  border: 1px solid var(--rt-border-card); border-radius: var(--rt-radius-pill);
  padding: 5px var(--rt-s3);
}

/* ── the status banner, deep green ──────────────────────────────────────── */
.tstatus {
  position: relative; overflow: hidden; background: var(--rt-green-deep);
  border-radius: var(--rt-radius-card); padding: var(--rt-s6) var(--rt-s7);
  margin-bottom: var(--rt-s7);
}
/* The decorative ring in the mock: a large, very faint gold circle bleeding off
   the right edge. Pointer-events off so it can never eat a click. */
.tstatus::after {
  content: ''; position: absolute; right: -140px; top: -60px;
  width: 340px; height: 340px; border-radius: 50%;
  border: 1px solid rgba(227,181,58,.11); pointer-events: none;
}
.tstatus__label {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--rt-gold-bright);
}
.tstatus__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 26px;
  color: var(--rt-offwhite); margin: var(--rt-s2) 0 var(--rt-s2); line-height: 1.25;
}
/* Same problem inside the banner, and worse: 52ch left an orphaned word on the
   last line. The decorative ring bleeds in from the right, so the measure stops
   short of it rather than running the full card width. */
.tstatus__body {
  font-family: var(--rt-font-ui); font-size: 13.5px; line-height: 1.6;
  color: rgba(255,255,255,.72); margin: 0; max-width: 78ch; padding-right: 150px;
}
@media (max-width: 900px) { .tstatus__body { padding-right: 0; } }

/* the three-step stepper */
.tsteps { display: flex; align-items: flex-start; margin-top: var(--rt-s6); }
.tstep { display: flex; flex-direction: column; align-items: center; width: 96px; flex: 0 0 auto; }
.tstep__dot {
  width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.35); display: grid; place-items: center;
  font-size: 10px; color: var(--rt-green-deep);
}
.tstep--done .tstep__dot { border-color: var(--rt-gold-bright); color: var(--rt-gold-bright); }
.tstep--on .tstep__dot { background: var(--rt-gold); border-color: var(--rt-gold); }
.tstep__label {
  font-family: var(--rt-font-ui); font-size: 10.5px; line-height: 1.35;
  text-align: center; margin-top: var(--rt-s2); color: rgba(255,255,255,.5);
}
.tstep--on .tstep__label { color: #fff; font-weight: 700; }
.tstep--done .tstep__label { color: rgba(255,255,255,.6); }
.tsteps__bar {
  flex: 1; height: 1px; background: rgba(255,255,255,.22); margin-top: 9px; min-width: var(--rt-s5);
}
.tsteps__bar--done { background: var(--rt-gold); }

/* ── overall progress ───────────────────────────────────────────────────── */
.tprogress__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--rt-s4); margin-bottom: var(--rt-s3); flex-wrap: wrap;
}
.tprogress__meta { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary); }
.tbar {
  height: 8px; border-radius: var(--rt-radius-pill);
  background: rgba(27,67,50,.1); overflow: hidden;
}
.tbar__fill { display: block; height: 100%; background: var(--rt-green-deep); border-radius: var(--rt-radius-pill); }
.tbar__fill--part { background: var(--rt-gold); }

/* ── the module rows ────────────────────────────────────────────────────── */
.tmods { list-style: none; padding: 0; margin: var(--rt-s6) 0 0; display: flex; flex-direction: column; gap: var(--rt-s3); }
.tmod {
  display: flex; align-items: center; gap: var(--rt-s4);
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6);
}
.tmod--on { border-color: var(--rt-gold); box-shadow: var(--rt-shadow-active-gold); }
.tmod--locked { background: rgba(27,67,50,.04); border-color: var(--rt-rule); }
.tmod__mark {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--rt-font-ui); font-size: 13px; font-weight: 700;
}
.tmod--done .tmod__mark { background: var(--rt-green-deep); color: var(--rt-offwhite); }
.tmod--on .tmod__mark { background: var(--rt-gold); color: var(--rt-green-deep); }
.tmod--locked .tmod__mark { background: rgba(27,67,50,.06); color: var(--rt-disabled); font-size: 14px; }
.tmod__main { flex: 1; min-width: 0; }
.tmod__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 20px;
  color: var(--rt-ink); line-height: 1.3;
}
.tmod--locked .tmod__title { color: var(--rt-disabled); }
.tmod__meta {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-muted);
  margin-top: var(--rt-s1);
}
.tmod__side { display: flex; align-items: center; gap: var(--rt-s5); flex: 0 0 auto; }
.tmod__state {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--rt-secondary);
}
.tmod--on .tmod__state { color: var(--rt-gold-dark); }
.tmod__unlocks { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-muted); text-align: right; }
.tmod__link {
  font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary);
  text-decoration: none; border-bottom: 1px solid var(--rt-underline-gold); padding-bottom: 1px;
}
.tmod__link:hover { color: var(--rt-ink); }

/* the lesson list under an open module (no equivalent in the mock, which links
   straight into a module; kept because this build has no module landing page) */
.tlessons { list-style: none; padding: 0; margin: var(--rt-s3) 0 0; display: flex; flex-wrap: wrap; gap: var(--rt-s1) var(--rt-s4); }
.tlesson { font-family: var(--rt-font-ui); font-size: 12.5px; }
.tlesson a { color: var(--rt-secondary); text-decoration: none; border-bottom: 1px solid var(--rt-rule); }
.tlesson a:hover { border-bottom-color: var(--rt-underline-gold); }
.tlesson--done a { color: var(--rt-muted); }

/* ── awaiting certification (screen 13) ─────────────────────────────────── */
.tcards { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--rt-s4); margin: var(--rt-s6) 0; }
.tcard {
  background: var(--rt-offwhite); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5);
}
.tcard__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 20px;
  color: var(--rt-ink); line-height: 1.25; margin-bottom: var(--rt-s3);
}
.tcard__body { font-family: var(--rt-font-ui); font-size: 13px; line-height: 1.6; color: var(--rt-body); }
@media (max-width: 780px) { .tcards { grid-template-columns: 1fr; } }

/* ── lesson view (screen 11) ────────────────────────────────────────────── */
.lessonwrap { display: flex; align-items: flex-start; gap: 0; }
.lessonrail {
  flex: 0 0 300px; padding: 0 var(--rt-s7) var(--rt-s8) 0;
  border-right: 1px solid var(--rt-rule); align-self: stretch;
}
.lessonrail__mod {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 22px;
  color: var(--rt-ink); line-height: 1.25; margin: var(--rt-s1) 0 var(--rt-s5);
}
.raillist { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--rt-s1); }
.railitem {
  display: flex; align-items: center; gap: var(--rt-s3);
  padding: var(--rt-s2) var(--rt-s3); border-radius: var(--rt-radius-input);
  border: 1px solid transparent; text-decoration: none;
}
.railitem__dot {
  flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center; font-family: var(--rt-font-ui);
  font-size: 11px; font-weight: 600; color: var(--rt-muted);
  border: 1px solid var(--rt-border-control);
}
.railitem__text { font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary); }
.railitem--done .railitem__dot { background: var(--rt-green-deep); border-color: var(--rt-green-deep); color: var(--rt-offwhite); }
.railitem--on { background: rgba(201,162,39,.12); border-color: var(--rt-border-gold); }
.railitem--on .railitem__dot { background: var(--rt-gold); border-color: var(--rt-gold); color: var(--rt-green-deep); }
.railitem--on .railitem__text { color: var(--rt-ink); font-weight: 600; }
.railitem:hover .railitem__text { color: var(--rt-ink); }
.railrule { border: 0; border-top: 1px solid var(--rt-rule); margin: var(--rt-s5) 0; }
.railitem--assess .railitem__dot { border-color: var(--rt-border-gold); color: var(--rt-gold-dark); }
.railitem--assess .railitem__text { color: var(--rt-ink); font-weight: 600; }

.lessonbody { flex: 1; min-width: 0; padding: 0 0 var(--rt-s8) var(--rt-s7); }
.lessonbody__head { display: flex; align-items: center; gap: var(--rt-s3); margin-bottom: var(--rt-s2); }
.lessonbody__count {
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--rt-muted);
}
.lesson {
  font-family: var(--rt-font-ui); font-size: 15px; line-height: 1.75;
  color: var(--rt-body); max-width: 62ch;
}
.lesson h3, .lesson h4, .lesson h5 {
  font-family: var(--rt-font-display); font-weight: 400; color: var(--rt-ink);
  line-height: 1.3; margin: var(--rt-s6) 0 var(--rt-s3);
}
.lesson h3 { font-size: 22px; }
.lesson h4 { font-size: 19px; }
.lesson h5 { font-size: 17px; }
.lesson p { margin: 0 0 var(--rt-s4); }
.lesson ol, .lesson ul { margin: 0 0 var(--rt-s4); padding-left: var(--rt-s6); }
.lesson li { margin-bottom: var(--rt-s2); }
.lesson strong { color: var(--rt-ink); font-weight: 600; }

/* the video block: a deep green panel with a gold play badge */
.lvideo {
  display: block; text-decoration: none; background: var(--rt-green-deep);
  border-radius: var(--rt-radius-card); padding: var(--rt-s10) var(--rt-s5);
  margin: var(--rt-s6) 0; text-align: center;
}
.lvideo__play {
  width: 64px; height: 64px; border-radius: 50%; background: var(--rt-gold);
  display: grid; place-items: center; margin: 0 auto var(--rt-s5);
  color: var(--rt-green-deep); font-size: 22px; line-height: 1;
}
.lvideo__title { font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-offwhite); }
.lvideo__meta { font-family: var(--rt-font-ui); font-size: 12px; color: rgba(255,255,255,.6); margin-top: var(--rt-s1); }

/* the attachment row */
.lfile {
  display: flex; align-items: center; gap: var(--rt-s4);
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s4) var(--rt-s5);
  margin-bottom: var(--rt-s6);
}
.lfile__badge {
  flex: 0 0 auto; width: 42px; height: 42px; border-radius: var(--rt-radius-input);
  background: var(--rt-green-deep); color: var(--rt-offwhite);
  display: grid; place-items: center;
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700; letter-spacing: .06em;
}
.lfile__main { flex: 1; min-width: 0; }
.lfile__name { font-family: var(--rt-font-ui); font-size: 13.5px; font-weight: 600; color: var(--rt-ink); word-break: break-all; }
.lfile__meta { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-muted); margin-top: 2px; }
.lfile__link {
  font-family: var(--rt-font-ui); font-size: 13px; font-weight: 600; color: var(--rt-ink);
  text-decoration: none; border-bottom: 1px solid var(--rt-underline-gold); padding-bottom: 1px;
}

.lessonacts { display: flex; align-items: center; gap: var(--rt-s5); flex-wrap: wrap; }
.lessonacts .t-btn { max-width: 160px; }
.lessondone { font-family: var(--rt-font-ui); font-size: 13px; font-weight: 600; color: var(--rt-green-button); }
.lessonnav { display: flex; gap: var(--rt-s5); flex-wrap: wrap; margin-top: var(--rt-s5); }

@media (max-width: 860px) {
  .lessonwrap { flex-direction: column; }
  .lessonrail { flex-basis: auto; width: 100%; border-right: 0; border-bottom: 1px solid var(--rt-rule); padding: 0 0 var(--rt-s5); }
  .lessonbody { padding-left: 0; padding-top: var(--rt-s6); }
}

/* ── the module assessment ──────────────────────────────────────────────── */
.assess { background: var(--rt-cream); }
.assess__head { display: flex; align-items: center; gap: var(--rt-s3); margin-bottom: var(--rt-s2); }
.tq {
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6);
  margin: 0 0 var(--rt-s4);
}
.tq__count {
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--rt-muted);
}
.tq__prompt {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 20px;
  color: var(--rt-ink); line-height: 1.35; margin: var(--rt-s2) 0 var(--rt-s4);
}
.tq__choice {
  display: flex; align-items: center; gap: var(--rt-s4);
  border: 1px solid var(--rt-border-card); border-radius: var(--rt-radius-card);
  padding: var(--rt-s4) var(--rt-s5); margin-bottom: var(--rt-s3); cursor: pointer;
}
.tq__choice:hover { border-color: var(--rt-border-control); }
.tq__choice input { appearance: none; margin: 0; width: 20px; height: 20px; flex: 0 0 auto;
  border: 1px solid rgba(27,67,50,.3); border-radius: 50%; cursor: pointer; }
.tq__choice input:checked { border: 6px solid var(--rt-green-deep); }
.tq__choice input:focus-visible { outline: 2px solid var(--rt-gold); outline-offset: 2px; }
.tq__label { flex: 1; font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-body); }
.tq__choice:has(input:checked) { border: 2px solid var(--rt-green-deep); background: var(--rt-tint-selected); padding: 15px 19px; }
.tq__choice:has(input:checked) .tq__label { font-weight: 600; color: var(--rt-ink); }
.tq__selected {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--rt-muted); display: none;
}
.tq__choice:has(input:checked) .tq__selected { display: inline; }

.tsubmit { display: flex; align-items: center; gap: var(--rt-s5); margin-top: var(--rt-s6); flex-wrap: wrap; }
.tsubmit .t-btn { max-width: 240px; }

/* the result of the attempt just submitted */
.tresult {
  display: flex; gap: var(--rt-s5); align-items: center;
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6);
  margin-bottom: var(--rt-s6); border: 1px solid var(--rt-border-card);
  background: var(--rt-offwhite);
}
.tresult--pass { border-color: var(--rt-border-gold); background: rgba(201,162,39,.07); }
.tresult--fail { border-color: rgba(27,67,50,.18); }
.tresult__score { font-family: var(--rt-font-display); font-weight: 400; font-size: 34px; color: var(--rt-ink); }
.tresult__body { font-family: var(--rt-font-ui); font-size: 13.5px; line-height: 1.6; color: var(--rt-body); }

/* the module-closed banner: what happened on the left, where to go on the right */
.tclosed {
  display: flex; align-items: center; gap: var(--rt-s5); flex-wrap: wrap;
  padding: var(--rt-s4) var(--rt-s5); margin-bottom: var(--rt-s6);
}
.tclosed__body { flex: 1 1 300px; margin: 0; }
.tclosed__go { flex: 0 0 auto; margin-left: auto; }

.tattempts { margin-bottom: var(--rt-s6); }
.tattempt {
  display: flex; gap: var(--rt-s4); align-items: baseline;
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary);
  padding: var(--rt-s2) 0; border-bottom: 1px solid var(--rt-rule);
}
.tattempt__n { min-width: 88px; }
.tattempt__score { font-weight: 600; color: var(--rt-ink); min-width: 56px; }
.tattempt__when { margin-left: auto; color: var(--rt-muted); }
.t-ok { color: var(--rt-green-button); font-weight: 600; }
.t-no { color: var(--rt-gold-dark); font-weight: 600; }

/* ── admin: the training roster (side drawer) ───────────────────────────────
   Built to handoff/design_handoff_training_roster, option 1b. It replaces the
   table with its per-module disclosure: one line per tutor, and the record in a
   drawer beside the list rather than inside it, so opening a tutor cannot move
   the rows underneath. Every colour here is a --rt-* token; the handoff's
   palette IS this file's palette, already reconciled at the top. */
.rosthead {
  display: flex; align-items: center; gap: var(--rt-s5); flex-wrap: wrap;
  margin-bottom: var(--rt-s3);
}
.rosthead .t-h1 { margin-bottom: 0; }
.rsearch {
  margin-left: auto; display: flex; align-items: center; gap: var(--rt-s2);
  background: var(--rt-white); border: 1px solid var(--rt-border-control);
  border-radius: var(--rt-radius-card); padding: 7px var(--rt-s3); width: 240px; max-width: 100%;
}
.rsearch__icon { font-size: 13px; color: var(--rt-muted); line-height: 1; }
.rsearch__input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-ink);
}
.rsearch__input::placeholder { color: var(--rt-muted); }
/* The submit exists for the scripting-off case; with the script running the box
   filters as you type and the button has nothing left to do. */
.rsearch__go {
  border: 0; background: none; cursor: pointer; padding: 0;
  font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-secondary);
  border-bottom: 1px solid var(--rt-underline-gold);
}

.rchips { display: flex; gap: var(--rt-s2); flex-wrap: wrap; margin: var(--rt-s4) 0 var(--rt-s5); }
.rchip {
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  padding: 5px 13px; border-radius: var(--rt-radius-pill); white-space: nowrap;
  text-decoration: none; border: 1px solid transparent;
}
/* The chosen chip keeps its own colour and gains a ring, because the colour is
   what names the stage: recolouring it to show selection would say two things
   with one channel. */
.rchip--on { outline: 2px solid var(--rt-gold); outline-offset: 1px; }
.rchip--all { background: var(--rt-green-deep); border-color: var(--rt-green-deep); color: var(--rt-offwhite); }
.rchip--training, .rpill--training {
  background: rgba(27,67,50,.06); border-color: rgba(27,67,50,.18); color: var(--rt-secondary);
}
.rchip--below_bar, .rpill--below_bar {
  background: rgba(161,66,50,.08); border-color: rgba(161,66,50,.35); color: #A14232;
}
.rchip--awaiting_mocks, .rpill--awaiting_mocks {
  background: rgba(201,162,39,.14); border-color: rgba(201,162,39,.45); color: var(--rt-gold-dark);
}
.rchip--ready, .rpill--ready {
  background: var(--rt-gold); border-color: var(--rt-gold); color: var(--rt-green-deep);
}
.rchip--certified, .rpill--certified {
  background: var(--rt-green-button); border-color: var(--rt-green-button); color: var(--rt-offwhite);
}

.rwrap {
  display: flex; align-items: stretch; background: var(--rt-cream);
  border: 1px solid var(--rt-border-card); border-radius: var(--rt-radius-panel);
  overflow: hidden; margin-bottom: var(--rt-s5);
}
.rwrap > .rlist { flex: 1; min-width: 0; padding: var(--rt-s5) var(--rt-s5) var(--rt-s6); }
/* SCOPED TO .rwrap, and that is the whole point of the selector. Bare `.rlist`
   also matched the admin tools rosters, which have used the same class for the
   tutors and students lists since long before this panel existed. They inherited
   20px of left and right padding and 20px on top from a rule written for a
   different screen: the gold STUDENTS label and "Show archived (n)" sat on the
   card's content edges while the filter and every row sat 20px inside them, both
   sides, and the gap under the header band came out at 34px instead of the 16px
   section rhythm. Neither number was ever declared anywhere near the roster.
   `flex: 1` was inert over there and is the reason this looked harmless. */
.rlist__empty, .rlist__foot {
  font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); margin-top: var(--rt-s4);
}

.rrow {
  display: flex; align-items: center; gap: var(--rt-s4); text-decoration: none;
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s3) var(--rt-s4);
  margin-bottom: 7px;
}
.rrow:hover { border-color: var(--rt-gold); }
.rrow--on { background: rgba(201,162,39,.08); border-color: var(--rt-gold); }
.rrow:focus-visible { outline: 2px solid var(--rt-gold); outline-offset: 2px; }
.ravatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  background: var(--rt-green-deep); color: var(--rt-gold-bright);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 700;
}
.ravatar--lg { width: 40px; height: 40px; font-size: 13px; }
.rrow__name {
  width: 150px; flex-shrink: 0; font-family: var(--rt-font-ui); font-size: 13px;
  font-weight: 600; color: var(--rt-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* One segment per module, which is the whole progress display on a row: a bar
   would say "62 percent" where what an admin needs is "four of six closed". */
.rsegs { display: flex; gap: 3px; width: 120px; flex-shrink: 0; }
.rseg { flex: 1; height: 5px; border-radius: var(--rt-radius-pill); background: rgba(27,67,50,.14); }
.rseg--on { background: var(--rt-green-button); }
.rsegs--lg { width: auto; gap: var(--rt-s1); margin-top: var(--rt-s2); }
.rsegs--lg .rseg { height: 6px; }
.rrow__line {
  flex: 1; min-width: 0; font-family: var(--rt-font-ui); font-size: 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--rt-secondary);
}
.rrow__line--ok { color: var(--rt-green-button); }
.rrow__line--no { color: #A14232; }
.rrow__line--none { color: var(--rt-muted); }
.rpill {
  display: inline-block; flex-shrink: 0; font-family: var(--rt-font-ui);
  font-size: 9.5px; font-weight: 700; letter-spacing: .08em;
  padding: 4px 11px; border-radius: var(--rt-radius-pill);
  border: 1px solid transparent; white-space: nowrap;
}

/* ── the drawer ─────────────────────────────────────────────────────────── */
.rdrawer {
  width: 380px; flex-shrink: 0; background: var(--rt-offwhite);
  border-left: 1px solid var(--rt-border-card); padding: var(--rt-s6) var(--rt-s6) var(--rt-s7);
}
.rdraw__head { display: flex; align-items: center; gap: var(--rt-s3); }
.rdraw__who { min-width: 0; }
.rdraw__name {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 19px;
  color: var(--rt-ink); margin: 0; line-height: 1.25;
}
.rdraw__sub {
  font-family: var(--rt-font-ui); font-size: 11px; color: var(--rt-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px;
}
.rdraw__stage { margin-top: var(--rt-s3); display: flex; align-items: center; gap: var(--rt-s2); flex-wrap: wrap; }
.rdraw__stagesub { font-family: var(--rt-font-ui); font-size: 11px; color: var(--rt-muted); }
.rdraw__rule { height: 1px; background: var(--rt-border-card); margin: var(--rt-s4) 0; }
.rkick {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .14em; color: var(--rt-gold-dark); margin-top: 18px;
}
.rdraw__prog { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-secondary); margin: 7px 0 0; }
.rdraw__score--ok { color: var(--rt-green-button); font-weight: 600; }
.rdraw__score--no { color: #A14232; font-weight: 600; }
.rdraw__score--none { color: var(--rt-muted); }

.rmods { display: flex; flex-direction: column; margin-top: 6px; }
.rmod {
  display: flex; gap: var(--rt-s2); align-items: baseline;
  padding: var(--rt-s2) 2px; border-bottom: 1px solid rgba(27,67,50,.07);
}
.rmod > span { line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The only column that may not push the others: it truncates instead. */
.rmod__name { flex: 1; min-width: 0; font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-ink); }
.rmod__lessons { width: 40px; font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-secondary); }
.rmod__score { width: 52px; font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 600; color: var(--rt-ink); }
.rmod__state {
  width: 82px; text-align: right; font-family: var(--rt-font-ui);
  font-size: 9.5px; font-weight: 600; letter-spacing: .05em;
}
.rmod__state--closed { color: var(--rt-green-button); }
.rmod__state--open { color: var(--rt-gold-dark); }
.rmod__state--locked { color: var(--rt-muted); }

.rmocks { display: flex; flex-direction: column; gap: var(--rt-s2); margin-top: var(--rt-s2); }
.rmock {
  display: flex; align-items: center; gap: var(--rt-s3);
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: 10px var(--rt-s4);
}
.rmock__name { flex: 1; font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 600; color: var(--rt-ink); }
.rmock__pct {
  font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 700;
  color: var(--rt-gold-dark); text-decoration: none;
  border-bottom: 1px solid var(--rt-underline-gold);
}
.rmock__go {
  font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 600;
  color: var(--rt-green-button); text-decoration: none;
  border-bottom: 1px solid var(--rt-underline-gold);
}

.rcert__done, .rcert__blocked {
  font-family: var(--rt-font-ui); font-size: 12px; line-height: 1.6; margin: var(--rt-s2) 0 0;
}
.rcert__done { color: var(--rt-secondary); }
.rcert__blocked { color: var(--rt-muted); }
.rcert__btn {
  display: block; width: 100%; margin-top: 10px; border: 0; cursor: pointer;
  background: var(--rt-green-button); color: var(--rt-offwhite);
  font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 600;
  padding: 11px 0; border-radius: var(--rt-radius-button);
}
.rcert__btn:hover { background: var(--rt-green-deep); }
.rcert__card {
  margin-top: 10px; background: var(--rt-white); border: 1.5px solid var(--rt-gold);
  border-radius: var(--rt-radius-card); padding: var(--rt-s3) var(--rt-s4);
}
.rcert__note { font-family: var(--rt-font-ui); font-size: 11.5px; line-height: 1.5; color: var(--rt-secondary); margin: 0; }
.rcert__label {
  display: block; margin-top: var(--rt-s2); font-family: var(--rt-font-ui);
  font-size: 9.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--rt-gold-dark);
}
.rcert__input {
  width: 100%; box-sizing: border-box; margin-top: 6px;
  border: 1px solid rgba(27,67,50,.2); border-radius: 4px; padding: var(--rt-s2) 10px;
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-ink); outline: none;
}
.rcert__input:focus-visible { outline: 2px solid var(--rt-gold); outline-offset: 1px; }
.rcert__acts { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.rcert__go {
  background: var(--rt-gold); color: var(--rt-green-deep); border: 0; cursor: pointer;
  font-family: var(--rt-font-ui); font-size: 12px; font-weight: 600;
  padding: var(--rt-s2) var(--rt-s4); border-radius: var(--rt-radius-button);
}
.rcert__go:disabled { opacity: .4; cursor: not-allowed; }
.rcert__cancel {
  background: none; border: 0; cursor: pointer; text-decoration: underline;
  font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted);
}

/* Under the drawer's own width plus a usable list, the two stack rather than
   squeezing the roster line until its readiness text is a single word. */
@media (max-width: 900px) {
  .rwrap { flex-direction: column; }
  .rdrawer { width: auto; border-left: 0; border-top: 1px solid var(--rt-border-card); }
  .rrow__name { width: 110px; }
  .rsegs { width: 78px; }
}

/* ── admin: mock session scoring (screen 15) ────────────────────────────── */
.mockhead { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--rt-s5); flex-wrap: wrap; }
.mockavg {
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s4) var(--rt-s6); text-align: center; min-width: 180px;
}
.mockavg__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--rt-muted);
}
.mockavg__value { font-family: var(--rt-font-display); font-weight: 400; font-size: 30px; color: var(--rt-ink); line-height: 1.1; margin-top: var(--rt-s1); }
/* "Not yet" is a sentence, not a number, so it takes the roster's empty-state
   treatment rather than 30px display type. */
.mockavg__value--empty {
  font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-muted);
  margin-top: var(--rt-s2);
}
/* The 1 to 5 average is what a scorer thinks in; the percentage is what the
   roster and the tutor record carry. Both are shown so the conversion is not a
   surprise that happens after the save. */
.mockavg__pct {
  font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 600;
  color: var(--rt-gold-dark); margin-top: 3px;
}
.mockmeta { font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary); margin: var(--rt-s1) 0 var(--rt-s6); }

.mockscale {
  display: flex; align-items: center; gap: var(--rt-s4); flex-wrap: wrap;
  background: var(--rt-offwhite); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s3) var(--rt-s5); margin-bottom: var(--rt-s4);
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary);
}
.mockscale__label { font-size: 9.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--rt-muted); }
.mockscale__sep { color: var(--rt-border-input); }

.mockswitch { display: flex; flex-wrap: wrap; gap: var(--rt-s2); margin-bottom: var(--rt-s5); }
.mocktab {
  font-family: var(--rt-font-ui); font-size: 12.5px; text-decoration: none;
  padding: var(--rt-s2) var(--rt-s4); border-radius: var(--rt-radius-pill);
  border: 1px solid var(--rt-border-card); color: var(--rt-secondary); background: var(--rt-white);
}
.mocktab--on { background: var(--rt-green-deep); border-color: var(--rt-green-deep); color: var(--rt-offwhite); }

/* ONE RHYTHM, not three margins. The card is a column with a single gap, so the
   space between the rubric text, the score row and the note is the same on every
   card whether the rubric line runs to one line or three. */
.mockline {
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6); margin-bottom: var(--rt-s3);
  display: flex; flex-direction: column; gap: var(--rt-s4);
}
/* A line the refused save is still waiting on. Marked on the card itself as well
   as counted in the alert, so finding them is scrolling rather than arithmetic.
   AFTER .mockline, not before it: the base rule sets the border shorthand, and
   at equal specificity the later declaration is the one that paints. */
.mockline--wanting { border-color: var(--rt-gold); background: rgba(201,162,39,.05); }
.mockline__top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--rt-s6); flex-wrap: wrap;
}
/* The text takes the room left by the score buttons rather than crowding them. */
.mockline__top > div:first-child { flex: 1 1 260px; min-width: 0; }
.mockline__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--rt-muted);
}
.mockline__text {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 19px;
  color: var(--rt-ink); line-height: 1.35; margin-top: var(--rt-s1);
}
.mockscore { display: flex; gap: var(--rt-s2); flex: 0 0 auto; }
.mockscore__opt { position: relative; }
.mockscore__opt input { position: absolute; inset: 0; opacity: 0; margin: 0; width: 100%; height: 100%; cursor: pointer; }
.mockscore__opt span {
  display: grid; place-items: center; width: 46px; height: 46px;
  border: 1px solid var(--rt-border-card); border-radius: var(--rt-radius-card);
  font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-secondary);
  background: var(--rt-white); cursor: pointer;
}
.mockscore__opt input:checked + span {
  background: var(--rt-green-deep); border-color: var(--rt-green-deep);
  color: var(--rt-gold-bright); font-weight: 700;
}
.mockscore__opt input:focus-visible + span { outline: 2px solid var(--rt-gold); outline-offset: 2px; }
.mocknote {
  /* No margin: the card's gap owns the spacing, so it cannot drift per card. */
  width: 100%; margin-top: 0; font-family: var(--rt-font-ui); font-size: 16px;
  color: var(--rt-body); background: var(--rt-cream);
  border: 1px solid var(--rt-border-card); border-radius: var(--rt-radius-input);
  padding: var(--rt-s3) var(--rt-s4);
}
.mocknote::placeholder { color: var(--rt-muted); }
.mocknote:focus { outline: none; border-color: var(--rt-gold); background: var(--rt-white); }
/* A hairline above the note separates it from the score row without adding a
   second spacing value. */
.mockline .mocknote { border-top-width: 1px; }
.mockoverall {
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6); margin: var(--rt-s4) 0;
}
.mockoverall textarea {
  width: 100%; margin-top: var(--rt-s3); font-family: var(--rt-font-ui); font-size: 16px;
  line-height: 1.6; color: var(--rt-body); background: var(--rt-white);
  border: 1px solid var(--rt-border-input); border-radius: var(--rt-radius-input); padding: var(--rt-s4);
}
.mockfoot { display: flex; align-items: center; gap: var(--rt-s5); flex-wrap: wrap; margin-top: var(--rt-s5); }
.mockfoot__count { flex: 1; font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-muted); }
.mockwhen { margin-bottom: var(--rt-s5); }
.mockwhen label { display: block; font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--rt-muted); margin-bottom: var(--rt-s2); }
.mockwhen input {
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-body);
  border: 1px solid var(--rt-border-input); border-radius: var(--rt-radius-input); padding: var(--rt-s2) var(--rt-s3);
}

/* ── admin: content admin ───────────────────────────────────────────────── */
.qform { border-top: 1px solid var(--rt-rule); padding: var(--rt-s4) 0; }
.qform--new { border-top: 1px dashed var(--rt-border-card); }
.qform__choices { display: grid; grid-template-columns: 1fr 1fr; gap: var(--rt-s2) var(--rt-s4); margin: var(--rt-s3) 0 var(--rt-s1); }
.qform__choice { display: flex; align-items: center; gap: var(--rt-s2); }
.qform__choice input[type=radio] { flex: 0 0 auto; }
.qform__choice input[type=text] { flex: 1; min-width: 0; font-size: 16px; }
.qform__acts { display: flex; align-items: center; gap: var(--rt-s4); margin-top: var(--rt-s3); flex-wrap: wrap; }
.lesson__body { width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 16px; line-height: 1.6; }
@media (max-width: 720px) { .qform__choices { grid-template-columns: 1fr; } }

/* Small shared utilities the training screens lean on. Named in the --rt-
   vocabulary so they read as part of the same system. */
.t-muted { color: var(--rt-muted); }
.t-acts { display: flex; align-items: center; gap: var(--rt-s5); margin-top: var(--rt-s5); flex-wrap: wrap; }
/* A navigation element, not a footnote. It was inheriting the roster's 12.5px
   link size and disappeared under the page title. */
.t-linkrow { margin: 0 0 var(--rt-s6); }
.t-linkrow a {
  font-family: var(--rt-font-ui); font-size: 15px; font-weight: 600;
  color: var(--rt-green-button); text-decoration: none;
  border-bottom: 2px solid var(--rt-underline-gold); padding-bottom: 2px;
}
.t-linkrow a:hover { color: var(--rt-ink); border-bottom-color: var(--rt-gold); }

/* ── ADMIN TOOLS: the four standalone screens, as a 2x2 card grid ──────────
   design_handoff_admin_tools, option 1b (approved). Option 1a, the 1x4 row,
   was rejected and is not built.

   TOKENS WHERE THEY MATCH, LITERALS WHERE THEY DO NOT. Most of the handoff's
   values already exist in the block at the top of this file and are used by
   name. Four do not, and they are written out rather than bent onto the
   nearest token, because the approved design is the specification and a
   near-miss colour is a silent redesign:
     icon tile fill    rgba(201,162,39,.14)  vs --rt-tint-gold   .18
     icon tile border  rgba(201,162,39,.35)  vs --rt-border-gold rgb 168,132,42
     pill border       rgba(201,162,39,.45)  same mismatch again
     hover shadow      0 8px 20px rgba(20,55,42,.1), no token of this weight
   If those ever become tokens, these four are the call sites to update. */
/* ⚠ THE TOP MARGIN IS NEW: the tile grid was pasted straight onto the rule under
   the Admin tools heading, headBottom and tilesTop both measuring 150 at 1440.
   The handoff names nav.atiles as a rail anchor and does NOT give a gap above
   it, so this is the design system's own step rather than a number quoted from
   the mock: --rt-s6, the same 24px the grid already carries BELOW itself, so the
   block sits in one rhythm top and bottom instead of being flush on one side. */
.atiles {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--rt-s4);
  margin: var(--rt-s6) 0;
}
/* The whole card is the link, so the hit area and the focus ring match the
   surface a reader sees. */
.atile {
  display: flex; gap: var(--rt-s4); text-decoration: none;
  background: var(--rt-white);
  border: 1px solid var(--rt-border-panel);
  border-radius: var(--rt-radius-panel);
  padding: 22px 24px;
  /* Colour and shadow only, so nothing shifts position under the cursor. */
  transition: border-color .15s ease, box-shadow .15s ease;
}
.atile:hover { border-color: var(--rt-gold); box-shadow: 0 8px 20px rgba(20,55,42,.1); }
/* Keyboard reaches these too, and gets the same affordance plus a real ring. */
.atile:focus-visible {
  border-color: var(--rt-gold); box-shadow: 0 8px 20px rgba(20,55,42,.1);
  outline: 2px solid var(--rt-gold); outline-offset: 2px;
}
.atile__icon {
  flex-shrink: 0; width: 42px; height: 42px; border-radius: var(--rt-radius-panel);
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(201,162,39,.14); border: 1px solid rgba(201,162,39,.35);
  font-family: var(--rt-font-display); font-size: 18px; font-weight: 400;
  color: var(--rt-gold-dark);
  /* The glyphs are drawn at different heights in the serif face; centring the
     line box keeps all four sitting on the same optical centre. */
  line-height: 1;
}
.atile__body { display: block; min-width: 0; }
.atile__title {
  display: block; font-family: var(--rt-font-ui); font-size: 15px; font-weight: 600;
  color: var(--rt-ink); line-height: 1.35;
}
.atile__desc {
  display: block; font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 400;
  line-height: 1.6; color: var(--rt-secondary); margin-top: 5px;
}
.atile__open {
  display: block; font-family: var(--rt-font-ui); font-size: 12px; font-weight: 600;
  color: var(--rt-green-button); margin-top: var(--rt-s2);
}
/* INLINE INSIDE THE TITLE, never a sibling of it. It is rendered within the
   title span so it cannot wrap onto a line of its own and lose the words it
   qualifies. Distinct from .fwbeta, which this deliberately does not reuse:
   the handoff specifies 8.5px/.12em and a different border, and matching the
   approved card mattered more than sharing a class with the old link row. */
.atile__beta {
  display: inline-block; vertical-align: middle;
  margin-left: var(--rt-s2); padding: 2px 8px;
  font-family: var(--rt-font-ui); font-size: 8.5px; font-weight: 700;
  letter-spacing: .12em;
  color: var(--rt-gold-dark);
  background: rgba(201,162,39,.14); border: 1px solid rgba(201,162,39,.45);
  border-radius: var(--rt-radius-pill);
}
/* ONE COLUMN before the cards get too narrow to read, rather than letting the
   grid overflow its column and put a horizontal scrollbar on the page. */
@media (max-width: 720px) {
  .atiles { grid-template-columns: 1fr; }
}

/* the remediation step: the questions a tutor missed, re-presented */
.tredo {
  background: var(--rt-offwhite); border: 1px solid var(--rt-border-gold);
  border-radius: var(--rt-radius-card); padding: var(--rt-s6);
  margin: var(--rt-s6) 0;
}
.tredo__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 22px;
  color: var(--rt-ink); line-height: 1.25; margin: var(--rt-s2) 0 var(--rt-s2);
}
.tredo .tq { background: var(--rt-white); }

/* the combined assessment score on the tutor's training home */
.tcombined {
  display: flex; gap: var(--rt-s6); align-items: flex-start; flex-wrap: wrap;
  background: var(--rt-offwhite); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-card); padding: var(--rt-s5) var(--rt-s6); margin-top: var(--rt-s5);
}
.tcombined--ok { border-color: var(--rt-border-gold); background: rgba(201,162,39,.07); }
.tcombined__main { flex: 0 0 auto; min-width: 180px; }
.tcombined__value {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 30px;
  color: var(--rt-ink); line-height: 1.1; margin-top: var(--rt-s1);
}
.tcombined__frac { display: block; font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-muted); margin-top: var(--rt-s1); }
.tcombined__body { flex: 1; min-width: 260px; font-family: var(--rt-font-ui); font-size: 13px; line-height: 1.65; color: var(--rt-body); margin: 0; }

/* the shared training checkmark (src/views/partials/check.ejs). Sized per
   context; the colour always comes from whatever encloses it. */
.tick { display: inline-block; width: 12px; height: 12px; vertical-align: -1px; }
.tstep__dot .tick { width: 10px; height: 10px; vertical-align: 0; }
.tmod__mark .tick { width: 16px; height: 16px; }
.railitem__dot .tick { width: 11px; height: 11px; }
.lessondone .tick { width: 12px; height: 12px; margin-right: 2px; }

/* ══ THE TRAINING TAB ON A PHONE ═══════════════════════════════════════════
   Reported from an installed app on an iPhone. Two symptoms, one cause.

   The lesson list under an open module was a column roughly 100px wide wrapping
   one word to a line -- "2. Before / Stage 1: / The / Framework / Rule" -- and a
   locked module's "Unlocks after ..." note sat ON TOP OF the module title.

   .tmod IS A THREE-PART DESKTOP ROW -- mark, text block, then status and CTA --
   and nothing below the desktop breakpoint ever told it to stop being one.
   .tmod__side is `flex: 0 0 auto`, so it never gives back a pixel; .tmod__main
   is `flex: 1` with `min-width: 0`, so it absorbs the entire shortfall and at
   390px there is next to nothing left of it. That is the 100px column. The title
   is 20px display type in that same shrunken box with no overflow rule, so it
   spills out of the box and across the note beside it. That is the overlap. Fix
   the row and both go.

   A GRID, NOT flex-wrap. What is wanted is "mark and text on line one, status
   and CTA on line two", which is a statement about rows and columns. flex-wrap
   decides its breaks from hypothetical sizes before any growing happens, so it
   cannot be asked to infer that; the admin trust rows learned this the expensive
   way and the comment there records it.

   WHY THE BLOCK IS DOWN HERE AND NOT UP BESIDE .tmod. A media query adds no
   specificity, so every override below wins on source order alone, and
   .tcombined__body's 260px min-width is declared further down the file than
   .tmod is. Written up beside the rules it overrides, this block would have read
   as a fix and left the page broken -- the same mistake the day-pill block
   records in its own comment. It is placed after the last of the .t* rules it
   touches, and training-mobile-layout.test.js asserts it stays there.

   LAYOUT ONLY. Nothing here changes a destination, an order, or what a control
   does.

   THE WIDTHS BELOW ARE ARITHMETIC, NOT MEASUREMENTS. There is no browser in
   this repo's suite, so every number in these comments is derived from the
   declared values -- page padding, card padding, the fixed track widths -- and
   not read off a rendered page. They are here to show the reasoning, and they
   are the part of this block that still wants a look on a real phone. */
@media (max-width: 760px), (max-height: 499px) {
  .tmod {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    align-items: start; gap: var(--rt-s3) var(--rt-s4);
    padding: var(--rt-s4) var(--rt-s5);
  }
  /* The mark keeps a column of its own, so the title, the meta and the lesson
     list share one left edge instead of stepping around the circle. */
  .tmod__main { grid-column: 2; }
  /* Status and CTA take the full width of line two and start at the card's left
     edge. Right-aligned they would read as a second column that is no longer
     there, and `margin-left: auto` on a full-width row is how a CTA ends up
     alone against the right edge with a hole beside it. */
  .tmod__side {
    grid-column: 1 / -1; flex-wrap: wrap; justify-content: flex-start;
    gap: var(--rt-s3) var(--rt-s4);
  }
  .tmod__unlocks { text-align: left; }

  /* ONE LESSON PER LINE. The desktop list flows horizontally and wraps, which is
     right where the links are short against a wide row and wrong here even with
     the row fixed: two titles that each wrap mid-phrase, separated by nothing
     but a 16px gap, give a reader no edge between one lesson and the next. */
  .tlessons { flex-direction: column; gap: var(--rt-s2); }
  /* So the underline is the width of the title rather than of the column. */
  .tlesson a { display: inline-block; }

  /* THE BANNER AND ITS STEPPER. Three .tstep columns of a fixed 96px plus two
     bars of at least 20px is a 328px minimum, against 232px of room inside the
     banner at a 320px viewport. .tstatus is `overflow: hidden`, so this did not
     show up as a scrollbar: the third step, Certified, was simply cut off the
     right edge of the card. Equal shares of the room that exists instead, and
     the bars take a fixed 20px rather than a share, so the labels get the rest. */
  .tstatus { padding: var(--rt-s5) var(--rt-s4); }
  .tstep { width: auto; flex: 1 1 0; min-width: 0; }
  .tsteps__bar { flex: 0 0 var(--rt-s5); min-width: 0; }

  /* 260px inside a card with about 244px to give at 320px. Unlike the stepper
     nothing clips this one, so it was the document that got wider than the
     window. */
  .tcombined__body { min-width: 0; }

  /* NOT ONE OF THE THREE REPORTED FAILURES, and not seen in a screenshot. Found
     auditing the rest of the tab for the same shape, and it is the same shape: a
     two-part row -- a 34px score beside a paragraph -- with no instruction to
     stop being one. The paragraph cannot be crushed the way the lesson list was,
     because a flex item's automatic minimum is its longest word, but at 390px
     the arithmetic leaves it about 219px beside a number that needs 75. The
     score goes above the sentence it belongs to. */
  .tresult {
    flex-direction: column; align-items: flex-start;
    gap: var(--rt-s2); padding: var(--rt-s4) var(--rt-s5);
  }
}

/* ── lesson card deck (handoff/README.md) ───────────────────────────────────
   The gamified reader that replaces the single-page lesson view. Values here
   are the handoff's, literally: where the README states a colour, a size or a
   duration, it is written out rather than mapped onto a near-enough token, so
   a later reader can check the design against the file it came from. The
   tokens are used wherever the two already agree. */

/* THE READER FITS THE VIEWPORT. The deck is a screen, not a document: header,
   stage, footer and footnote are one flex column sized to the viewport less the
   60px portal nav, and nothing scrolls.

   The vertical page padding is removed for this screen alone. `.page` carries
   28px top and 44px bottom, which sat OUTSIDE a wrap already sized to
   100vh - 60px, so the document was 72px taller than the window whatever the
   card contained. The horizontal 26px stays: it is what keeps the rail and the
   card column off the window edges, and this change is about height.

   MIN-HEIGHT, NOT HEIGHT, and that is the edge case. A card taller than the
   space available grows the stage past its share, the wrap past its minimum,
   and the page scrolls for that card. The alternative, a fixed height with
   overflow hidden, would silently crop the bottom of a long card, and shrinking
   the card to fit would misrepresent the sizes the design specifies.

   FULL BLEED, BOTH EDGES. `.page` centres a 1180px container, which is right
   for a document and wrong for a reader: it floated the rail in from the window
   edge and left the deck sitting in a box. Here the rail runs flush to the left
   edge and the content region takes everything from the rail to the right edge.
   No horizontal padding on the page at all: the rail and the four content bands
   own their own insets. */
.page--deck { padding: 0; max-width: none; margin: 0; }
.page--deck .lessonwrap { min-height: calc(100vh - 60px); }
.page--deck .lessonrail {
  flex: 0 0 280px; background: var(--rt-offwhite);
  border-right: 1px solid var(--rt-border-card);
  padding: var(--rt-s6) 22px;
}

/* UNCAPPED. The content region runs from the rail to the right edge of the
   window. Capping it at 900px skewed the whole screen left: the XP pill, the
   step counter and Continue all sat in the middle of the window with dead cream
   beside them, which read as a mistake rather than a measure.

   The MEASURE now lives on the card alone, which keeps its 720px and centres
   itself in the region. The four bands span the full width so their furniture
   lands where the eye expects it: the XP pill in the top right corner, Continue
   at the bottom right. The lesson title stays left aligned above the stage,
   because a heading belongs on the left margin whatever the region is doing. */
.deck {
  flex: 1; min-width: 0; background: var(--rt-cream);
  display: flex; flex-direction: column; align-self: stretch;
}
/* Tightened from 30px. The gaps between the four bands are the only vertical
   measurements this rework touches; every size inside them is unchanged. */
.deck__head { padding: var(--rt-s5) var(--rt-s9) 0; }
.deck__meta { display: flex; align-items: center; gap: var(--rt-s3); }
.deck__count {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--rt-muted);
}
/* The XP pill. Cosmetic, private, and shown to nobody but its owner. */
.xppill {
  margin-left: auto; border-radius: var(--rt-radius-pill); padding: 4px 12px;
  background: rgba(201,162,39,.14); border: 1px solid rgba(201,162,39,.45);
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  color: var(--rt-gold-dark); white-space: nowrap;
}
.deck__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 28px;
  color: var(--rt-ink); margin: 10px 0 0; line-height: 1.25;
}

/* The stage. One card, and nothing else in the document.

   THE CARD SITS ABOVE CENTRE, one part of the free space above it to three
   below. The two pseudo-element spacers are what carry that ratio: they have a
   zero basis, so they divide only what is left over after the card has taken
   its natural height, and they collapse to nothing when there is none.

   The card itself is flex: 0 0 auto. It never absorbs the ratio and it never
   shrinks, so a card too tall for the stage overflows into a page scroll rather
   than being squeezed. */
.deck__stage {
  flex: 1 1 auto; display: flex; flex-direction: column; align-items: center;
  padding: var(--rt-s3) var(--rt-s9);
}
.deck__stage::before { content: ""; flex: 1 1 0; min-height: 0; }
.deck__stage::after  { content: ""; flex: 3 1 0; min-height: 0; }
/* SCOPED TO THE STAGE, and it has to be. `.card` is the shared surface used by
   every admin section, the queue, the survey and the competency screens. An
   unscoped max-width here capped all of them at 880px while their neighbours
   (the auto-send banner, the Add tutor pair) took the container's full width, so
   the admin page had two different right edges. The measure belongs to the
   reader's card alone. */
.deck__stage > .card { width: 100%; max-width: 880px; flex: 0 0 auto; }

@keyframes cardIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.cardin { animation: cardIn .4s ease; }
@media (prefers-reduced-motion: reduce) {
  .cardin, .medal, .confetti { animation: none; }
}

/* CARD TYPOGRAPHY, SCALED ~112% FOR READABILITY.
   Every line-height here is unitless, so raising a size carries its leading up
   with it and the proportions are unchanged.

   What does NOT scale, deliberately: the kicker, the XP pill, the small caps
   labels on reveal rows, the lesson title in the header block, the rail, and
   the footer. Those are chrome. Growing them with the prose would have made the
   screen louder rather than easier to read, which is the opposite of the point. */
.card__kicker {
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--rt-gold-dark);
}
.card__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 29px;
  line-height: 1.3; color: var(--rt-ink); margin: 11px 0 0;
}
.card__title--check { font-size: 25px; line-height: 1.35; }
.card__quote {
  border-left: 3px solid var(--rt-gold); padding: 8px 0 8px 24px; margin: 16px 0 0;
  font-family: var(--rt-font-display); font-style: italic; font-weight: 400;
  font-size: 17.5px; line-height: 1.6; color: var(--rt-green-button);
}
.card__body {
  font-family: var(--rt-font-ui); font-size: 17px; line-height: 1.75;
  color: var(--rt-body); margin-top: 14px;
}
.card__body p { margin: 0 0 var(--rt-s3); }
.card__body p:last-child { margin-bottom: 0; }

/* The session-skeleton timeline, plain CSS per the handoff's Assets note. */
.tline { display: flex; gap: 4px; margin-top: var(--rt-s6); }
.tline__seg--open  { width: 15%; }
.tline__seg--work  { flex: 1; }
.tline__seg--close { width: 13%; }
.tline__bar { height: 32px; border-radius: 4px; }
.tline__seg--open  .tline__bar { background: var(--rt-gold); }
.tline__seg--work  .tline__bar { background: var(--rt-green-button); }
.tline__seg--close .tline__bar { background: rgba(27,67,50,.35); }
.tline__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .04em; margin-top: 7px;
}
.tline__seg--open  .tline__label { color: var(--rt-gold-dark); }
.tline__seg--work  .tline__label { color: var(--rt-green-button); }
.tline__seg--close .tline__label { color: var(--rt-muted); }

/* Reveal rows. A shut row is a button and carries no text to reveal early. */
.revrows { display: flex; flex-direction: column; gap: 10px; margin-top: var(--rt-s5); }
.revrows form { display: block; }
.revrow { display: flex; width: 100%; text-align: left; }
.revrow--shut {
  align-items: center; justify-content: space-between; gap: var(--rt-s3);
  background: rgba(27,67,50,.05); border: 1.5px dashed rgba(27,67,50,.28);
  border-radius: var(--rt-radius-card); padding: 19px 23px; cursor: pointer;
  font: inherit; transition: background .15s ease, border-color .15s ease;
}
.revrow--shut:hover { background: rgba(201,162,39,.1); border-color: var(--rt-gold); }
.revrow__chip {
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  letter-spacing: .1em; color: var(--rt-green-button); white-space: nowrap;
}
.revrow__cta {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 600;
  color: var(--rt-gold-dark); white-space: nowrap;
}
.revrow--open {
  flex-direction: column; gap: 5px; background: var(--rt-white);
  border: 1px solid var(--rt-border-panel); border-radius: var(--rt-radius-card);
  padding: 17px 23px; animation: cardIn .3s ease;
}
.revrow__label {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .1em; color: var(--rt-gold-dark);
}
.revrow__text {
  font-family: var(--rt-font-ui); font-size: 15.5px; line-height: 1.6; color: var(--rt-body);
}

/* Quick check options. Identical until picked: the page holds no answer key. */
.opts { display: flex; flex-direction: column; gap: 10px; margin-top: var(--rt-s5); }
.opts form { display: block; }
.opt {
  display: flex; flex-direction: column; gap: 8px; width: 100%; text-align: left;
  background: var(--rt-white); border: 1.5px solid var(--rt-border-control);
  border-radius: var(--rt-radius-card); padding: 16px 23px; cursor: pointer;
  font: inherit; transition: border-color .15s ease, background .15s ease;
}
.opt:hover { border-color: var(--rt-gold); }
.opt__text {
  font-family: var(--rt-font-ui); font-size: 15.5px; line-height: 1.55; color: var(--rt-ink);
}
.opt__fb {
  font-family: var(--rt-font-ui); font-size: 14px; line-height: 1.55;
  border-top: 1px solid rgba(27,67,50,.1); padding-top: 11px;
}
/* Wrong stays clickable elsewhere and the tutor retries, per the handoff. */
.opt--wrong { border-color: rgba(161,66,50,.5); background: rgba(161,66,50,.05); }
.opt--wrong .opt__fb { color: #A14232; }
.opt--right { border-color: var(--rt-green-button); background: rgba(27,67,50,.06); cursor: default; }
.opt--right .opt__fb { color: var(--rt-green-button); }

/* The completion card. */
.card--done { text-align: center; display: flex; flex-direction: column; align-items: center; }
@keyframes pop {
  0%   { transform: scale(.4); opacity: 0; }
  60%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.medal {
  position: relative; width: 80px; height: 80px; border-radius: 50%;
  background: var(--rt-gold); display: grid; place-items: center;
  box-shadow: 0 10px 26px rgba(201,162,39,.35); animation: pop .5s ease;
}
.medal__star { font-size: 34px; line-height: 1; color: var(--rt-green-deep); }
@keyframes fall {
  from { transform: translateY(0) rotate(0deg); opacity: 1; }
  to   { transform: translateY(90px) rotate(220deg); opacity: 0; }
}
.confetti { position: absolute; top: 0; width: 7px; height: 7px; animation: fall 1.1s ease forwards; }
.confetti--1 { left: -18px; width: 6px; height: 6px; background: var(--rt-gold); animation-delay: .05s; }
.confetti--2 { left: 10px;  width: 8px; height: 8px; background: var(--rt-green-button); animation-delay: .15s; }
.confetti--3 { left: 40px;  width: 6px; height: 6px; background: var(--rt-gold-bright); border-radius: 50%; animation-delay: .25s; animation-duration: 1.25s; }
.confetti--4 { left: 68px;  width: 9px; height: 9px; background: var(--rt-gold); animation-delay: .35s; animation-duration: 1.2s; }
.confetti--5 { left: 92px;  width: 6px; height: 6px; background: var(--rt-green-button); border-radius: 50%; animation-delay: .45s; }
.card__title--done { font-size: 31px; margin-top: var(--rt-s6); }
.doneline {
  font-family: var(--rt-font-ui); font-size: 15.5px; line-height: 1.6;
  color: var(--rt-body); margin: var(--rt-s3) 0 var(--rt-s6);
}
.doneline strong { color: var(--rt-gold-dark); font-weight: 700; }
.replay {
  background: none; border: 0; padding: 0; margin-top: var(--rt-s4); cursor: pointer;
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary);
  text-decoration: underline;
}
.replay:hover { color: var(--rt-ink); }

/* The footer bar. Hidden on the completion card. */
.deck__foot {
  display: flex; align-items: center; gap: var(--rt-s4);
  padding: var(--rt-s3) var(--rt-s9); border-top: 1px solid rgba(27,67,50,.1);
}
.segs { display: flex; gap: 4px; flex: 1; min-width: 0; }
.seg { flex: 1; height: 4px; border-radius: var(--rt-radius-pill); }
.seg--done    { background: var(--rt-gold); }
.seg--current { background: var(--rt-green-button); }
.seg--todo    { background: rgba(27,67,50,.14); }
.deck__step {
  font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); white-space: nowrap;
}
/* Back mirrors Continue's position and metrics, and deliberately not its
   weight: going on is the action, going back is a way out. Invisible rather
   than absent on the first card, so the segments beside it never shift. */
.deck__backform { flex: 0 0 auto; }
.deck__backform--off { visibility: hidden; pointer-events: none; }
.deck__back {
  background: none; border: 1px solid var(--rt-border-control); cursor: pointer;
  border-radius: var(--rt-radius-button); padding: 11px var(--rt-s5);
  font-family: var(--rt-font-ui); font-size: 13px; font-weight: 600; color: var(--rt-secondary);
}
.deck__back:hover { border-color: var(--rt-green-button); color: var(--rt-green-button); }
.deck__next { padding: 11px var(--rt-s6); font-size: 13px; }
.deck__next:disabled { opacity: .35; pointer-events: none; }
/* The footnote keeps its type size and is free to wrap to however many lines
   that takes. Only the space above and below it is narrowed. */
.deck__note {
  padding: var(--rt-s2) var(--rt-s9) var(--rt-s4); margin: 0;
  font-family: var(--rt-font-ui); font-size: 11.5px; line-height: 1.6; color: var(--rt-muted);
}

@media (max-width: 860px) {
  .page--deck .lessonwrap { flex-direction: column; }
  .page--deck .lessonrail { flex: none; width: 100%; border-right: 0; border-bottom: 1px solid var(--rt-border-card); }
  .deck__head, .deck__stage, .deck__foot, .deck__note { padding-left: var(--rt-s5); padding-right: var(--rt-s5); }
}

/* ── lesson step editor (admin) ─────────────────────────────────────────────
   A founder tool, in the same shape as the assessment question editor. */
.stepsrc {
  background: var(--rt-cream); border: 1px solid var(--rt-rule);
  border-radius: var(--rt-radius-card); padding: var(--rt-s3) var(--rt-s4);
  margin-bottom: var(--rt-s4);
}
.stepsrc__quote {
  border-left: 3px solid var(--rt-gold); padding-left: var(--rt-s3);
  font-family: var(--rt-font-display); font-style: italic; font-size: 13.5px;
  line-height: 1.6; color: var(--rt-green-button); margin-bottom: var(--rt-s2);
}
.stepsrc__body {
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.65;
  color: var(--rt-secondary); white-space: pre-wrap;
}
.stepitem {
  display: grid; grid-template-columns: auto 150px 1fr 1fr; gap: var(--rt-s2);
  align-items: center; margin-bottom: var(--rt-s2);
}
@media (max-width: 780px) {
  .stepitem { grid-template-columns: auto 1fr; }
}

/* ── competency tests (handoff/competency/README.md) ─────────────────────────
   Per-subject certification. Values are the handoff's; tokens are used wherever
   the two already agree. */

.cchips { display: flex; gap: var(--rt-s2); flex-wrap: wrap; margin: var(--rt-s4) 0 var(--rt-s6); }
.cchips--right { margin-left: auto; }
.cchip {
  border-radius: var(--rt-radius-pill); padding: 4px 12px; white-space: nowrap;
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  background: var(--rt-tint-neutral); color: var(--rt-secondary);
}
.cchip--gold { background: rgba(201,162,39,.14); border: 1px solid rgba(201,162,39,.45); color: var(--rt-gold-dark); }
.cchip--green { background: none; border: 1px solid rgba(27,67,50,.3); color: var(--rt-green-button); }
.cchip--red { background: rgba(161,66,50,.12); border: 1px solid rgba(161,66,50,.4); color: #A14232; }
.cnone { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-muted); }

.cgroup { margin-bottom: var(--rt-s7); }
.crow {
  display: flex; align-items: center; gap: var(--rt-s3); flex-wrap: wrap;
  padding: 13px var(--rt-s4); border-radius: var(--rt-radius-card);
  border: 1px solid transparent; margin-bottom: 6px;
}
.crow--certified { background: var(--rt-white); border-color: rgba(201,162,39,.4); }
.crow--admin_passed { background: var(--rt-white); border-color: rgba(201,162,39,.4); }
.crow--unlocked {
  background: var(--rt-offwhite); border: 1.5px solid rgba(27,67,50,.3);
  box-shadow: 0 2px 8px rgba(20,55,42,.06);
}
.crow--failed { background: rgba(161,66,50,.06); border-color: rgba(161,66,50,.35); }
.crow--locked { opacity: .62; border-color: var(--rt-rule); }
.crow__badge {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
  font-size: 11px; color: var(--rt-green-deep);
}
.crow--certified .crow__badge, .crow--admin_passed .crow__badge { background: var(--rt-gold); }
.crow--failed .crow__badge { background: rgba(161,66,50,.18); color: #A14232; }
.crow__name { font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-ink); font-weight: 600; }
.crow__note { font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); }
.crow__state { font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700; letter-spacing: .06em; margin-left: auto; }
.crow__state--gold { color: var(--rt-gold-dark); }
.crow__state--red { color: #A14232; }
.crow .t-btn, .crow .cunlock, .crow .crevoke { margin-left: auto; }
.cunlock {
  background: none; border: 1px solid var(--rt-border-control); border-radius: var(--rt-radius-button);
  padding: 8px 16px; font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--rt-green-button); cursor: pointer;
}
.cunlock:hover { border-color: var(--rt-green-button); }
.crevoke { background: none; border: 0; padding: 0; cursor: pointer; font-family: var(--rt-font-ui); font-size: 12.5px; color: #A14232; text-decoration: underline; }
.coverride { flex: 0 0 100%; margin-top: 6px; }
.coverride summary { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-gold-dark); cursor: pointer; }
.coverride .in { max-width: 320px; margin: var(--rt-s2) 0; }
/* Reversing a certification. SECONDARY to Override: pass on purpose, and the
   only red control on this screen. Muted until opened, so taking a standing
   away never reads as an ordinary action sitting next to granting one. */
.coverride--undo summary { color: var(--rt-muted, #6B7280); font-size: 11.5px; }
.coverride--undo summary:hover { color: var(--rt-danger, #9B2C2C); }
.coverride--undo[open] summary { color: var(--rt-danger, #9B2C2C); }
.submit--danger { background: var(--rt-danger, #9B2C2C); border-color: var(--rt-danger, #9B2C2C); }
.submit--danger:hover { background: #7A1F1F; border-color: #7A1F1F; }

/* the focus stage */
.testbody { background: var(--rt-cream); }
.testbar {
  background: var(--rt-green-deep); height: 52px; display: flex; align-items: center;
  justify-content: center; position: relative; padding: 0 var(--rt-s5);
}
.testbar__label {
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 700; letter-spacing: .16em;
  color: var(--rt-offwhite);
}
.testbar__timer {
  position: absolute; right: var(--rt-s5); border-radius: var(--rt-radius-pill); padding: 4px 12px;
  background: rgba(201,162,39,.18); border: 1px solid rgba(201,162,39,.45);
  font-family: var(--rt-font-display); font-size: 14px; color: var(--rt-gold-bright);
}
.testbar__timer--warn { color: #A14232; background: rgba(161,66,50,.18); border-color: rgba(161,66,50,.5); }
.tprog { display: flex; gap: 5px; justify-content: center; padding: var(--rt-s4) var(--rt-s5) 0; flex-wrap: wrap; }
.tdot { width: 8px; height: 8px; border-radius: 50%; background: rgba(27,67,50,.18); }
.tdot--now { background: var(--rt-gold); }
.tdot--done { background: var(--rt-green-button); }
.tprog--bar { gap: 3px; max-width: 640px; margin: 0 auto; }
.tseg { flex: 1; height: 4px; border-radius: var(--rt-radius-pill); background: rgba(27,67,50,.18); }
.tseg--now { background: var(--rt-gold); }
.tseg--done { background: var(--rt-green-button); }
.teststage { max-width: 640px; margin: 0 auto; padding: var(--rt-s7) var(--rt-s5) var(--rt-s9); }
.tcard__kicker { font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 700; letter-spacing: .16em; color: var(--rt-gold-dark); }
.tcard__q { font-family: var(--rt-font-display); font-weight: 400; font-size: 24px; line-height: 1.4; color: var(--rt-ink); margin: 12px 0 0; }
.topts { display: flex; flex-direction: column; gap: 10px; margin-top: var(--rt-s6); }
.topt {
  display: flex; align-items: flex-start; gap: var(--rt-s3); cursor: pointer;
  background: var(--rt-white); border: 1.5px solid var(--rt-border-control);
  border-radius: var(--rt-radius-card); padding: 14px 17px;
}
.topt input { position: absolute; opacity: 0; width: 0; height: 0; }
.topt__dot { flex: 0 0 auto; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--rt-border-control); margin-top: 1px; }
.topt--on { border-color: var(--rt-green-button); background: rgba(27,67,50,.06); }
.topt--on .topt__dot { border-color: var(--rt-green-button); box-shadow: inset 0 0 0 3px var(--rt-white), inset 0 0 0 9px var(--rt-green-button); }
.topt__text { font-family: var(--rt-font-ui); font-size: 15px; line-height: 1.55; color: var(--rt-ink); }
.tanswer {
  width: 100%; margin-top: var(--rt-s6); background: var(--rt-white);
  border: 1.5px solid var(--rt-border-control); border-radius: var(--rt-radius-card);
  padding: 14px 17px; font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-ink);
}
.tanswer:focus { outline: none; border-color: var(--rt-gold); }
.tkeys { display: flex; gap: 6px; flex-wrap: wrap; margin-top: var(--rt-s3); }
.tkey {
  background: var(--rt-white); border: 1px solid var(--rt-border-control); border-radius: 4px;
  padding: 7px 12px; font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-body); cursor: pointer;
}
.tkey:hover { border-color: var(--rt-gold); }
.tframe { margin-top: var(--rt-s6); }
.tframe__label { font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--rt-gold-dark); }
.tframe__prompt { font-family: var(--rt-font-ui); font-size: 14px; color: var(--rt-body); margin: 6px 0 0; }
.tframe__box {
  width: 100%; margin-top: var(--rt-s2); background: var(--rt-white);
  border: 1.5px solid var(--rt-border-control); border-radius: var(--rt-radius-card);
  padding: 13px 16px; font-family: var(--rt-font-ui); font-size: 16px; line-height: 1.6; color: var(--rt-ink);
}
.tframe__box:focus { outline: none; border-color: var(--rt-gold); }
.tframe__help { font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); margin: 6px 0 0; }
.tfoot { display: flex; align-items: center; gap: var(--rt-s4); margin-top: var(--rt-s7); }
.tfoot__spacer { flex: 1; }
.tprev { background: none; border: 0; padding: 0; cursor: pointer; font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary); }
.thint { font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); }
.tnext:disabled { opacity: .35; pointer-events: none; }

/* the result */
.cresult { max-width: 560px; margin: var(--rt-s8) auto; text-align: center; }
.cmedal { width: 80px; height: 80px; border-radius: 50%; margin: 0 auto; display: grid; place-items: center; font-size: 34px; animation: pop .5s ease; }
.cmedal--pass { background: var(--rt-gold); color: var(--rt-green-deep); box-shadow: 0 10px 26px rgba(201,162,39,.35); }
.cmedal--fail { background: rgba(161,66,50,.18); color: #A14232; }
.cresult__score { font-family: var(--rt-font-display); font-size: 46px; color: var(--rt-ink); margin-top: var(--rt-s4); }
.cresult__title { font-family: var(--rt-font-display); font-weight: 400; font-size: 24px; color: var(--rt-ink); margin: var(--rt-s2) 0 var(--rt-s5); }
.cbreak { background: var(--rt-white); border: 1px solid var(--rt-border-panel); border-radius: var(--rt-radius-card); padding: var(--rt-s4); margin: var(--rt-s6) 0; text-align: left; }
.cbreak__row { display: flex; justify-content: space-between; font-family: var(--rt-font-ui); font-size: 13.5px; color: var(--rt-body); padding: 6px 0; }
.cbreak__gold { color: var(--rt-gold-dark); font-weight: 700; }

/* the roster */
.croster { border-top: 1px solid var(--rt-rule); }
.croster__head, .croster__row {
  display: grid; grid-template-columns: 190px 1fr 130px 130px 90px 80px;
  gap: var(--rt-s3); align-items: center; padding: 12px 0; border-bottom: 1px solid var(--rt-rule);
}
.croster__head { font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--rt-muted); }
.croster__tutor { display: flex; align-items: center; gap: var(--rt-s3); }
.cavatar { flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%; background: var(--rt-green-deep); color: var(--rt-gold-bright); display: grid; place-items: center; font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700; }
.cavatar--lg { width: 46px; height: 46px; font-size: 15px; }
.croster__name { display: block; font-family: var(--rt-font-ui); font-size: 13.5px; font-weight: 600; color: var(--rt-ink); }
.croster__meta { display: block; font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); }
.croster__cell { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.cmore { position: relative; border: 1px dashed var(--rt-border-control); border-radius: var(--rt-radius-pill); padding: 3px 10px; font-family: var(--rt-font-ui); font-size: 11px; color: var(--rt-secondary); cursor: pointer; }
.cpop { display: none; }
/* Anchored 10px to the RIGHT of the chip and vertically centred, so opening it
   never grows the row. */
.cmore--open .cpop {
  display: flex; flex-direction: column; gap: 5px; position: absolute; left: calc(100% + 10px);
  top: 50%; transform: translateY(-50%); z-index: 20; background: var(--rt-white);
  border: 1px solid var(--rt-border-panel); border-radius: var(--rt-radius-card);
  padding: var(--rt-s3); box-shadow: var(--rt-shadow-frame); min-width: 200px;
}
.cpop__head { font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--rt-muted); }

.cpanelhead { display: flex; align-items: center; gap: var(--rt-s4); margin: var(--rt-s4) 0 var(--rt-s6); }
.caccordion { border-bottom: 1px solid var(--rt-rule); padding: var(--rt-s3) 0; }
/* One rule serves the competency hub and the unlock manager, so their headers
   cannot drift apart. The default disclosure triangle is hidden in favour of a
   chevron on the right that turns when the group opens. */
.caccordion__head {
  display: flex; align-items: center; gap: var(--rt-s4); cursor: pointer;
  padding: 10px var(--rt-s3); border-radius: var(--rt-radius-input);
  list-style: none; transition: background .12s ease;
}
.caccordion__head::-webkit-details-marker { display: none; }
.caccordion__head::marker { content: ''; }
.caccordion__head > .croster__meta { margin-left: auto; }
.caccordion__head::after {
  content: '\25BE'; flex: 0 0 auto; color: var(--rt-gold-dark); font-size: 12px;
  line-height: 1; transition: transform .15s ease;
}
.caccordion[open] > .caccordion__head::after { transform: rotate(180deg); }
.caccordion__head:hover { background: var(--rt-tint-neutral); }
.caccordion__head:hover .t-eyebrow { color: var(--rt-ink); }
.caccordion__head:focus-visible { outline: 2px solid var(--rt-gold); outline-offset: 2px; }
.cframe { border-top: 1px solid var(--rt-rule); padding: var(--rt-s3) 0; }
.cframe__q { font-family: var(--rt-font-ui); font-size: 13.5px; font-weight: 600; color: var(--rt-ink); }
.cframe__p { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-muted); margin: 4px 0; }
.cframe__a { white-space: pre-wrap; font-family: var(--rt-font-ui); font-size: 13.5px; line-height: 1.6; color: var(--rt-body); background: var(--rt-cream); border-radius: var(--rt-radius-card); padding: var(--rt-s3); margin: 6px 0 0; }
@media (max-width: 900px) {
  .croster__head { display: none; }
  .croster__row { grid-template-columns: 1fr; }
}

/* The free-response parse preview. Three states plus a pending one, because
   grading is failure-closed and a tutor must see an unreadable answer before
   they submit it rather than in their score. */
.tread {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--rt-s2);
  margin-top: var(--rt-s3); padding: 10px 14px; border-radius: var(--rt-radius-card);
  border: 1px solid var(--rt-rule); background: var(--rt-offwhite);
}
.tread__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 700;
  letter-spacing: .12em; color: var(--rt-muted); white-space: nowrap;
}
.tread__body {
  font-family: var(--rt-font-display); font-size: 17px; color: var(--rt-ink);
}
.tread__hint {
  flex: 1 0 100%; font-family: var(--rt-font-ui); font-size: 11.5px; line-height: 1.55;
  color: var(--rt-muted);
}
.tread[data-state="empty"] .tread__label,
.tread[data-state="empty"] .tread__body { display: none; }
.tread[data-state="ok"] { border-color: rgba(27,67,50,.25); }
.tread[data-state="ok"] .tread__label { color: var(--rt-green-button); }
.tread[data-state="pending"] { opacity: .55; }
/* Unreadable is the state that matters, so it is the loud one. */
.tread[data-state="bad"] { border-color: rgba(161,66,50,.5); background: rgba(161,66,50,.06); }
.tread[data-state="bad"] .tread__label { color: #A14232; }
.tread[data-state="bad"] .tread__label::after { content: " · NOT READABLE"; }
.tread[data-state="bad"] .tread__hint { color: #A14232; }

/* ── admin roster pair: equal height, filter and pager ────────────────────────
   The two cards stretch to the taller of the pair, structurally: the row
   stretches its items and each card takes an equal share of the container. No
   fixed heights, so it holds as either roster grows. Scoped to the pair, because
   .cols is also the approval screen's and the survey's two-column layout. */
.cols--pair { align-items: stretch; }
.cols--pair > .card { flex: 1 1 0; min-width: 0; }

/* THE HEIGHT MOVES FROM PADDING TO min-height, which is the half of
   design_handoff_roster_spacing item 2 that landed. 8px of vertical padding
   sized the box off the type, so the two could not be tuned separately.
   min-height states the box directly: 40px on a pointer, 44px where it is a
   touch target. Horizontal padding stays as padding because there it IS the
   text inset, and box-sizing keeps the border box on the group's edges.

   THE 13px FONT-SIZE IS NOT HERE, AND THAT IS NOT AN OVERSIGHT. Item 2 asks for
   13px on the desktop with 16px kept below 760px. The 16px floor in this file is
   an iOS zoom fix from a real device incident, and the test that enforces it
   (survey-submitting, "no focusable control in the portal computes under 16px")
   is deliberately WIDTH-BLIND: it reads every rule in this file whatever media
   query it sits in. It is right to be. An iPad in landscape is 1024px or wider
   and is still a touch device with a keyboard that zooms, so a width query
   cannot express "has a pointer" and 13px above 760px would zoom the page on a
   focused filter there. Awaiting Trey's ruling; see the readback. */
.rlist__filter {
  width: 100%; margin: 10px 0 4px; box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 5px; padding: 0 12px;
  min-height: 40px;
  font-family: var(--sans); font-size: 16px; color: var(--body); background: #fff;
}
/* AFTER the rule above and not in the roster's own phone block at line ~1655,
   which sits 2700 lines earlier. A media query adds no specificity, so an
   override written up there would tie with this rule and LOSE on source order,
   silently, at exactly the width it was written for. */
@media (max-width: 760px), (max-height: 499px) {
  .rlist__filter { min-height: 44px; }
}
.rlist__filter:focus { outline: none; border-color: var(--gold); }
.rlist__none { font-size: 13px; color: var(--muted); padding: 12px 0; }
.rlist__pager {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding-top: 10px; margin-top: 4px; border-top: 1px solid var(--line);
}
/* Same trap as .roster__row above: the display declaration defeats [hidden], so
   the pager rendered on a single-page roster despite being marked hidden. */
.rlist__pager[hidden] { display: none; }
.rlist__page {
  background: none; border: 1px solid var(--line); border-radius: 4px; cursor: pointer;
  padding: 6px 12px; font-family: var(--sans); font-size: 12.5px; color: var(--body);
}
.rlist__page:hover:not(:disabled) { border-color: var(--green-btn); color: var(--green-btn); }
.rlist__page:disabled { opacity: .4; cursor: default; }
.rlist__count { font-family: var(--sans); font-size: 12px; color: var(--muted); }

/* ── Framework Builder (AI-graded practice) ─────────────────────────────────
   The handoff's option 1a, recreated on this file's own tokens and fonts. No
   new font loads and no parallel token file: every colour below is a --rt-*
   variable or an alpha of one already in the palette.

   BETA IS PART OF THE FEATURE. The pill ships wherever the feature is named, in
   two skins because it sits on both cream and deep green. */
.fwbeta {
  display: inline-block; vertical-align: middle; margin-left: var(--rt-s2);
  font-family: var(--rt-font-ui); font-size: 9px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--rt-gold-dark); background: var(--rt-tint-gold);
  border: 1px solid var(--rt-border-gold); border-radius: var(--rt-radius-pill);
  padding: 3px 9px;
}
.fwbeta--dark {
  color: var(--rt-gold-bright); background: rgba(201,162,39,.16);
  border-color: rgba(227,181,58,.45);
}

/* The takeover, in the deck reader's pattern: portal nav above, then this page
   goes full bleed and owns everything under it. */
.page--fw { padding: 0; max-width: none; margin: 0; background: var(--rt-cream); }

.fwbar {
  display: flex; align-items: center; gap: var(--rt-s4);
  background: var(--rt-green-deep); padding: 0 var(--rt-s5); height: 48px;
}
.fwbar__back {
  font-family: var(--rt-font-ui); font-size: 12px; color: rgba(255,255,255,.75);
  text-decoration: none; white-space: nowrap;
}
.fwbar__back:hover { color: var(--rt-offwhite); }
.fwbar__mid { flex: 1; display: flex; align-items: center; justify-content: center; }
.fwbar__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 600;
  letter-spacing: .12em; color: rgba(255,255,255,.55); text-transform: uppercase;
}
.fwbar__pill {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  color: var(--rt-offwhite); background: rgba(255,255,255,.08);
  border: 1px solid rgba(255,255,255,.2); border-radius: var(--rt-radius-pill);
  padding: 4px 12px;
}
.fwbar__pill--xp {
  color: var(--rt-gold-bright); background: rgba(201,162,39,.16);
  border-color: rgba(227,181,58,.45);
}

/* 108px is the nav plus this page's own header band; the inset is the nav's
   status-bar pad, subtracted here for the same reason as the deck above. */
.fwwrap { display: flex; min-height: calc(100vh - 108px); }
.fwrail {
  width: 400px; flex: 0 0 400px; border-right: 1px solid rgba(27,67,50,.1);
  padding: var(--rt-s6) var(--rt-s7); display: flex; flex-direction: column; gap: var(--rt-s5);
}
.fwbuild { flex: 1; padding: var(--rt-s6) var(--rt-s7); display: flex; flex-direction: column; }
.fwgap { flex: 1; min-height: var(--rt-s5); }
.fwempty { padding: var(--rt-s9) var(--rt-s7); max-width: 720px; }

.fwchips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: var(--rt-s2); }
.fwchip {
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  color: var(--rt-green-button); background: var(--rt-white);
  border: 1px solid rgba(27,67,50,.2); border-radius: var(--rt-radius-pill);
  padding: 5px 13px; text-decoration: none; white-space: nowrap;
}
.fwchip:hover { border-color: var(--rt-gold); }
.fwchip--on {
  background: var(--rt-green-deep); border-color: var(--rt-green-deep); color: var(--rt-offwhite);
}

/* ── THE SUBJECT SELECTOR: three bucket pills, each with its own menu ───────
   design_handoff_framework_builder, left rail item 1. Built on <details>, so
   [open] is the state both the chevron and the menu read. Values the handoff
   specifies and this file has no token for are written as literals: the menu
   shadow, and the gold tint behind the selected row. */
.fwbuckets { display: flex; gap: 6px; flex-wrap: wrap; margin-top: var(--rt-s2); }
.fwbucket { position: relative; }
/* The pill. Inactive: white with green text and a green chevron. */
.fwbucket__btn {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
  color: var(--rt-green-button); background: var(--rt-white);
  border: 1px solid rgba(27,67,50,.2); border-radius: var(--rt-radius-pill);
  padding: 5px 13px; white-space: nowrap; list-style: none;
}
/* Safari draws its own disclosure triangle without this. */
.fwbucket__btn::-webkit-details-marker { display: none; }
.fwbucket__btn:hover { border-color: var(--rt-gold); }
.fwbucket__btn:focus-visible { outline: 2px solid var(--rt-gold); outline-offset: 2px; }
/* Active bucket: solid deep green, label reads "Bucket · Subject", gold chevron. */
.fwbucket--on > .fwbucket__btn {
  background: var(--rt-green-deep); border-color: var(--rt-green-deep); color: var(--rt-offwhite);
}
.fwbucket--on > .fwbucket__btn .fwbucket__chev { color: var(--rt-gold); }
.fwbucket__chev {
  font-size: 9px; line-height: 1; color: var(--rt-green-button);
  transition: transform .15s ease;
}
/* 180 degrees while the menu is open, on either state of pill. */
.fwbucket[open] > .fwbucket__btn .fwbucket__chev { transform: rotate(180deg); }
/* The menu card. Absolute, so opening one does not shove the prompt card down. */
.fwmenu {
  position: absolute; z-index: 30; top: calc(100% + 6px); left: 0; min-width: 190px;
  background: var(--rt-white); border: 1px solid var(--rt-border-panel);
  border-radius: 10px; box-shadow: 0 10px 28px rgba(20,55,42,.14);
  padding: 6px; display: flex; flex-direction: column;
}
.fwmenu__row {
  display: flex; align-items: center; gap: var(--rt-s2);
  font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 400;
  color: var(--rt-body); text-decoration: none;
  padding: 7px 10px; border-radius: 6px; white-space: nowrap;
}
.fwmenu__row:hover { background: var(--rt-tint-neutral); }
/* Selected: bold, gold tint, and a 6px gold dot. */
.fwmenu__row--on { font-weight: 700; color: var(--rt-ink); background: rgba(201,162,39,.12); }
.fwmenu__dot { width: 6px; height: 6px; border-radius: 50%; background: transparent; flex-shrink: 0; }
.fwmenu__row--on .fwmenu__dot { background: var(--rt-gold); }

/* The empty-bank state. A statement, not an error, so it is set in the same
   Caslon as a real prompt rather than in an alert's voice. */
.fwprompt--soon .fwprompt__soon {
  font-family: var(--rt-font-display); font-size: 17px; line-height: 1.5;
  color: var(--rt-secondary); margin-top: var(--rt-s2);
}
/* The inert builder beside it. Dimmed, so it reads as waiting rather than broken. */
.fwform--soon { opacity: .55; }
.fwform--soon .fwstep__in, .fwform--soon .fwdone__in { cursor: not-allowed; }
.fwform--soon .t-btn[disabled] { opacity: .35; cursor: not-allowed; }

.fwprompt {
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-radius: var(--rt-radius-panel); padding: 18px var(--rt-s5);
}
.fwprompt__text {
  font-family: var(--rt-font-display); font-size: 17px; line-height: 1.5;
  color: var(--rt-ink); margin-top: 10px;
}
.fwprompt__help {
  font-family: var(--rt-font-ui); font-size: 12px; line-height: 1.6;
  color: var(--rt-muted); margin-top: var(--rt-s3);
}
.fwrubric {
  background: rgba(201,162,39,.08); border: 1px solid rgba(201,162,39,.3);
  border-radius: var(--rt-radius-panel); padding: 14px 18px;
}
.fwrubric__lines { display: flex; flex-direction: column; gap: 5px; margin-top: var(--rt-s2); }
.fwrubric__lines span {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-body);
}

/* the grade card, which replaces the prompt */
.fwgrade {
  background: var(--rt-white); border: 1.5px solid var(--rt-border-control);
  border-radius: var(--rt-radius-panel); padding: 18px var(--rt-s5);
}
.fwgrade--top { border-color: var(--rt-gold); }
.fwgrade--down { border-color: rgba(161,66,50,.35); }
.fwgrade__head { display: flex; align-items: center; gap: var(--rt-s3); }
.fwmedal {
  width: 52px; height: 52px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--rt-font-ui); font-size: 22px;
  background: var(--rt-tint-neutral); color: var(--rt-gold-dark);
}
.fwmedal--solid { background: rgba(201,162,39,.2); }
.fwmedal--gold { background: var(--rt-gold); color: var(--rt-green-deep); }
.fwgrade__pct {
  font-family: var(--rt-font-display); font-size: 30px; color: var(--rt-ink); line-height: 1.1;
}
.fwgrade__label {
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 600;
  letter-spacing: .06em; color: var(--rt-green-button);
}
.fwgrade__label--low { color: #A14232; }
.fwbars { display: flex; flex-direction: column; gap: 7px; margin-top: 14px; }
.fwcrit { display: flex; align-items: center; gap: 10px; }
.fwcrit__name {
  width: 78px; flex: 0 0 78px;
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 600; color: var(--rt-secondary);
}
.fwcrit__track {
  flex: 1; height: 6px; border-radius: var(--rt-radius-pill);
  background: rgba(27,67,50,.1); overflow: hidden;
}
.fwcrit__fill { display: block; height: 100%; border-radius: var(--rt-radius-pill); }
.fwcrit__fill--good { background: var(--rt-green-button); }
.fwcrit__fill--mid { background: var(--rt-gold); }
.fwcrit__fill--low { background: #A14232; }
.fwcrit__val {
  width: 24px; text-align: right;
  font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 700; color: var(--rt-ink);
}
.fwgrade__fb {
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.65; color: var(--rt-body);
  margin: 14px 0 0; border-top: 1px solid rgba(27,67,50,.1); padding-top: var(--rt-s3);
}
.fwgrade--down .fwgrade__fb { border-top: 0; padding-top: var(--rt-s2); }
.fwrewrite {
  background: var(--rt-cream); border: 1px solid rgba(27,67,50,.1);
  border-radius: var(--rt-radius-card); padding: var(--rt-s3) 14px; margin-top: var(--rt-s3);
}
.fwrewrite__label {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 700;
  letter-spacing: .12em; color: var(--rt-gold-dark);
}
.fwrewrite__text {
  font-family: var(--rt-font-display); font-style: italic; font-size: 13px; line-height: 1.6;
  color: var(--rt-green-button); margin-top: 6px;
}
.fwgrade__next { display: block; text-align: center; margin-top: 14px; }
.fwgrade__skip {
  display: inline-block; margin-top: var(--rt-s3);
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-secondary);
}

/* the builder */
.fwsteps { display: flex; flex-direction: column; gap: var(--rt-s2); margin-top: var(--rt-s3); }
.fwstep { display: flex; gap: 10px; align-items: center; }
.fwstep__n {
  width: 26px; height: 26px; flex: 0 0 26px; border-radius: 50%;
  background: var(--rt-green-deep); color: var(--rt-gold-bright);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--rt-font-ui); font-size: 11px; font-weight: 700;
}
.fwstep__in {
  flex: 1; border: 1px solid var(--rt-border-control); border-radius: var(--rt-radius-card);
  padding: 10px 14px; background: var(--rt-white);
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-ink); outline: none;
}
.fwstep__in:focus { border-color: var(--rt-gold); }
.fwstep__del {
  background: none; border: 0; cursor: pointer; padding: 0 4px;
  font-family: var(--rt-font-ui); font-size: 15px; color: var(--rt-muted);
}
.fwstep__del:hover { color: #A14232; }
.fwstep--read { align-items: flex-start; }
.fwstep__text {
  flex: 1; font-family: var(--rt-font-ui); font-size: 13.5px; line-height: 1.6; color: var(--rt-ink);
  padding: 4px 0;
}
.fwadd {
  display: block; width: calc(100% - 36px); box-sizing: border-box;
  border: 1.5px dashed rgba(27,67,50,.25); background: none; cursor: pointer;
  border-radius: var(--rt-radius-card); padding: 9px 0; margin: var(--rt-s2) 0 0 36px;
  font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 600; color: var(--rt-green-button);
}
.fwadd:hover { border-color: var(--rt-gold); background: rgba(201,162,39,.06); }
.fwdone { margin-top: 18px; }
.fwdone__head { display: flex; align-items: baseline; gap: 10px; }
.fwdone__label {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .14em; color: var(--rt-green-button);
}
.fwdone__help { font-family: var(--rt-font-ui); font-size: 11px; color: var(--rt-muted); }
.fwdone__in {
  width: 100%; box-sizing: border-box; margin-top: var(--rt-s2);
  border: 1px solid var(--rt-border-control); border-radius: var(--rt-radius-card);
  padding: 10px 14px; background: var(--rt-white);
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-ink); outline: none;
}
.fwdone__in:focus { border-color: var(--rt-gold); }
.fwdone__read { padding-top: var(--rt-s2); }
.fwreasons { margin: var(--rt-s2) 0; padding-left: var(--rt-s5); }
.fwreasons li { margin-bottom: 3px; }

.fwgrading { display: flex; align-items: center; gap: 10px; padding: var(--rt-s3) 0; }
/* [hidden] loses to a display declaration, so the attribute has to be honoured
   explicitly or the grading dots sit on the page before anything is submitted. */
.fwgrading[hidden] { display: none; }
.fwgrading__text { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary); }
.fwdot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--rt-gold-dark);
  animation: fwblink 1s infinite;
}
.fwdot:nth-child(2) { animation-delay: .2s; }
.fwdot:nth-child(3) { animation-delay: .4s; }
@keyframes fwblink { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }
@media (prefers-reduced-motion: reduce) { .fwdot { animation: none } }

.fwfoot { display: flex; align-items: center; gap: 14px; margin-top: 10px; flex-wrap: wrap; }
.fwfoot__note { font-family: var(--rt-font-ui); font-size: 11.5px; color: var(--rt-muted); }
.t-btn--dim { opacity: .35; }

@media (max-width: 900px) {
  .fwwrap { flex-direction: column; }
  .fwrail { width: 100%; flex: none; border-right: 0; border-bottom: 1px solid rgba(27,67,50,.1); }
}

/* the admin review and library screens, in the existing admin card shapes */
.fwreview__prompt {
  font-family: var(--rt-font-display); font-size: 16px; line-height: 1.5;
  color: var(--rt-ink); padding: var(--rt-s3) 0;
}
.fwreview__row {
  display: flex; gap: var(--rt-s5); align-items: flex-start;
  border-top: 1px solid var(--hair); padding: var(--rt-s4) 0;
}
.fwreview__main { flex: 1; min-width: 0; }
.fwreview__meta { display: flex; align-items: baseline; gap: var(--rt-s3); flex-wrap: wrap; }
.fwreview__pct {
  font-family: var(--rt-font-display); font-size: 20px; color: var(--rt-ink);
}
.fwreview__label {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 600;
  letter-spacing: .06em; color: var(--rt-secondary);
}
.fwreview__who { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-muted); }
.fwreview__steps {
  margin: var(--rt-s2) 0; padding-left: var(--rt-s5);
  font-family: var(--rt-font-ui); font-size: 13px; line-height: 1.7; color: var(--rt-body);
}
.fwreview__done, .fwreview__scores {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-body); margin-top: 4px;
}
.fwreview__scores { color: var(--rt-muted); font-size: 11.5px; }
.fwreview__acts { display: flex; flex-direction: column; gap: var(--rt-s2); align-items: flex-end; }
.fwreview__dismiss { background: none; border: 0; cursor: pointer; padding: 0; }

.fwlib__row { border-top: 1px solid var(--hair); padding: var(--rt-s4) 0; }
.fwlib__head { display: flex; align-items: baseline; gap: var(--rt-s3); flex-wrap: wrap; }
.fwlib__pill { color: var(--rt-gold-dark); background: var(--rt-tint-gold); border-color: var(--rt-border-gold); }
.fwlib__pct { font-family: var(--rt-font-display); font-size: 18px; color: var(--rt-ink); }
.fwlib__who { font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-muted); }
.fwlib__prompt {
  font-family: var(--rt-font-display); font-size: 15px; line-height: 1.5;
  color: var(--rt-ink); margin-top: 6px;
}
.fwlib__note {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary);
  margin-top: 6px; border-left: 3px solid var(--rt-border-gold); padding-left: var(--rt-s3);
}

/* the training home's entry into framework practice */
.fwentry {
  display: flex; align-items: center; gap: var(--rt-s5); flex-wrap: wrap;
  background: var(--rt-white); border: 1px solid var(--rt-border-card);
  border-left: 3px solid var(--rt-gold);
  border-radius: var(--rt-radius-card); padding: var(--rt-s4) var(--rt-s5);
  margin-bottom: var(--rt-s6);
}
.fwentry__main { flex: 1 1 320px; }
.fwentry__link {
  font-family: var(--rt-font-display); font-size: 18px; color: var(--rt-ink);
  text-decoration: none; border-bottom: 1px solid var(--rt-underline-gold);
}
.fwentry__link:hover { color: var(--rt-green-button); }
.fwentry__body {
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.6;
  color: var(--rt-secondary); margin: 6px 0 0;
}
.fwbuild > .alert { margin-bottom: var(--rt-s4); }

/* ── ROLODEX: the admin staff directory ─────────────────────────────────────

   Desktop-first by decision, not by omission. The page targets admin laptops at
   roughly 1024px and up; below that the table scrolls sideways inside
   .rolodex__scroll rather than restacking, because a stacked row destroys the
   column-by-column reading the directory exists for.

   The columns are flex ratios rather than a grid so the header row and the body
   rows can share one set of widths without a second declaration to keep in sync;
   .rolodex__c carries the ratio and each modifier sets it. */

/* [hidden] MUST ACTUALLY HIDE.
   Rows and menus below are display:flex, and an author-origin display outranks
   the user agent's [hidden]{display:none}. That exact mistake put every
   deactivated and archived roster row on screen in production (see CLAUDE.md),
   and this page hides rows on every keystroke. One rule, scoped to the page,
   ahead of every layout declaration that follows. */
.rolodex [hidden] { display: none !important; }

.rolodex__head { margin-bottom: var(--rt-s5); }
.rolodex__title {
  font-family: var(--rt-font-display); font-weight: 400; font-size: 28px;
  color: var(--rt-ink); margin: 0;
}
.rolodex__count {
  font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary);
  margin: 4px 0 0;
}

/* the smart search bar: the only search input on the page */
.rolodex__search {
  display: flex; align-items: center; gap: 14px;
  background: var(--rt-white); border: 1px solid var(--rt-border-control);
  border-radius: var(--rt-radius-panel); padding: 8px 8px 8px 20px;
  box-shadow: 0 4px 14px rgba(20, 55, 42, .06);
}
.rolodex__searchicon { flex-shrink: 0; }
.rolodex__input {
  flex: 1; border: none; outline: none; background: transparent;
  font-family: var(--rt-font-ui); font-size: 16px; color: var(--rt-body);
  padding: 10px 0;
}
.rolodex__searchbtn {
  padding: 11px 22px; background: var(--rt-green-button);
  border: 1.5px solid var(--rt-gold); border-radius: 4px;
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 11.5px;
  color: #fff; letter-spacing: .06em; cursor: pointer; white-space: nowrap;
}
.rolodex__searchbtn:hover { background: var(--rt-green-deep); }
.rolodex__note {
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-secondary);
  margin: 8px 0 0;
}
.rolodex__hint {
  font-family: var(--rt-font-ui); font-size: 11px; color: var(--rt-muted);
  margin: 6px 0 0;
}
/* Label kept for a screen reader without printing it: the placeholder carries
   the visible instruction. */
.rolodex__sronly {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* the filter row */
.rolodex__filters {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: var(--rt-s5);
}
.rolodex__kicker {
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 10.5px;
  color: var(--rt-gold-dark); letter-spacing: .14em;
}
.rolodex__facet { position: relative; }
.rolodex__pill {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--rt-white); border: 1px solid rgba(27, 67, 50, .2);
  color: var(--rt-green-button);
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 11.5px;
  padding: 8px 15px; border-radius: var(--rt-radius-pill);
  cursor: pointer; white-space: nowrap;
}
.rolodex__pill:hover { border-color: var(--rt-gold); }
/* active: the facet is carrying a value */
.rolodex__facet--on .rolodex__pill {
  background: var(--rt-green-deep); border-color: var(--rt-green-deep);
  color: var(--rt-offwhite);
}
.rolodex__facet--on .rolodex__chev { color: var(--rt-gold-bright); }
.rolodex__chev { flex-shrink: 0; transition: transform .18s ease; }
.rolodex__facet--open .rolodex__chev { transform: rotate(180deg); }

.rolodex__menu {
  position: absolute; top: calc(100% + 7px); left: 0; z-index: 40;
  min-width: 190px; background: var(--rt-white);
  border: 1px solid var(--rt-border-panel); border-radius: var(--rt-radius-dialog);
  box-shadow: 0 10px 28px rgba(20, 55, 42, .14); padding: 6px;
  animation: rolodexMenuIn .18s ease;
}
@keyframes rolodexMenuIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
.rolodex__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 8px 11px; border-radius: 6px; cursor: pointer; background: transparent;
}
.rolodex__opt:hover { background: rgba(201, 162, 39, .1); }
.rolodex__optlabel {
  font-family: var(--rt-font-ui); font-weight: 400; font-size: 12.5px;
  color: var(--rt-body); white-space: nowrap;
}
.rolodex__optmark { display: inline-flex; align-items: center; gap: 6px; }
/* the selected dot, present in the markup and revealed by the selected state, so
   nothing has to be created or destroyed to move the mark */
.rolodex__dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--rt-gold);
  flex-shrink: 0; visibility: hidden;
}
.rolodex__opt--on { background: rgba(201, 162, 39, .12); }
.rolodex__opt--on .rolodex__optlabel { font-weight: 700; color: var(--rt-green-deep); }
.rolodex__opt--on .rolodex__dot { visibility: visible; }
/* an expandable group: bolder, and its chevron turns while open */
.rolodex__opt--group .rolodex__optlabel { font-weight: 600; color: var(--rt-green-deep); }
.rolodex__kidchev { transition: transform .18s ease; }
.rolodex__opt--expanded .rolodex__kidchev { transform: rotate(180deg); }
.rolodex__opt--child { padding-left: 26px; }

.rolodex__clear {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-green-button);
  border-bottom: 1px solid var(--rt-underline-gold); margin-left: 4px;
}
.rolodex__clear:hover { color: var(--rt-gold-dark); }

/* the directory table */
.rolodex__scroll { overflow-x: auto; margin-top: var(--rt-s5); }
.rolodex__table {
  min-width: 1024px;
  background: var(--rt-white); border: 1px solid var(--rt-border-panel);
  border-radius: var(--rt-radius-card); overflow: hidden;
}
/* ONE GUTTER FOR THE WHOLE TABLE, declared once and applied to the header and
   the body rows alike so the columns keep lining up.

   It replaces a per-cell padding-right that had drifted to five different values
   (0, 0, 12, 10, 12, 12, 6). AVAILABILITY had the tightest of them at 10px, and
   it is the worst place for it: its day chips WRAP, so on a two-row cluster the
   last chip on a line can end flush against the cell edge, and the column after
   it is plain text that starts flush too. Ten pixels between a pill and a word
   read as no gap at all, and "Baylor" looked like the end of the availability
   cluster rather than the undergrad column.

   16px rather than the old dominant 12px. Twelve is enough between two chip
   clusters, where each pill carries 8 to 9px of its own padding and buys the
   eye some air on both sides. It is not enough between a pill and a bare word,
   which is the join this fixes. One value everywhere beats two rules nobody can
   keep straight.

   Safe against the widths: the cells are flex-basis 0 and grow, so a gap simply
   redistributes the row rather than widening it, and the column proportions and
   the table's 1024px min-width are untouched. */
.rolodex__table { --rt-rolodex-gutter: 16px; }

/* ⚠ 9a: THE HEAD TAKES THE NAV'S GREEN, so the table is anchored rather than
   floating on white. All portal colours, no new tokens. */
.rolodex__thead {
  display: flex; align-items: center; padding: 11px 20px;
  column-gap: var(--rt-rolodex-gutter);
  background: var(--rt-green-deep);
  border-bottom: 0;
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 9.5px;
  color: var(--rt-gold-bright); letter-spacing: .14em;
}
/* The rules through the availability column, in gold against the dark head. */
.rolodex__thead .rolodex__c--days {
  border-left-color: rgba(227, 181, 58, .28);
  border-right-color: rgba(227, 181, 58, .28);
}
/* ⚠ ZEBRA SCOPED WITH nth-of-type ON THE ROW CLASS, NOT nth-child. The head and
   the empty state are siblings inside .rolodex__table, and nth-child counts
   them: the stripes would start on the wrong row and the empty state would take
   one. It matters when the eye travels 1,300px across seven columns. */
.rolodex__row:nth-of-type(even) { background: var(--rt-cream); }
.rolodex__row:nth-of-type(even):hover { background: rgba(201, 162, 39, .05); }
/* ⚠ flex-start, NOT center, AND THIS IS HALF THE SPACING BUG. The six shorter
   columns were centred against the tallest cell, so a tall availability cell
   pushed every other column apart and the row filled with dead space. Rows ran
   about 340px; they now run 63 to 136. */
.rolodex__row {
  display: flex; align-items: flex-start; padding: 16px 20px;
  column-gap: var(--rt-rolodex-gutter);
  border-bottom: 1px solid var(--rt-rule);
}
.rolodex__row:hover { background: rgba(201, 162, 39, .05); }

.rolodex__c { min-width: 0; }
.rolodex__c--tutor { flex: 1.7; display: flex; align-items: center; gap: 12px; }
/* 1.8 rather than 1.6. The shared gutter gave TUTOR and CONTACT the 16px they
   never had -- those two were the only pair in the table with NO gutter at all --
   and that space has to come from somewhere. Taken evenly it came off CONTACT,
   the one column whose content is a fixed-length string that cannot be reflowed,
   so the ratio buys back exactly what the gutter cost it (152.9px before, 137.9
   at 1.6, 152.2 at 1.8). The other six columns hold text and chips that reflow,
   and give up 2 to 3px each. */
/* ⚠ 1.4. The four donors below give AVAILABILITY the width it needs, and the
   seven still sum to 10.5, so .rolodex__table's min-width: 1024px and its
   sideways scroll below that are untouched. That sum is load bearing. */
.rolodex__c--contact { flex: 1.4; }
.rolodex__c--subjects { flex: 1.5; display: flex; gap: 5px; flex-wrap: wrap; }
/* AVAILABILITY, AS HOURS. A column of lines rather than a wrap of gold pills:
   the pills could not hold hours at all. Its width is the constraint the whole
   notation was chosen against -- "3-8p" fits here and "3:00 PM to 8:00 PM"
   never will -- so the flex ratio is the one number in this block that is load
   bearing. 1.35 rather than 1.2, taken from INTERESTS, which is the only
   neighbour holding free text that reflows to any width. */
.rolodex__c--days {
  /* ⚠ 2.6, UP FROM 1.35, AND THIS IS THE OTHER HALF OF THE BUG. The
     availability column was the second narrowest of seven on the page whose job
     is scheduling. A prior pass hit this and removed the gold day pills because
     the cell "is about 165px wide and they could not hold hours at all"; it
     compacted the text instead of widening the column. This widens the column.

     ⚠ AND IT IS RULED ON BOTH SIDES, so the column the page exists for reads as
     the centre of the row rather than as the fourth thing along. */
  flex: 2.6; display: flex; flex-direction: column; gap: 4px;
  padding: 0 14px;
  border-left: 1px solid rgba(27, 67, 50, .1);
  border-right: 1px solid rgba(27, 67, 50, .1);
  font-family: var(--rt-font-ui);
}
.rolodex__hourline { display: flex; gap: 8px; align-items: baseline; }
/* A fixed days column so the times line up down the cell. 62px holds
   "Mon - Thu", the longest label the grouping can produce. */
.rolodex__hourdays {
  /* 96px, up from 62. The cell has the width now, and a wider label column is
     what makes every line start at the same x rather than at whatever the
     grouping produced. */
  flex-shrink: 0; width: 96px; font-size: 12px; font-weight: 700;
  line-height: 1.5; color: var(--rt-ink);
}
.rolodex__hourtimes {
  flex: 1; min-width: 0; font-size: 12px; line-height: 1.5; color: var(--rt-secondary);
}
/* "No hours set" and "Not available" are not hours, so they are not set like
   hours: gold, which is this palette's colour for something needing attention,
   and never red. The row carries a 4px gold left border for the first of them,
   the same rule the kill banners use. */
.rolodex__nohours { font-size: 12px; font-weight: 700; color: var(--gold-dark); }
/* A subheading inside a form, naming the person the next fields belong to.
   design_handoff_presession 1d: grouped by person rather than by kind. */
.lbl--group {
  margin-top: 22px; text-transform: none;
  font-family: var(--serif); font-size: 15px; letter-spacing: normal;
  color: var(--ink);
}
/* The one word that says a field cannot be left blank, in the gold this
   product uses for a state worth noticing. */
.lbl__req {
  margin-left: 6px; font-size: 9px; font-weight: 700; letter-spacing: .08em;
  color: var(--gold-ink);
}

.rolodex__hoursnote { font-size: 11.5px; line-height: 1.5; color: var(--rt-secondary); }
/* The one action on the Zoom flag, in the same gold as the fact above it. */
.rolodex__zoomask {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 11.5px; font-weight: 600; color: var(--green-btn);
}
/* THE TUTOR'S ZONE, above the Updated stamp. Same gold ink as the family read
   on the roster, and the same `time` suffix, for the same reason: "Central"
   beside a list of hours could be read as a place. Not red, not a badge: a
   non-default zone is a fact, not a fault. */
.rolodex__hourzone {
  margin-top: 4px; font-size: 11.5px; font-weight: 700; color: var(--gold-ink);
}
.rolodex__hoursstamp { margin-top: 4px; font-size: 11px; color: var(--rt-secondary); }
/* The border is drawn INSIDE the row's own padding rather than added to it, so
   a flagged row keeps the same height and the same left edge as every other
   one and the table does not step in and out down the page. */
.rolodex__row--nohours { box-shadow: inset 4px 0 0 var(--gold); }
.rolodex__c--undergrad {
  flex: 1; font-family: var(--rt-font-ui); font-size: 12.5px;
  color: var(--rt-body);
}
/* WEEKLY EARNINGS. Its own column between UNDERGRAD and INTERESTS. Narrow: the
   content is a short amount and an hours figure under it, never a sentence. */
.rolodex__c--earnings {
  flex: 1; font-family: var(--rt-font-ui);
}
/* ⚠ 9a's ONE TYPE CHANGE: a 16px Libre Caslon figure, so each row has one
   number the eye lands on. */
.rolodex__earn {
  display: block; font-family: var(--rt-font-display);
  font-size: 16px; font-weight: 400; color: var(--rt-green-deep);
  white-space: nowrap;
}
/* "No rate set" is not an amount, so it is not set like one: lighter, italic,
   and no numeral treatment. It reads as an unfinished record rather than as a
   figure of zero, which is the whole reason it is not $0. */
.rolodex__earn--norate {
  font-weight: 400; font-style: italic; font-size: 12px; color: var(--rt-secondary);
}
.rolodex__earnhours {
  display: block; font-size: 11px; color: var(--rt-secondary); margin-top: 2px;
  white-space: nowrap;
}

/* 1.45 rather than 1.6. AVAILABILITY took 0.15 when it stopped being pills and
   became lines of hours, and it came from here: INTERESTS is the only neighbour
   holding free text that reflows to any width, so it is the column that can
   give it up without losing a reading. */
.rolodex__c--interests {
  flex: 1.3; font-family: var(--rt-font-ui); font-size: 12px; line-height: 1.5;
  color: var(--rt-secondary);
}
/* THE HEADER ROW'S CELLS ARE LABELS, NOT CONTENT.
   They keep the column ratio and take EVERYTHING else from .rolodex__thead, so
   all seven headings are one style: 9.5px, 600, --rt-muted, tracked out.

   The column classes are shared by the header cell and the body cell, and most
   of them set layout only, putting the body's typography on inner elements that
   exist in a row and not in the header. Two did not: --undergrad set 12.5px in
   --rt-body and --interests set 12px in --rt-secondary, directly on the shared
   class. Those two headings therefore rendered darker, heavier and larger than
   the five beside them, and the defect was invisible until a sixth column landed
   between them and read correctly.

   RESET RATHER THAN UNPICKED, and deliberately so. Moving those two declarations
   onto row-scoped selectors would fix today's two and leave the next column free
   to reintroduce it. This says the header takes the header row's type whatever a
   column class asks for, which is the rule that was meant all along -- the
   display reset below it was already here for exactly the same reason, keeping
   the body cells' flex layout out of the header. */
.rolodex__thead .rolodex__c {
  display: block;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

.rolodex__avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--rt-green-deep); color: var(--rt-gold-bright);
  font-family: var(--rt-font-ui); font-weight: 700; font-size: 12px;
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
}
/* a tutor still in training: the same avatar, tinted rather than solid. This is
   the only place status is expressed, and it is display only. */
.rolodex__avatar--training {
  background: var(--rt-tint-gold); color: var(--rt-gold-dark);
}
.rolodex__who { min-width: 0; }
.rolodex__name {
  display: block; font-family: var(--rt-font-ui); font-weight: 600;
  font-size: 14px; color: var(--rt-ink);
}
.rolodex__meta {
  display: block; font-family: var(--rt-font-ui); font-size: 11px;
  color: var(--rt-muted); margin-top: 2px;
}
.rolodex__phone {
  display: block; font-family: var(--rt-font-ui); font-size: 12.5px;
  color: var(--rt-body);
}
/* Long addresses wrap inside the cell instead of running under SUBJECTS.
   WHERE they wrap is the point. overflow-wrap:anywhere breaks at any character,
   which is what produced "alison_macleod1@baylor.ed / u": a break two letters
   into the TLD, which reads as a typo rather than as a wrap. It also feeds the
   break opportunities into intrinsic sizing, so the column shrank more readily
   than it needed to.
   break-word is the fallback only, for a local part with no break point that
   genuinely cannot fit. The ordinary break now happens at the @, offered as a
   <wbr> in the markup, so an address that does not fit becomes
   "alison_macleod1@" over "baylor.edu".
   The old padding-right is gone: the row's column-gap is the gutter now, and a
   second one inside the cell only narrowed the address further. */
.rolodex__email {
  display: block; font-family: var(--rt-font-ui); font-size: 11.5px;
  color: var(--rt-green-button); margin-top: 2px;
  overflow-wrap: break-word;
}
.rolodex__chip {
  padding: 3.5px 9px; border-radius: var(--rt-radius-pill);
  background: rgba(27, 67, 50, .06); border: 1px solid var(--rt-border-panel);
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 10px;
  color: var(--rt-green-button); white-space: nowrap;
}
/* ⚠ TWO CLASSES, ONE TREATMENT, AND THAT IS DELIBERATE. --day is the gold day
   pill the availability cell no longer uses; --none is the NONE ON RECORD chip
   in the SUBJECTS cell. design_handoff_rolodex says to use the day chip's gold
   treatment, which is what this does, and a separate class is what lets the
   standing guard "no day pills in the availability cell" go on meaning exactly
   that instead of being loosened to accommodate a chip in another column. */
.rolodex__chip--day,
.rolodex__chip--none {
  background: rgba(201, 162, 39, .14); border-color: var(--rt-border-gold);
  font-size: 9.5px; color: #7A6218; padding: 3.5px 8px;
}

.rolodex__empty { padding: 44px 20px; text-align: center; }
.rolodex__emptytitle {
  font-family: var(--rt-font-display); font-size: 18px; color: var(--rt-ink); margin: 0;
}
.rolodex__emptytitle em { color: var(--rt-gold-dark); font-style: normal; }
.rolodex__emptyhint {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-muted);
  margin: 6px 0 0;
}

/* The Add/Edit tutor forms' weekday picker (partials/tutor-directory-fields).
   Seven checkboxes rather than a text field, because what is typed here has to
   equal a canonical weekday name for the directory's filter to match it. */
.daypick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.daypick__day {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 11px; border: 1px solid var(--field);
  background: var(--field-bg); border-radius: var(--rt-radius-pill);
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--body);
  cursor: pointer; user-select: none;
}
.daypick__day:hover { border-color: var(--gold); }
.daypick__day input { margin: 0; cursor: pointer; }
/* :has is the only way to style the label from its own checked input without a
   script. Where it is unsupported the pill simply stays untinted; the checkbox
   inside it still shows the state, so nothing becomes unreadable. */
.daypick__day:has(input:checked) {
  background: rgba(201, 162, 39, .16); border-color: var(--rt-border-gold);
  color: var(--green-deep); font-weight: 600;
}

/* Section spacing as classes rather than inline styles: this template is one of
   the ones a test holds to that rule. */
.h2--spaced { margin-top: 30px; }
.empty--spaced { margin-top: 18px; }

/* ── SUGGESTED MATCHES, on the TutorBird screen ───────────────────────────
   One card per student the calendar's own label points at. A suggestion, not a
   link: nothing here has matched anything until the button is pressed. */
.sugg {
  border: 1px solid var(--line); border-radius: 6px;
  padding: 14px 16px; margin-top: 10px; background: var(--off-white);
}
.sugg__head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.sugg__feed { font-size: 14px; color: var(--muted); }
.sugg__arrow { color: var(--faint); }
.sugg__student { font-size: 15px; font-weight: 700; color: var(--ink); }
.sugg__meta { margin: 6px 0 0; font-size: 12px; color: var(--faint); }
/* Gold, not red: this is something to look at, not something that went wrong,
   and the portal has no error colour anyway. */
.sugg--ambiguous { border-color: var(--rt-border-gold); }
.sugg__warn {
  margin: 8px 0 0; font-size: 12.5px; color: var(--gold-dark);
  border-left: 3px solid var(--gold); padding-left: 10px;
}
.sugg__lessons {
  list-style: none; margin: 8px 0 12px; padding: 0;
  font-size: 12.5px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 4px 14px;
}
@media (max-width: 760px), (max-height: 499px) {
  .sugg__lessons { flex-direction: column; gap: 2px; }
  .sugg .btn, .sugg .sel { width: 100%; margin-top: 8px; }
}

/* ── THE DAY PILLS ON A PHONE ─────────────────────────────────────────────
   WRITTEN HERE, IMMEDIATELY AFTER THE BASE RULES, AND THAT IS THE POINT. The
   first version of this lived in the page's own 760px block two and a half
   thousand lines EARLIER in the file. At equal specificity the later rule wins,
   so `.daypick { display: flex }` below it quietly beat `display: grid`, and
   `padding: 7px 11px` beat the tightened padding. Only the declarations the base
   rule happened not to mention took effect. That is the second time a mobile
   override has been written where a later rule could outrank it; the placement
   is the fix, not a stronger selector.

   A GRID, NOT A FLEX ROW, because flex line-breaking and shrinking are exactly
   what could not be relied on. Refusing to shrink fixed it in Chrome and changed
   nothing on an iPhone, where the labels still read Mo, Tu, W.

   THE CHECKBOX IS THE VARIABLE. Its intrinsic size is a user-agent decision and
   WebKit draws it larger than Blink, so a pill sized to "checkbox plus label" is
   a different width in each engine. Every previous attempt was arithmetic
   performed on a number the engine was free to change. Pinning it makes the
   pill's width the same everywhere, which is what makes this deterministic
   rather than merely wider.

   Four columns of max-content, so no track can be narrower than the pill it
   holds and no label is ever asked to give anything up. */
@media (max-width: 760px), (max-height: 499px) {
  .daypick {
    display: grid;
    grid-template-columns: repeat(4, max-content);
    justify-content: start;
    gap: 6px;
  }
  /* 7px sides rather than the desktop's 11: four columns of max-content measure
     275px against the 288px the card gives them at a 390px viewport. At 11 they
     came to 291 and overflowed. */
  .daypick__day {
    min-height: 44px; padding: 7px 7px; justify-content: center; flex: 0 0 auto;
  }
  .daypick__day input { width: 16px; height: 16px; flex: 0 0 16px; }
  .daypick__day span { white-space: nowrap; }
}
/* Narrower than any current iPhone: three columns rather than four, so it
   degrades by wrapping sooner instead of by clipping. */
@media (max-width: 344px) {
  .daypick { grid-template-columns: repeat(3, max-content); }
}

/* ── the second parent block (partials/second-parent.ejs) ───────────────────
   The "+ Parent" link and the gold-outlined panel it opens, on Add student and
   Edit student. Values from the handoff mock. */

/* [hidden] MUST ACTUALLY HIDE. The panel and the link below both carry a
   display, and an author-origin display outranks the user agent's
   [hidden]{display:none}: that is exactly how every deactivated roster row
   ended up visible in production (see CLAUDE.md). Scoped to this block and
   placed ahead of the declarations it protects. */
/* ── AN OPTIONAL EXTRA BLOCK: + Parent, + Tutor ─────────────────────────────
   One set of rules, two users. The second parent block came first and these
   values are its, unchanged; the tutor rows on the same forms now render from
   the same classes rather than a second copy of the same numbers.
   Named for what it IS rather than for the first thing that used it, so the
   third one does not arrive as .p2__panel. */
.extra [hidden] { display: none !important; }

.extra__add {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 10px; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 11.5px;
  color: var(--rt-gold-dark); letter-spacing: .04em;
}
.extra__add:hover { color: #7A6218; }
.extra__plus { font-size: 14px; line-height: 1; }

.extra__panel {
  margin-top: 14px; padding: 14px 14px 16px;
  border: 1px solid rgba(201, 162, 39, .4); border-radius: var(--rt-radius-card);
  background: rgba(201, 162, 39, .05);
}
.extra__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 10px;
}
.extra__kicker {
  font-family: var(--rt-font-ui); font-weight: 600; font-size: 10px;
  color: var(--rt-gold-dark); letter-spacing: .12em;
}
.extra__remove {
  padding: 0; border: 0; background: none; cursor: pointer; white-space: nowrap;
  font-family: var(--rt-font-ui); font-weight: 400; font-size: 11px;
  color: var(--rt-secondary); border-bottom: 1px solid rgba(27, 67, 50, .2);
}
.extra__remove:hover { color: var(--rt-green-button); }
/* The survey's skip panel reuses .extra__panel. That block carries a top margin
   for its place in the student forms; here it follows the question's own
   spacing. Declared beside the class it modifies, and AFTER the [hidden] guard
   above, so the cascade cannot undo the guard. */
.extra__panel--flat { margin-top: 11px; padding: 14px 16px; }

/* ── the roster caseload pill and its popover ───────────────────────────────
   On the tutors list: how many students a tutor carries, and who they are.

   The pill is sized to match .norate exactly, because the two sit side by side
   on the same line and a badge a pixel taller than its neighbour reads as a
   mistake. Everything below that both must share is duplicated deliberately
   rather than factored into a shared class: .norate is load-bearing on a screen
   this change was told not to touch, and a shared base would let a future edit
   to one silently resize the other. */

/* [hidden] MUST ACTUALLY HIDE. The card below is display:block from its own
   rule, and an author-origin display outranks the user agent's
   [hidden]{display:none} — the mistake that put every deactivated roster row on
   screen in production (see CLAUDE.md). */
.spop [hidden] { display: none !important; }

.spop { position: relative; display: inline-flex; vertical-align: baseline; }

/* ⚠ THE PILL CARRIES NO LEFT MARGIN, AND THAT IS DELIBERATE.
   It used to carry `margin-left: 6px` to separate it from the name INLINE.
   Every container that holds this pill is a flex row with a gap of its own, so
   the margin was never what did the separating; it was a second copy of it.
   What it DID do is indent the pill on any line it STARTED, and on the admin
   grid the name row is allowed to wrap, so a tutor with an ADMIN badge beside
   the name pushes the students pill onto a line of its own, where the 6px read
   as a misalignment against the name above it.

   Same defect and same fix the phone block already records at length: "the name
   started at x=71 and the pill at x=77: six pixels of indent that read as a
   mistake". That fixed the stacked phone case by zeroing the margin there; this
   removes it at the source, so every card is covered rather than the one width
   somebody happened to look at.

   ⚠ AND NOTHING IN THIS RULE MAY CONTAIN A BRACE, INCLUDING IN A COMMENT.
   test/roster-popover slices this rule from its opening brace to the first `}`
   it finds, so a selector written inside a comment here ends the slice early
   and the size assertions fail against a truncated block. That is exactly how
   this comment came to be outside the rule rather than in it. */
.spop__pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* ⚠ NO margin-left, AND ITS ABSENCE IS THE FIX. See the note above. */
  padding: 3px 9px;
  border-radius: 100px;
  background: rgba(201, 162, 39, .16);
  border: 1px solid rgba(168, 132, 42, .45);
  font-family: var(--sans);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--gold-dark);
  letter-spacing: .06em;
  white-space: nowrap;
  cursor: pointer;
}
.spop__subject {
  margin-left: 6px; padding: 1px 7px; border-radius: 100px;
  background: rgba(27,67,50,.06); border: 1px solid var(--line);
  font-size: 10px; font-weight: 600; letter-spacing: .04em; color: var(--muted);
  white-space: nowrap;
}
.spop__pill:hover { background: rgba(201, 162, 39, .28); }
.spop__chev { font-size: 8px; line-height: 1; }

.spop__card {
  position: absolute;
  top: calc(100% + 7px);
  left: 0;
  /* ⚠ ABOVE THE ROWS, AND UNDER THE STICKY HEADER. This was 60 against .shead's
     30, so the popover drew ON TOP of the Admin tools header unit and covered
     the heading and the chip rail as the page scrolled. Every other element on
     this page goes under that bar; a popover is not the exception.
   *
   * THE TWO COMPETE DIRECTLY, which is why the number alone decides it: .spop is
   * position:relative with NO z-index, and position without z-index creates no
   * stacking context, so this value is compared against .shead's in the root
   * rather than sealed inside the roster row.
   *
   * 20 keeps its original job, above the rows below it and above the row's own
   * hover state, and loses the one contest it should never have won. */
  z-index: 20;
  min-width: 210px;
  /* Bottom padding moved onto .spop__list, so it sits INSIDE the scroll area
     and the last student clears the card's edge when scrolled to the end. */
  padding: 10px 0 0;
  background: #fff;
  border: 1px solid rgba(27, 67, 50, .14);
  border-radius: 8px;
  box-shadow: 0 10px 28px rgba(20, 55, 42, .16);
  /* The trigger's letter-spacing and 9.5px would otherwise be inherited by
     every line inside the card. */
  letter-spacing: normal;
  text-align: left;
  cursor: default;
}
.spop__head {
  padding: 0 14px 8px;
  border-bottom: 1px solid rgba(27, 67, 50, .1);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--gold-dark);
  letter-spacing: .12em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* THE SCROLL AREA. Found at real scale: with 21 students the card clipped at the
   bottom with no scroll, and every student past the fold was unreachable. The
   design handoff never specified overflow for this card.

   min(340px, 50vh) IS the viewport clamp, and it is why no repositioning
   machinery is needed. 340px is the comfortable reading height; 50vh takes over
   on a short viewport, where a fixed 340px card opening below a row near the
   bottom of the page is exactly what clips. Half the viewport always leaves the
   card somewhere to be.

   Not max-height on .spop__card: the head must stay out of the scroll, so the
   bound belongs on the list alone. */
.spop__list {
  max-height: min(340px, 50vh);
  overflow-y: auto;
  /* A scroll that reaches the end must not then scroll the page behind it: the
     roster is long, and chaining would drag the reader away from the row they
     opened. */
  overscroll-behavior: contain;
  padding-bottom: 10px;
}
.spop__item {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 8px 14px 0;
}
.spop__student { font-size: 12.5px; font-weight: 600; line-height: 1.4; color: var(--ink); }
/* Parent 1 only. Long addresses wrap inside the card rather than widening it. */
.spop__parent {
  font-size: 10.5px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--faint);
  overflow-wrap: anywhere;
}
.spop__none {
  padding: 10px 14px 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--faint);
}

/* ── smooth admin actions: the toast and the confirm dialog ─────────────────
   Feedback for add tutor, add student, archive student and deactivate tutor.
   These four no longer reload, so the confirmation has to arrive without one. */

.toasts {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  /* Above the page and above the roster popovers, below the modal. */
  z-index: 200;
  display: flex;
  justify-content: center;
  /* The strip spans the viewport so the pill can centre in it, but only the
     pill itself may be clicked: an invisible full-width bar across the bottom
     of the page would swallow clicks meant for whatever sits under it. */
  width: 100%;
  pointer-events: none;
}
.toast {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  max-width: min(560px, calc(100vw - 32px));
  padding: 11px 20px;
  background: var(--green-deep);
  border: 1px solid rgba(201, 162, 39, .55);
  border-radius: 100px;
  box-shadow: 0 14px 36px rgba(20, 55, 42, .3);
  pointer-events: auto;
}
.toast__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--gold);
  color: var(--green-deep);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
}
.toast__msg {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--off-white);
}
.toast__undo {
  flex-shrink: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  color: var(--gold-light);
  border-bottom: 1px solid rgba(227, 181, 58, .5);
}
.toast__undo:hover { border-bottom-color: var(--gold-light); }
.toast__x {
  flex-shrink: 0;
  padding: 0 0 0 2px;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1;
  color: rgba(255, 255, 255, .55);
}
.toast__x:hover { color: #fff; }
/* A failure keeps the same pill and changes the mark and the message colour,
   so it reads as the same channel rather than a different component. */
.toast--error .toast__icon { background: var(--gold-light); }
.toast--error .toast__msg { color: var(--gold-light); }

/* the confirm dialog for archive and deactivate */
.sdlg {
  padding: 0;
  border: 0;
  background: none;
  /* The UA centres a top-layer dialog; these keep it centred in both axes
     without the margin:auto quirks older engines have. */
  max-width: none;
  max-height: none;
}
.sdlg::backdrop { background: rgba(20, 55, 42, .38); }
.sdlg__card {
  width: 400px;
  max-width: calc(100vw - 32px);
  padding: 26px 28px 24px;
  background: var(--off-white);
  border-radius: 8px;
  box-shadow: 0 24px 60px rgba(20, 55, 42, .35);
}
.sdlg__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 400;
  font-size: 19px;
  color: var(--ink);
}
.sdlg__body {
  margin: 10px 0 0;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.65;
  color: var(--muted);
}
.sdlg__acts {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 22px;
}
.sdlg__cancel,
.sdlg__go {
  padding: 10px 18px;
  border-radius: 4px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
}
.sdlg__cancel {
  background: none;
  border: 1px solid rgba(27, 67, 50, .25);
  color: var(--green-btn);
}
.sdlg__cancel:hover { border-color: var(--green-btn); }
.sdlg__go {
  background: var(--green-btn);
  border: 1.5px solid var(--gold);
  color: #fff;
}
.sdlg__go:hover { background: var(--green-deep); }

/* A row added without a reload, tinted so it is findable at the bottom of a
   list that pages at ten. The tint is not persisted: it is a property of THIS
   render, and the next page load shows an ordinary row. */
.roster__row--new { background: rgba(201, 162, 39, .05); }

/* ── session requests ───────────────────────────────────────────────────────
   A tutor whose lesson the ingest missed asks for a session; an admin approving
   it is what creates one. The tutor's control is on the dashboard, the queue of
   asks is in Admin tools under Create session. */

/* the admin's queue of asks */
.sreq { margin-top: 22px; }
.sreq__row {
  display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap;
  padding: 12px 0; border-top: 1px solid var(--hair);
}
.sreq__main { flex: 1 1 260px; min-width: 0; }
.sreq__who { font-family: var(--sans); font-weight: 600; font-size: 13.5px; color: var(--ink); }
.sreq__meta {
  margin-top: 3px;
  font-family: var(--sans); font-size: 11.5px; line-height: 1.5; color: var(--faint);
}
.sreq__note { color: var(--muted); font-style: italic; }
.sreq__acts { display: flex; align-items: center; gap: 10px; padding-top: 2px; }
.sreq__go {
  padding: 8px 16px; cursor: pointer;
  background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px;
  font-family: var(--sans); font-weight: 600; font-size: 11px;
  letter-spacing: .06em; color: #fff;
}
.sreq__go:hover { background: var(--green-deep); }
.sreq__no {
  padding: 0; border: 0; background: none; cursor: pointer; white-space: nowrap;
  font-family: var(--sans); font-size: 11.5px; color: var(--muted);
  border-bottom: 1px solid rgba(27, 67, 50, .2);
}
.sreq__no:hover { color: var(--danger); border-color: var(--danger); }

/* The tutor dashboard's student-list pager. Rendered only when there is more
   than one page, so these rules describe a control most tutors never see.

   Its own modifier rather than reusing .page--sent .pager: that one is scoped to
   the sent page and this list sits above the escape hatch, which needs the
   spacing below it to stay the hatch's own. */
/* The trust table's pager shares every rule below with the dashboard's: same
   control, same three parts, and splitting them would let the two drift. Only
   the top margin differs, since this one closes a card rather than sitting above
   the escape hatch. */
.pager--trust { margin-top: 16px; border-top: 1px solid var(--rt-rule); padding-top: 14px; }
.pager--dash, .pager--trust {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  margin-top: 14px;
}
/* The two labels stack on the centre so the range reads as a detail of the page
   number rather than as a third control competing with Prev and Next. */
.pager--dash .pager__mid, .pager--trust .pager__mid {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  text-align: center;
}
.pager--dash .pager__page, .pager--trust .pager__page {
  font-family: var(--sans); font-size: 12px; font-weight: 600; color: var(--ink);
}
.pager--dash .pager__where, .pager--trust .pager__where {
  font-family: var(--sans); font-size: 11.5px; color: var(--faint);
}
.pager--dash .pager__link, .pager--trust .pager__link {
  font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: var(--green-btn);
  border-bottom: 1px solid rgba(201,162,39,.55); padding-bottom: 1px;
}
.pager--dash .pager__link:hover, .pager--trust .pager__link:hover { color: var(--gold-dark); border-color: var(--gold); }
/* An end of the list. Rendered as a span rather than a disabled link, so it is
   neither clickable nor focusable, and it holds its width so the row does not
   reflow as the reader pages through it. */
.pager--dash .pager__link--off, .pager--trust .pager__link--off {
  color: var(--faint); border-color: transparent; pointer-events: none;
}

/* The Session tags form: one row of controls above the list of tags, sharing the
   survey-requests panel's frame so the two read as the same kind of thing. */
.tagform { margin: 10px 0 4px; }
.tagform__row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tagform__row .selwrap { flex: 1 1 160px; min-width: 140px; }
.tagform__date { flex: 0 1 165px; }
/* The charge-rate input in a tagform row. Narrower than a date because a rate is
   three or five characters, and wrapped in the .money block the pay rate already
   uses so the $ sits inside the field's frame. */
.tagform__row .money { flex: 0 1 130px; }
.tagform__rate { width: 100%; }
@media (max-width: 720px) {
  /* Stacked on a phone: three controls and a button will not sit on one line at
     that width without each becoming too narrow to read. */
  .tagform__row { flex-direction: column; align-items: stretch; gap: 10px; }

  /* FLEX-BASIS IS THE HEIGHT ONCE THE ROW IS A COLUMN, and that is the whole
     bug. The row above gives .selwrap `flex: 1 1 160px` and the date input
     `flex: 0 1 165px` to size them ACROSS a horizontal row; turning the row into
     a column reinterprets both as 160px and 165px TALL, which is why each
     control rendered as a tall empty box with its value floating in the middle.
     Nothing here is a new layout: it is the same stack with the basis returned
     to the content's own height. */
  /* Written at the SAME specificity as the rules being undone, and after them.
     `.tagform__row > *` is one class and loses to `.tagform__row .selwrap`,
     which is two, whatever the order: specificity beats position. */
  .tagform__row .selwrap { flex: 0 0 auto; }
  .tagform__date { flex: 0 0 auto; width: 100%; }
  /* The button is a target rather than a line of text, so it keeps 44px while
     everything else keeps the height of an input. */
  .tagform__row .sreq__go { min-height: 44px; width: 100%; }
  /* THE SAME BASIS-BECOMES-HEIGHT TRAP the comment above records, and the money
     block walks straight into it: `flex: 0 1 130px` in a column is 130px TALL,
     which is a rate floating in the middle of an empty box. Returned to the
     content's own height, at the same specificity, after the rule it undoes. */
  .tagform__row .money { flex: 0 0 auto; width: 100%; }
}

/* The demoted escape hatch at the foot of the student list.
   A text link, not a button: the primary path is TutorBird attendance, and this
   must not read as an equal second way to get a survey. */
/* EQUAL SPACE ABOVE AND BELOW THIS LINE. The space above is this margin; the
   space below is the notifications card's margin-top, the same token.

   MARGINS RATHER THAN A FIXED OFFSET, so it holds as either neighbour changes
   height. A survey list of one row and a survey list of twelve leave the line
   in the same place relative to both. */
.reqsurvey { margin-top: var(--dash-foot-gap); }
.reqsurvey > summary { list-style: none; cursor: pointer; display: inline; }
.reqsurvey > summary::-webkit-details-marker { display: none; }
.reqsurvey__link {
  font-family: var(--sans); font-size: 12.5px; color: var(--muted);
  border-bottom: 1px solid rgba(27, 67, 50, .22);
}
.reqsurvey__link:hover { color: var(--green-btn); border-color: var(--green-btn); }
.reqsurvey__form {
  margin-top: 12px; padding: 16px; max-width: 460px;
  background: var(--off-white);
  border: 1px solid rgba(201, 162, 39, .4); border-radius: var(--rt-radius-card);
}
.reqsurvey__label {
  display: block; margin-bottom: 6px;
  font-family: var(--sans); font-weight: 600; font-size: 10px;
  letter-spacing: .06em; color: var(--green-deep);
}
.reqsurvey__row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.reqsurvey__sel {
  flex: 1 1 200px; min-width: 0; padding: 9px 10px;
  border: 1px solid var(--field); border-radius: 4px; background: var(--field-bg);
  font-family: var(--sans); font-size: 16px; color: var(--body);
}
.reqsurvey__go {
  padding: 10px 16px; cursor: pointer; white-space: nowrap;
  background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px;
  font-family: var(--sans); font-weight: 600; font-size: 11px;
  letter-spacing: .06em; color: #fff;
}
.reqsurvey__go:hover { background: var(--green-deep); }
.reqsurvey__note {
  margin: 10px 0 0;
  font-family: var(--sans); font-size: 11.5px; line-height: 1.5; color: var(--faint);
}

/* ══ WORK SUBMISSION: student screens ═══════════════════════════════════════
   Full-bleed dark, phone-first, no portal chrome. These render to a signed-out
   minor holding a phone, so they carry their own layout rather than the 860px
   cream shell.

   Values are from the design bundle's token table, which is the portal's own
   token set, so everything below resolves to the variables at the top of this
   file rather than to invented hexes.

   NO RED ANYWHERE. The portal has no error colour; the upload-failure state
   uses gold like every other attention state.

   Touch targets are 44px minimum throughout, per the design and per the fact
   that the only device this ever renders on is a phone. */

/* ⚠ A GREEN-GROUNDED SHELL ON A KEYBOARD SURFACE, which is the exact shape the
   dead green block comes from: this page takes a typed note, and its own ground
   shows in any space the shell fails to cover. svh does not move for the
   keyboard, so there is nothing to go stale here. See the note on
   body:has(> .tabbar) for the full account. */
.work {
  margin: 0;
  background: var(--green-deep);
  color: var(--off-white);
  min-height: 100vh;
  min-height: 100dvh;           /* iOS Safari: dvh excludes the URL bar */
  min-height: 100svh;           /* and svh does not move for the keyboard */
  -webkit-font-smoothing: antialiased;
}
.work__shell {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: 520px;             /* a phone screen, capped for desktop preview */
  margin: 0 auto;
}
.work__stage { display: flex; flex-direction: column; flex: 1; }
.work__stage[hidden] { display: none; }

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

/* ── S2 capture ──────────────────────────────────────────────────────── */
.work__head {
  padding: max(70px, var(--sat)) 22px 16px;
  display: flex; flex-direction: column; gap: 7px;
}
.work__headrow { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.work__eyebrow {
  font-size: 10px; font-weight: 600; letter-spacing: .14em;
  color: var(--gold-light); text-transform: uppercase;
}
.work__meta { font-size: 11.5px; color: rgba(255,255,255,.6); text-align: right; }
.work__headline {
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 22px; font-weight: 400; line-height: 1.3; color: var(--off-white); margin: 0;
}

/* THE CAPTURE STAGE IS CAPPED AT THE VIEWPORT, and only the capture stage.
   :has(.work__viewfinder) picks S2, because S3 has no viewfinder; the review
   screen's layout is not touched by this task and must not be.

   WHY IT IS NEEDED. .work__shell is min-height: 100dvh, so nothing above the
   viewfinder was ever under pressure to shrink: the column simply grew and the
   page scrolled. That is invisible while the paper is the thing absorbing the
   squeeze, which is exactly what it was doing by stretching. Lock the paper to
   3:4 without capping the column and the squeeze has to go somewhere else, and
   where it went was the shutter falling below the fold on a reopened link with
   pages already sent. Capping the stage puts the pressure back on the paper,
   which is the only decorative thing here and the right thing to shrink. */
.work__stage:has(.work__viewfinder) { max-height: 100dvh; }
/* Everything that is not the paper holds its size, so the squeeze lands on the
   decoration rather than on the pages a student is trying to recognise. */
.work__head, .work__already { flex-shrink: 0; }

.work__viewfinder {
  flex: 1;
  /* min-height: 0 is the half of the stretch fix nobody remembers. A flex child
     will not shrink below its content without it, so the paper could push this
     taller than the space available and the overflow: hidden would just crop. */
  min-height: 0;
  position: relative; overflow: hidden; background: #0F2A20;
  /* COLUMN, so the sheet's HEIGHT is the flex main axis and can therefore
     shrink. max-height: 100% cannot do this job: it resolves against a height
     that depends on the sheet, which is circular, so the browser drops it and
     the sheet overflows and is cropped rather than shrinking. Flex shrink along
     the main axis has no such problem. Both centring properties still centre;
     they only swap which axis they name. */
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 0 44px;
}

/* THE GOLD CORNER BRACKETS, four of them, pinned to the VIEWFINDER.
   Not to the paper: the paper is rotated -1.2deg and brackets that inherited
   that would read as a crooked frame rather than a camera's.

   Drawn as eight background layers on ONE pseudo-element rather than borders on
   two. The design has four L-shaped marks and an element has one box, so
   ::before and ::after between them can carry at most two. Eight gradients on a
   single inset box gives all four with no markup, which is what the handoff
   asked for and preferred over shipping two.

   Each corner is a 50x3 arm and a 3x50 arm meeting at the same point. */
.work__viewfinder::before {
  content: "";
  position: absolute;
  /* The design's insets are 32 / 58 / 100 against a viewfinder around 500px
     tall. The percentages take over only when the viewfinder is squeezed, which
     happens on a reopened link with pages already sent: at the fixed values the
     58 and the 100 would meet in the middle and the four marks would collapse
     into one box. min() keeps the design's numbers wherever there is room. */
  left: min(32px, 8%); right: min(32px, 8%);
  top: min(58px, 14%); bottom: min(100px, 24%);
  z-index: 1;
  pointer-events: none;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold));
  background-size:
    50px 3px, 3px 50px,
    50px 3px, 3px 50px,
    50px 3px, 3px 50px,
    50px 3px, 3px 50px;
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

/* THE RULED SHEET. A placeholder for the page the student is about to shoot;
   the OS camera supplies the real viewfinder.

   IT USED TO STRETCH, and this is the fix. It was absolutely positioned with
   top and bottom insets inside a flex child that grows, so its height was
   whatever was left over: on a tall phone a long thin rectangle, on a short one
   a letterbox, and never the sheet in the design. Now it is a flex item with a
   locked 3:4 ratio, so it is a sheet at every viewport height and the leftover
   space becomes margin around it instead of stretch inside it. */
.work__page-mock {
  position: relative;
  inset: auto;
  /* THE SHEET FITS BOTH WAYS, and the ratio never breaks.
     height 400 is what it wants; flex-shrink takes it down when the viewfinder
     is shorter than that, and min-height: 0 is what allows a flex item to go
     below its content size at all. width is auto so aspect-ratio derives it
     from whatever height survives, and max-width caps the wide case. */
  flex: 0 1 auto;
  height: 400px;
  min-height: 0;
  width: auto;
  max-width: min(300px, 100%);
  aspect-ratio: 3 / 4;
  background: var(--off-white);
  transform: rotate(-1.2deg);
  box-shadow: 0 16px 40px rgba(0,0,0,.5);
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 33px, rgba(27,67,50,.13) 33px 34px);
}

/* THE INSTRUCTION PILL, centred over the paper.

   Absolute now, because the paper stopped being absolute. With both in flow the
   viewfinder would lay them out side by side, which is not a thing the design
   has ever shown. left/top 50% with a translate keeps the pill hugging its own
   text rather than stretching to the column, so it stays a pill however long
   the sentence gets. */
.work__hint {
  position: absolute; z-index: 2;
  /* left AND right at 0 with auto side margins, not left: 50% with a translate.
     A shrink-to-fit absolute box offset from the left can only use the width
     REMAINING to its right, so left: 50% halved the pill's available width and
     wrapped a one-line sentence onto three. Both offsets give it the full
     width; fit-content and auto margins keep it a pill and keep it centred. */
  left: 0; right: 0; top: 50%; transform: translateY(-50%);
  width: fit-content;
  max-width: calc(100% - 44px);
  margin: 0 auto;
  background: rgba(20,55,42,.86); border: 1px solid rgba(227,181,58,.5);
  border-radius: 100px; padding: 13px 22px;
  font-size: 13.5px; font-weight: 600;
  color: var(--off-white); text-align: center;
  box-shadow: 0 10px 26px rgba(3,20,10,.4);
}

.work__controls {
  /* flex-shrink: 0 so the bar never grows into the space the viewfinder gave up.
     Without it the controls float in a tall band with uneven air above and
     below, which is the "dead space" in the handoff. */
  flex-shrink: 0;
  /* The bottom inset lives on .work__library, which sits under this row and is
     the thing actually touching the home indicator. */
  padding: 22px 24px 12px;
  background: var(--green-deep);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}

/* CHOOSE FROM PHOTOS. Full width under the shutter row, so it is a destination
   rather than a thing tucked beside the DONE button. Gold because it is an
   action; quieter weight than DONE because the camera is still the main path. */
.work__library {
  flex-shrink: 0;
  background: var(--green-deep);
  /* THE BOTTOM INSET LIVES HERE because this row, not the shutter row, is what
     touches the home indicator and Safari's address bar. max(24px, inset) gave
     34px on a notched phone, which is the inset alone with no breathing room
     left over; adding to 22px always clears it. */
  padding: 0 24px calc(22px + var(--sab));
  display: flex; justify-content: center;
}
.work__librarybtn, .work__addbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 44px; padding: 0 18px; cursor: pointer;
  border: 1px solid rgba(227,181,58,.4); border-radius: 100px;
  background: rgba(227,181,58,.08); color: var(--gold-light);
  font-size: 12px; font-weight: 600; letter-spacing: .06em;
}
.work__librarybtn:active, .work__addbtn:active { background: rgba(227,181,58,.16); }
/* Drawn, not a glyph: the same reason the delete X is drawn. A font's idea of a
   picture character varies by platform and can be swapped for an emoji. */
.work__libraryicon, .work__addicon { width: 17px; height: 17px; flex: none; }

/* THE PAIR ON THE REVIEW SCREEN. Centred under the strip and equal width, so
   neither reads as the primary and the choice is camera-or-library rather than
   a button with an afterthought beside it. They share the width rather than
   hugging their labels, because "TAKE ANOTHER" and "ADD FROM PHOTOS" are very
   different lengths and matching pills is the point.
   min-width: 0 so a long label shrinks the pill instead of the row. */
.work__addrow {
  display: flex; justify-content: center; gap: 10px;
}
.work__addrow .work__addbtn { flex: 1 1 0; min-width: 0; padding: 0 10px; }
.work__addbtn { text-align: center; }
/* MEASURED, not assumed: at 320px "ADD FROM PHOTOS" wrapped to two lines while
   "TAKE ANOTHER" stayed on one, which is exactly the mismatch the pair is meant
   to avoid. Tightened here so both hold one line on the narrowest phone still
   in use. A future longer label would wrap inside its own pill rather than push
   the page sideways, which is the failure worth having. */
@media (max-width: 360px) {
  .work__addrow { gap: 8px; }
  .work__addrow .work__addbtn {
    padding: 0 6px; gap: 6px; font-size: 11px; letter-spacing: .03em;
  }
  .work__addrow .work__addicon { width: 15px; height: 15px; }
}
/* And at 320px, the narrowest screen anyone still holds, the tightened type is
   still 9px short of "ADD FROM PHOTOS" on one line, so the icons go. BOTH of
   them: losing one icon and keeping the other is worse than losing both, which
   is the whole reason the pair exists. The labels say what they do without
   help. */
@media (max-width: 330px) {
  .work__addrow .work__addicon { display: none; }
  .work__addrow .work__addbtn { gap: 0; }
}
.work__thumbslot {
  position: relative; width: 56px; height: 56px; border-radius: 5px;
  border: 1.5px solid rgba(227,181,58,.5); overflow: hidden; flex: none;
}
/* NO LABEL INSIDE IT, decided rather than overlooked. The capture-restyle
   handoff asked for text in this square so it reads as a state rather than a
   gap; Trey ruled on 2026-08-30 that the styled dashed square stands on its
   own. Leave it empty. */
.work__thumbslot--empty { border-style: dashed; border-color: rgba(227,181,58,.5); }
.work__thumbslot[hidden] { display: none; }
.work__thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.work__count {
  position: absolute; right: 0; bottom: 0; width: 24px; height: 24px;
  background: var(--gold); border-radius: 5px 0 0 0;
  font-size: 11px; font-weight: 700; color: var(--green-deep);
  display: flex; align-items: center; justify-content: center;
}
.work__shutter {
  width: 78px; height: 78px; border-radius: 50%;
  background: var(--off-white); border: 4px solid rgba(227,181,58,.5);
  cursor: pointer; flex: none; display: block;
}
.work__shutter:active { transform: scale(.96); }
.work__done {
  padding: 14px 24px; min-height: 44px; border-radius: 4px;
  /* A TRANSPARENT BORDER IN THE ENABLED STATE, so the disabled one can have a
     real border without the button changing size the moment it enables. */
  border: 1px solid transparent;
  background: var(--gold-light); color: var(--green-deep);
  font-size: 13px; font-weight: 600; letter-spacing: .06em; cursor: pointer;
}
/* DISABLED READ AS ENABLED. opacity: .38 over the deep green ground turned the
   gold into a solid olive block that looked like a button waiting to be
   pressed, on the screen where nothing has been shot yet and DONE is disabled
   the whole time. An outline says "not yet" the way a dimmed fill does not. */
.work__done:disabled {
  background: transparent;
  border-color: rgba(255,255,255,.25);
  color: rgba(255,255,255,.45);
  cursor: default;
}

/* ── S3 review ───────────────────────────────────────────────────────── */
/* GRID, not space-between. The corner control moved out, and with two children
   space-between would have pushed the page count to the right edge. The empty
   third column keeps it centred. */
.work__reviewhead {
  padding: max(66px, var(--sat)) 20px 14px;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
}
.work__reviewhead .work__pagecount { text-align: center; }
/* 44px target without shifting the label, per the design's negative-margin
   trick. */
.work__linkbtn {
  background: none; border: 0; cursor: pointer;
  font-size: 12.5px; font-weight: 600; letter-spacing: .06em;
  color: rgba(255,255,255,.72);
  min-height: 44px; padding: 0 12px; margin: 0 -12px;
  display: inline-flex; align-items: center;
}
.work__linkbtn--gold { color: var(--gold-light); }
.work__pagecount { font-family: 'Libre Caslon Text', Georgia, serif; font-size: 19px; }

.work__reviewbody { flex: 1; padding: 0 18px; display: flex; flex-direction: column; gap: 14px; }
.work__thumbs { display: flex; gap: 10px; list-style: none; margin: 0; padding: 0; overflow-x: auto; }
.work__thumbitem {
  position: relative; flex: 1 0 96px; height: 188px; border-radius: 5px;
  background: var(--off-white); border: 1px solid rgba(255,255,255,.24); overflow: hidden;
}
/* THE WHOLE PAGE, NOT A CROP. object-fit: cover filled the tile by cutting the
   edges off, which is exactly the wrong trade here: the student's decision on
   this screen is "is this photo good enough to send", and they were judging it
   from a centre crop that hid whether the page edges made it into frame.
   `contain` fits the entire image and leaves ground around it, which is the same
   choice the confirmation screen makes. */
.work__thumbimg {
  width: 100%; height: 100%; object-fit: contain; display: block;
  background: var(--green-deep);
}
.work__pagebadge {
  position: absolute; left: 7px; bottom: 7px;
  padding: 3px 9px; border-radius: 100px; background: rgba(20,55,42,.85);
  font-size: 10.5px; font-weight: 700; color: var(--off-white);
}
.work__delete {
  position: absolute; top: 0; right: 0; width: 44px; height: 44px;
  background: none; border: 0; cursor: pointer; color: var(--off-white);
  display: flex; align-items: center; justify-content: center;
}
/* The 44px target stays; the visible circle is smaller and now carries a drawn
   X so the control reads as "remove" rather than as an unexplained dot. */
.work__delete::before {
  content: ''; position: absolute; width: 28px; height: 28px; border-radius: 50%;
  background: rgba(20,55,42,.88); border: 1px solid rgba(227,181,58,.5);
}
.work__deleteglyph { position: relative; z-index: 1; }

/* The confirmation strip already showed whole pages; keep the two consistent. */
.work__receivedimg { object-fit: contain; background: var(--green-deep); }

.work__notefield {
  background: rgba(253,252,249,.07); border: 1px solid rgba(227,181,58,.28);
  border-radius: 6px; padding: 15px 16px;
}
.work__notelabel {
  display: block; font-size: 9.5px; font-weight: 600; letter-spacing: .14em;
  color: var(--gold-light); margin-bottom: 6px;
}
.work__note {
  width: 100%; background: none; border: 0; resize: none;
  font: inherit; font-size: 16px; line-height: 1.6; color: var(--off-white);
}
.work__note::placeholder { color: rgba(255,255,255,.38); }
.work__note:focus { outline: 2px solid rgba(227,181,58,.5); outline-offset: 4px; }

.work__summary { margin: 0; display: flex; flex-direction: column; gap: 6px; }
.work__summaryrow { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; }
.work__summaryrow dt { color: rgba(255,255,255,.6); }
.work__summaryrow dd { margin: 0; color: var(--off-white); font-weight: 600; text-align: right; }

/* Upload failure. Gold, never red. */
.work__failure {
  background: rgba(227,181,58,.14); border: 1px solid rgba(227,181,58,.45);
  border-radius: 6px; padding: 14px 16px;
  display: flex; flex-direction: column; gap: 10px;
}
/* [hidden] MUST ACTUALLY HIDE.
   The display declaration above outranks the user agent's [hidden]{display:none},
   so without this the failure panel is on screen from the moment the review
   screen renders and never goes away. That is what made the review screen show
   a "Try again" panel beside a healthy, sendable page: there had been no
   failure at all, the panel had simply never been hidden. It is the same trap
   recorded against .roster__row, .rolodex, .extra and .spop above, which put
   every deactivated roster row on production. */
.work__failure[hidden] { display: none !important; }
.work__failuretext { margin: 0; font-size: 13px; line-height: 1.55; color: var(--off-white); }
.work__retry {
  align-self: flex-start; min-height: 44px; padding: 0 18px;
  border: 1px solid rgba(227,181,58,.6); border-radius: 4px;
  background: none; color: var(--gold-light);
  font-size: 12.5px; font-weight: 600; letter-spacing: .06em; cursor: pointer;
}

.work__cta { padding: 18px 20px max(38px, var(--sab)); }
.work__send {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 52px; border: 0; border-radius: 4px;
  background: var(--gold-light); color: var(--green-deep);
  font-size: 13.5px; font-weight: 600; letter-spacing: .06em;
  cursor: pointer; text-decoration: none;
}
.work__send:disabled { opacity: .38; cursor: default; }

/* ── S4 confirmed ────────────────────────────────────────────────────── */
.work--sent { position: relative; }
/* Clips the two oversized decorative glows. Fixed + inset:0 + overflow:hidden
   is unambiguous; overflow on <body> propagates to the viewport and leaves the
   element's own clipping behaviour a question. */
.work__glowwrap {
  position: fixed; inset: 0; overflow: hidden; pointer-events: none; z-index: 0;
}
.work__glow { position: absolute; pointer-events: none; border-radius: 50%; }
.work__glow--a {
  width: 640px; height: 640px; left: -180px; top: -140px;
  background: radial-gradient(circle at 42% 40%,
    rgba(227,181,58,.26) 0%, rgba(227,181,58,.06) 46%, rgba(20,55,42,0) 72%);
}
.work__glow--b {
  width: 520px; height: 520px; right: -160px; bottom: -120px;
  background: radial-gradient(circle at 58% 56%,
    rgba(227,181,58,.14) 0%, rgba(20,55,42,0) 68%);
}
.work__shell--sent, .work__shell--notice { position: relative; z-index: 1; }
.work__wordmark {
  padding: max(78px, var(--sat)) 26px 0;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-family: 'Libre Caslon Text', Georgia, serif; font-size: 13px;
  color: rgba(255,255,255,.62);
}
.work__wordmark em { font-style: normal; color: rgba(227,181,58,.8); }
.work__centre {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 10px; padding: 24px 26px; text-align: center;
}
.work__stack { position: relative; width: 190px; height: 150px; margin-bottom: 14px; }
.work__sheet {
  position: absolute; background: var(--off-white); border-radius: 4px;
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 11px, rgba(27,67,50,.13) 11px 12px);
}
.work__sheet--back-l { width: 96px; height: 120px; left: 14px; top: 22px; transform: rotate(-13deg); }
.work__sheet--back-r { width: 96px; height: 120px; right: 14px; top: 18px; transform: rotate(9deg); }
.work__sheet--front  { width: 96px; height: 124px; left: 47px; top: 8px; transform: rotate(-2deg);
                       box-shadow: 0 18px 40px rgba(3,20,10,.5); }
.work__check {
  position: absolute; left: 50%; bottom: -6px; transform: translateX(-50%);
  width: 52px; height: 52px; border-radius: 50%; background: var(--gold-light);
  color: var(--green-deep); font-size: 24px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 10px 24px rgba(3,20,10,.4);
}
.work__thanks {
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 38px; font-weight: 400; line-height: 1.12; margin: 0; color: var(--off-white);
}
.work__bang { color: var(--gold-light); }
.work__body {
  max-width: 296px; margin: 0; font-size: 15px; line-height: 1.65;
  color: rgba(255,255,255,.78);
}
.work__body b { color: #fff; font-weight: 600; }
.work__closing {
  max-width: 280px; margin: 0; font-size: 13px; line-height: 1.7;
  color: rgba(255,255,255,.55);
}
.work__sentcta { padding: 0 26px max(44px, var(--sab)); }
.work__filed {
  margin: 0; min-height: 44px; display: flex; align-items: center; justify-content: center;
  font-size: 12.5px; color: rgba(255,255,255,.5);
}
.work__noticehead {
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 27px; font-weight: 400; margin: 0 0 4px; color: var(--off-white);
}

/* ══ WORK SUBMISSION: tutor inbox and detail ════════════════════════════════
   M1/D1 and M2/D2 are the SAME views. Everything below is the desktop
   treatment; the existing 760px block at the end of this file stacks it.

   No new tokens, no new architecture: counters, rows, chips and eyebrows all
   reuse the shapes the dashboard and queue already use. */

/* ── THE WEEK STRIP ──────────────────────────────────────────────────────
   Sits between the subhead and the counters. Seven links in a grid, never a
   scroller: SEVEN ACROSS AT 390px IS THE MEASUREMENT TO DEFEND, because a day
   that has to be scrolled to is a day nobody checks. Everything else on this
   row gives way before that does.

   No new architecture: the existing 760px breakpoint handles the mobile size,
   and every colour is an existing token. */
.wweek { margin: 0 0 22px; }
.wweek__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 26px 0 10px;
}
.wweek__eyebrow {
  font-size: 10.5px; font-weight: 600; letter-spacing: .14em; color: var(--gold-dark);
}
.wweek__nav { display: flex; align-items: center; gap: 8px; }
.wweek__arrow, .wweek__today {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 34px; border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  text-decoration: none;
}
.wweek__arrow { min-width: 34px; font-size: 14px; color: var(--muted); }
/* Before the school year there is nothing to go back to. Dimmed and inert
   rather than removed, so the row does not reflow when you reach the edge. */
.wweek__arrow--off { color: var(--line); cursor: default; }
.wweek__today { padding: 0 14px; font-size: 12px; font-weight: 600; color: var(--green-btn); }

.wweek__row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
.wweek__day {
  background: #fff; border: 1px solid rgba(27,67,50,.14); border-radius: 6px;
  padding: 13px 10px 12px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-decoration: none;
  /* A seven-column grid whose cells refuse to shrink is a seven-column grid
     that overflows. This is what lets the row hold at 390px. */
  min-width: 0;
}
.wweek__wd {
  font-size: 9.5px; font-weight: 600; letter-spacing: .12em; color: var(--faint);
  white-space: nowrap;
}
.wweek__date {
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 22px; line-height: 1.1; color: var(--ink);
}
.wweek__count { font-size: 11px; color: var(--faint); white-space: nowrap; }
.wweek__count--short { display: none; }

/* THE THREE BLOCK STATES ARE WRITTEN WEAKEST FIRST, and the order IS the
   precedence: empty, then today, then selected. Every one of these is a single
   class, so they all have the same specificity and the last one written wins.
   Getting this backwards is not cosmetic. Written the other way round, a
   selected day with no sessions renders as an ordinary empty block and the
   tutor cannot see which day they are on, and today with no sessions loses its
   gold border entirely. Both were real: measured, then reordered. */

/* A day with nothing on it reads as unavailable rather than as a zero to chase.
   #A9B1AA is the handoff's disabled grey and has no token of its own. */
.wweek__day--empty { background: rgba(27,67,50,.04); border-color: rgba(27,67,50,.1); }
.wweek__day--empty .wweek__wd,
.wweek__day--empty .wweek__date,
.wweek__day--empty .wweek__count { color: #A9B1AA; }

/* TODAY, NOT SELECTED. A different mark from selected on purpose: a tutor
   reading next week must not lose sight of where today is. */
.wweek__day--today { border: 1.5px solid var(--gold); }
.wweek__day--today .wweek__wd { color: var(--gold-dark); }

/* SELECTED WINS over both, and its border is the fill rather than a drawn edge. */
.wweek__day--selected {
  background: var(--green-deep); border: 1px solid var(--green-deep);
  box-shadow: 0 6px 16px rgba(20,55,42,.2);
}
.wweek__day--selected .wweek__wd { color: var(--gold-light); }
.wweek__day--selected .wweek__date { color: var(--off-white); }
.wweek__day--selected .wweek__count { color: rgba(255,255,255,.72); }

/* THE DOT IS ALWAYS IN THE BOX, coloured only when it means something, so a day
   gaining work cannot make the whole row jump a pixel. */
.wweek__dot { width: 7px; height: 7px; border-radius: 50%; background: none; }
.wweek__dot--on { background: var(--gold); }
.wweek__day--selected .wweek__dot--on { background: var(--gold-light); }

.wweek__legend {
  display: flex; align-items: center; gap: 16px;
  margin: 11px 0 0; font-size: 11px; color: var(--faint);
}
.wweek__key { display: inline-flex; align-items: center; gap: 6px; }
.wweek__keybox { width: 9px; height: 9px; border: 1.5px solid var(--gold); border-radius: 2px; }

@media (max-width: 760px), (max-height: 499px) {
  .wweek__row { gap: 5px; }
  .wweek__day { padding: 9px 2px 8px; min-height: 44px; }
  .wweek__wd { font-size: 9px; letter-spacing: .06em; }
  .wweek__date { font-size: 18px; }
  /* The numeral replaces the word: there is room for one and not the other, and
     the word is what a 49px block cannot hold. */
  .wweek__count { font-size: 9.5px; }
  .wweek__count--full { display: none; }
  .wweek__count--short { display: inline; }
  .wweek__dot { width: 6px; height: 6px; }
  /* TODAY's marker is the gold border here. The word does not fit beside MON in
     49px and the border already says it, so the suffix goes rather than the
     seven-across measurement. */
  .wweek__todaymark { display: none; }
}

.page--work .wcount, .page--wdetail .wcount { margin: 0 0 22px; }
.wcount { display: flex; gap: 12px; margin: 0 0 22px; flex-wrap: wrap; }
.wcount__tile {
  flex: 1 1 180px; background: var(--off-white);
  border: 1px solid var(--line); border-radius: 6px;
  padding: 16px 18px; display: flex; flex-direction: column; gap: 4px;
}
/* "Work to review" is the one that matters, so it is the inverted tile. */
.wcount__tile--on { background: var(--green-deep); border-color: var(--green-deep); }
.wcount__n { font-family: 'Libre Caslon Text', Georgia, serif; font-size: 26px; color: var(--ink); }
.wcount__tile--on .wcount__n { color: var(--gold-light); }
.wcount__label { font-size: 9.5px; font-weight: 600; letter-spacing: .12em; color: var(--faint); }
.wcount__tile--on .wcount__label { color: rgba(255,255,255,.72); }

/* The calendar's own label for a lesson that resolved to no tutor. Quiet: it is
   evidence for a decision, not a status. */
.wrow__feed {
  display: block; margin-top: 3px; font-size: 11.5px; color: var(--faint);
  font-style: italic;
}

.wfilter { font-size: 13px; color: var(--muted); margin: 0 0 16px; }
.wfilter__link { color: var(--green-btn); }

.eyebrow {
  font-size: 10.5px; font-weight: 600; letter-spacing: .14em;
  color: var(--gold-dark); margin: 0 0 8px;
}

.wlist {
  list-style: none; margin: 0; padding: 0;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px;
  overflow: hidden;
}
.wrow {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 18px; border-top: 1px solid var(--hair);
}
.wrow:first-child { border-top: 0; }
/* Rows with new work get the gold edge. Nothing gets a "late" treatment: there
   is no late state in this feature. */
.wrow--new { border-left: 3px solid var(--gold); }
.wrow__main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.wrow__name { font-size: 14px; font-weight: 600; color: var(--ink); }
.wrow__meta { font-size: 11px; color: var(--gold-dark); }
.wrow__note { font-size: 12px; font-style: italic; color: var(--muted); }
.wrow__flag {
  font-size: 9.5px; font-weight: 600; letter-spacing: .12em; color: var(--gold-dark);
}
.wrow__nudge { font-size: 11.5px; color: var(--faint); }
.wrow__side { flex: none; }

.wchip {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 7px 13px; border-radius: 100px;
  background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.35);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  color: var(--green-deep); text-decoration: none; text-align: center;
}
.wchip--quiet { background: var(--row-tint, rgba(27,67,50,.04)); border-color: var(--line); color: var(--muted); }
.wchip--none { background: none; border-color: transparent; color: var(--faint); }

/* ── detail ──────────────────────────────────────────────────────────── */
.wback { margin: 0 0 8px; }
.wback__link { font-size: 13px; color: var(--green-btn); text-decoration: none; }

.wnote {
  background: var(--off-white); border: 1px solid var(--line);
  border-left: 3px solid var(--gold); border-radius: 6px;
  padding: 14px 16px; margin: 0 0 18px;
}
.wnote__label {
  display: block; font-size: 9.5px; font-weight: 600; letter-spacing: .14em;
  color: var(--gold-dark); margin-bottom: 6px;
}
.wnote__quote { margin: 0; font-size: 13.5px; font-style: italic; color: var(--body); }

.wpages { list-style: none; display: flex; gap: 12px; margin: 0 0 18px; padding: 0; flex-wrap: wrap; }
.wpages__item { position: relative; flex: 1 1 160px; max-width: 260px; }
.wpages__img {
  width: 100%; height: 186px; object-fit: cover; display: block;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 5px;
}
.wpages__num {
  position: absolute; left: 7px; bottom: 7px;
  padding: 3px 9px; border-radius: 100px; background: rgba(20,55,42,.85);
  font-size: 10.5px; font-weight: 700; color: var(--off-white);
}

.wactions { display: flex; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; }
.wbtn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 52px; padding: 0 20px; border-radius: 4px; border: 0;
  font-size: 13.5px; font-weight: 600; cursor: pointer; text-decoration: none;
}
.wbtn--primary { background: var(--green-btn); color: #fff; }
.wbtn--ghost {
  background: none; border: 1px solid rgba(27,67,50,.25); color: var(--green-btn);
  min-height: 48px;
}
.wbtn:disabled { opacity: .5; cursor: default; }
.wsavehint { font-size: 12.5px; color: var(--muted); margin: 0 0 18px; }

/* ── the full-screen page viewer ─────────────────────────────────────────
   THE [hidden] GUARD IS FIRST AND IS !important, for the reason recorded in
   CLAUDE.md: [hidden] is a user-agent rule and loses to any author display, so
   an author-origin `display: flex` on this block would leave the viewer sitting
   over the whole page permanently. That is the exact shape of the roster
   incident, and it is cheaper to write this line than to find out again. */
.wview[hidden] { display: none !important; }
.wview {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column;
  background: rgba(9, 26, 19, .96);
}
/* The page behind must not scroll under the viewer, which on iOS is the
   difference between a viewer and a viewer that drifts. */
body.wview-open { overflow: hidden; }

.wview__bar {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: max(14px, var(--sat)) 16px 12px;
  color: var(--off-white);
}
.wview__count { font-size: 13px; font-weight: 600; letter-spacing: .04em; }
.wview__close {
  width: 44px; height: 44px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer; color: var(--off-white);
}
.wview__close svg { width: 22px; height: 22px; }

.wview__stage {
  flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 0 8px max(16px, var(--sab));
}
/* THE WHOLE PAGE, NEVER A CROP. Same trade as the student's submit tiles: a
   tutor is reading the work, so an edge cut off is the edge they needed. */
.wview__img {
  max-width: 100%; max-height: 100%;
  object-fit: contain; border-radius: 4px; background: var(--off-white);
}
.wview__nav {
  width: 48px; height: 48px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 1px solid rgba(255,255,255,.28);
  background: rgba(9,26,19,.5); color: var(--off-white); cursor: pointer;
}
.wview__nav svg { width: 22px; height: 22px; }
.wview__nav:disabled { opacity: .3; cursor: default; }
.wview__nav[hidden] { display: none !important; }

@media (max-width: 720px) {
  /* On a phone the arrows sit over the image rather than beside it: 48px of
     furniture each side of a portrait page leaves the page itself too small to
     read, and swipe is the primary gesture here anyway. */
  .wview__stage { position: relative; padding-left: 0; padding-right: 0; }
  .wview__nav { position: absolute; top: 50%; transform: translateY(-50%); }
  .wview__nav--prev { left: 8px; }
  .wview__nav--next { right: 8px; }
}

.wprep { display: flex; align-items: center; gap: 12px; margin: 0 0 22px; }
.wprep__at { font-size: 12.5px; color: var(--faint); }

.wfiled {
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px;
  padding: 14px 16px;
}
.wfiled__label {
  display: block; font-size: 9.5px; font-weight: 600; letter-spacing: .14em;
  color: var(--faint); margin-bottom: 8px;
}
.wfiled__rows { margin: 0; display: flex; flex-direction: column; gap: 6px; }
.wfiled__rows > div { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; }
.wfiled__rows dt { color: var(--muted); }
.wfiled__rows dd { margin: 0; color: var(--ink); font-weight: 600; text-align: right; }

@media (max-width: 760px), (max-height: 499px) {
  /* Rows become stacked cards and the chip drops below, per the existing
     mobile rules for the dashboard and queue tables. */
  .wrow { flex-direction: column; align-items: flex-start; gap: 10px; }
  .wrow__side { width: 100%; }
  .wchip { width: 100%; justify-content: center; }
  .wcount__tile { flex: 1 1 46%; }
  .wactions { flex-direction: column; }
  .wbtn { width: 100%; }
  .wpages__item { flex: 1 1 46%; }
}

/* ══ WORK SUBMISSION: consent, ADMIN side ══════════════════════════════════
   The parent-facing page has its own block further down (.pc__*). These are the
   admin screen's classes only. */
.cnst__lockup { display: flex; align-items: center; gap: 8px; margin: 0 0 22px; }
.cnst__wordmark { font-family: 'Libre Caslon Text', Georgia, serif; font-size: 15px; color: var(--ink); }
.cnst__wordmark em { font-style: normal; color: var(--gold-dark); }
.cnst__form { display: flex; flex-direction: column; gap: 16px; margin: 22px 0 0; }
.cnst__field { display: flex; flex-direction: column; gap: 5px; }
.cnst__opt { font-size: 9px; color: var(--faint); letter-spacing: .1em; }
.cnst__submit { align-self: flex-start; }
/* Gold, never red: the portal has no error colour. */
.cnst__error {
  background: rgba(227,181,58,.14); border: 1px solid rgba(227,181,58,.45);
  border-radius: 6px; padding: 12px 14px; margin: 0 0 16px;
  font-size: 13px; color: var(--ink);
}
.cnst__ok {
  background: rgba(27,67,50,.06); border: 1px solid var(--line);
  border-radius: 6px; padding: 12px 14px; margin: 0 0 16px; font-size: 13px; color: var(--ink);
}
.cnst__link { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.cnst__pointer { font-size: 13px; color: var(--muted); margin: 14px 0 0; }
.h2 { font-family: 'Libre Caslon Text', Georgia, serif; font-size: 19px; font-weight: 400; color: var(--ink); margin: 28px 0 10px; }
@media (max-width: 760px), (max-height: 499px) {
  .cnst__link { width: 100%; }
  .cnst__link .in, .cnst__link .sel, .cnst__link .wbtn { width: 100%; }
}
@media (max-width: 760px), (max-height: 499px) {
  .cnst__link { width: 100%; }
  .cnst__link .in, .cnst__link .sel, .cnst__link .wbtn { width: 100%; }
}

/* ══ PARENT CONSENT (.pc__*) ═══════════════════════════════════════════════
   design_handoff_parent_consent/README.md is the spec. Phone at 390 is the
   primary target; the desktop treatment centres a 560px card and never widens.

   TOKENS. Deep green, green, gold, paper and ink are the portal's own and are
   referenced through the variables at the top of this file. The five derived
   neutrals below are specific to this page and are declared here rather than
   promoted to :root, because nothing else uses them and a global token nobody
   else needs is just a wider blast radius.

   NO RED ANYWHERE, and none of the rules below can produce any. Validation is
   gold border, gold tint fill, gold-ink message. Native bubbles are suppressed
   with novalidate on the form.

   NO SHADOWS and NO MOTION, per the spec. Depth comes from hairline borders. */
.pc {
  --pc-gold-ink: #7C6210;    /* validation message text, clears 5:1 on paper */
  --pc-gold-tint: #FDF7E6;   /* fill behind an invalid field or panel */
  --pc-muted: #5C6B61;       /* secondary copy, labels, eyebrows on paper */
  --pc-hairline: #DDD6C6;    /* borders on inputs and cards */
  --pc-placeholder: #96A29A;
  --pc-slash: #B8B0A0;

  --pc-mono: ui-monospace, Menlo, monospace;
  --pc-serif: 'Libre Caslon Text', Georgia, serif;
  --pc-sans: Helvetica, Arial, sans-serif;

  margin: 0;
  min-height: 100%;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--pc-sans);
  -webkit-font-smoothing: antialiased;
}

/* Every control shows the same ring. :focus-visible so a mouse click on the
   button does not draw one. */
.pc :focus-visible { outline: 2px solid var(--green-deep); outline-offset: 2px; }

/* ── masthead ─────────────────────────────────────────────────────────── */
.pc__mast {
  height: 56px; padding: 0 20px;
  background: var(--green-deep); border-bottom: 2px solid var(--gold);
  display: flex; align-items: center; justify-content: space-between;
}
.pc__wordmark {
  font-family: var(--pc-serif); font-weight: 400; font-size: 16px;
  line-height: 1; color: var(--cream);
}
.pc__eyebrow {
  font-family: var(--pc-mono); font-size: 10.5px; line-height: 1;
  letter-spacing: .12em; text-transform: uppercase; color: var(--gold);
}

/* ── content column ───────────────────────────────────────────────────── */
.pc__col { padding: 28px 20px 36px; }
.pc__col--done { padding: 36px 20px; }
/* Phone: no card, just the stack. The column owns the padding, this owns the
   rhythm between blocks. */
.pc__card { display: flex; flex-direction: column; gap: 22px; }
.pc__col--done .pc__card { gap: 24px; }

.pc__headgroup { display: flex; flex-direction: column; gap: 10px; }
.pc__headgroup--done { gap: 12px; }
.pc__h1 {
  margin: 0; font-family: var(--pc-serif); font-weight: 400;
  font-size: 27px; line-height: 1.22; color: var(--ink); text-wrap: pretty;
}
.pc__h1--done { font-size: 30px; line-height: 1.2; }
.pc__body {
  margin: 0; font-size: 15px; line-height: 1.6; color: var(--pc-muted);
  text-wrap: pretty;
}

/* ── form ─────────────────────────────────────────────────────────────── */
.pc__form { display: flex; flex-direction: column; gap: 22px; }
.pc__fields { display: flex; flex-direction: column; gap: 14px; }
.pc__field { display: flex; flex-direction: column; gap: 6px; }
.pc__label {
  font-size: 11.5px; font-weight: 700; line-height: 1.3; letter-spacing: .07em;
  text-transform: uppercase; color: var(--pc-muted);
}
.pc__input {
  width: 100%; height: 48px; padding: 0 12px; box-sizing: border-box;
  border-radius: 4px; border: 1px solid var(--pc-hairline);
  background: #FFFFFF; color: var(--ink);
  font-family: var(--pc-sans); font-weight: 400;
  /* 16px is load bearing: anything smaller makes iOS Safari zoom on focus. */
  font-size: 16px; line-height: 1;
}
.pc__input::placeholder { color: var(--pc-placeholder); }
/* The 2px border REPLACES the 1px one. box-sizing above is what stops the
   field shifting by a pixel when it does. */
.pc__input--bad { border: 2px solid var(--gold); background: var(--pc-gold-tint); }

.pc__msg {
  margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--pc-gold-ink);
}
/* An empty message element still exists in the DOM so the client can fill it
   in without building nodes. Empty means gone, not blank space. */
.pc__msg:empty { display: none; }

/* ── consent panel ────────────────────────────────────────────────────── */
.pc__consent { display: flex; flex-direction: column; gap: 8px; }
.pc__panel {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px; min-height: 44px; box-sizing: border-box;
  border-radius: 6px; border: 1px solid var(--pc-hairline);
  background: #FFFFFF; cursor: pointer;
}
.pc__panel--bad { border: 2px solid var(--gold); background: var(--pc-gold-tint); }
.pc__check {
  width: 22px; height: 22px; margin: 1px 0 0; flex-shrink: 0;
  accent-color: var(--green-btn);
}
.pc__consenttext { font-size: 13px; line-height: 1.55; color: var(--ink); }

/* ── links row ────────────────────────────────────────────────────────── */
.pc__links { margin: 0; display: flex; align-items: center; gap: 10px; font-size: 14px; }
.pc__link {
  display: flex; align-items: center; min-height: 44px;
  color: var(--green-btn); text-decoration: underline; text-underline-offset: 2px;
}
.pc__link:hover { color: var(--green-deep); }
.pc__slash { color: var(--pc-slash); }

/* ── primary button ───────────────────────────────────────────────────── */
.pc__btn {
  width: 100%; height: 52px; border: 0; border-radius: 4px;
  background: var(--green-btn); color: var(--cream);
  font-family: var(--pc-sans); font-weight: 700; font-size: 16px; line-height: 1;
  cursor: pointer;
}
.pc__btn:hover { background: var(--green-deep); }

/* ── S2 confirmation ──────────────────────────────────────────────────── */
.pc__mark {
  width: 44px; height: 44px; border-radius: 50%; background: var(--gold);
  display: flex; align-items: center; justify-content: center;
}
.pc__numcard {
  padding: 20px; border-radius: 6px; border: 1px solid var(--pc-hairline);
  background: #FFFFFF; display: flex; flex-direction: column; gap: 8px;
}
.pc__numeyebrow {
  font-family: var(--pc-mono); font-size: 10.5px; line-height: 1;
  letter-spacing: .12em; text-transform: uppercase; color: var(--pc-muted);
}
.pc__num {
  font-family: var(--pc-serif); font-weight: 400; font-size: 30px; line-height: 1.15;
  color: var(--green-deep); font-variant-numeric: tabular-nums;
}
.pc__numcaption { font-size: 13.5px; line-height: 1.5; color: var(--pc-muted); }

/* ── desktop ──────────────────────────────────────────────────────────────
   The spec's own breakpoint: the phone column holds to about 640, above which
   a single 560px card centres and never widens. This is NOT the portal's 760px
   breakpoint, because this page is not a portal page and shares none of its
   layout. */
@media (min-width: 641px) and (min-height: 500px) {
  .pc__mast { height: 64px; padding: 0 40px; }
  .pc__wordmark { font-size: 18px; }
  .pc__eyebrow { font-size: 11px; }

  /* ⚠ THE align-items BELOW HAD NEVER DONE ANYTHING, because .pc__col is a
     <main> and a block container ignores it. Every parent-facing card on this
     site has therefore sat pinned to the left padding edge since the consent
     page shipped: 560px of card and 840px of empty paper beside it at 1440. It
     went unreported until the booking grid made the card 980px, where the gap
     stops reading as a wide margin and starts reading as a page that failed to
     load its right-hand half.

     THE DECLARATION IS THE FIX, NOT A NEW INTENT. The centring was always what
     this rule asked for; it just needed a formatting context that honours it.
     Every one of the six .pc__col pages has exactly one child, the card, so
     there is nothing else here for a flex context to move.

     DESKTOP ONLY, as it always was. Below 641px there is no card at all, just
     the stack, and the column owns the padding. */
  .pc__col {
    padding: 56px 40px 80px;
    display: flex; flex-direction: column;
    align-items: center;
  }
  .pc__col--done { padding: 56px 40px 80px; }

  /* ONE card, 560px, never wider. */
  .pc__card {
    width: 560px; max-width: 100%; box-sizing: border-box;
    background: #FFFFFF; border: 1px solid var(--pc-hairline); border-radius: 6px;
    padding: 40px; display: flex; flex-direction: column; gap: 24px;
  }
  /* The number card sits INSIDE the white card there, so it takes paper to
     stay distinguishable from its surroundings. */
  .pc__numcard { background: var(--cream); }
  .pc__h1 { font-size: 31px; line-height: 1.2; }
  .pc__h1--done { font-size: 31px; }
  .pc__body { font-size: 15.5px; }
  .pc__fields { gap: 16px; }
  .pc__consenttext { font-size: 13.5px; }
  .pc__num { font-size: 32px; }

  /* Not full width on desktop. */
  .pc__btn { width: auto; align-self: flex-start; min-width: 240px; padding: 0 28px; }
}

/* ══ TEXT REMINDERS block (admin student record and New Student) ════════════
   Portal classes, no new architecture. The status dot is the only new shape and
   it reuses the existing token set: gold for anything needing attention, green
   for active, faint for nothing on file. No red: opted out is a fact, not an
   error. */
.txr {
  border: 1px solid var(--line); border-radius: 6px;
  padding: 16px 18px; margin: 18px 0 0; background: var(--off-white);
}
.txr__title {
  font-family: 'Libre Caslon Text', Georgia, serif; font-size: 16px; font-weight: 400;
  color: var(--ink); margin: 0 0 10px;
}
.txr__status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.txr__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--faint); flex: none; }
.txr__status--active .txr__dot { background: var(--green-btn); }
.txr__status--pending .txr__dot { background: var(--gold); }
.txr__status--opted_out .txr__dot { background: var(--gold-dark); }
.txr__state { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.txr__num { font-size: 13px; color: var(--muted); }
.txr__note { margin: 8px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.txr__note a { color: var(--green-btn); }
.txr__actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 12px 0 0; }
.txr__copy {
  min-height: 44px; padding: 0 16px; border-radius: 4px;
  border: 1px solid rgba(27,67,50,.25); background: none; color: var(--green-btn);
  font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
}
.txr__url { font-size: 12px; color: var(--muted); word-break: break-all; }
.txr__toggle { margin-top: 14px; }
@media (max-width: 760px), (max-height: 499px) {
  .txr__actions { flex-direction: column; align-items: stretch; }
  .txr__copy { width: 100%; }
}

/* The verbal-consent form inside the Text reminders block. Reuses the admin
   form controls (.lbl, .in, .submit) rather than inventing any. */
.txr__verbal { margin: 16px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line); }
.txr__subtitle {
  font-family: 'Libre Caslon Text', Georgia, serif; font-size: 14px; font-weight: 400;
  color: var(--ink); margin: 0 0 4px;
}
.txr__verbal .lbl:first-of-type { margin-top: 10px; }
.txr__attest { margin-top: 14px; }
.txr__save { margin-top: 14px; }

/* The contact-card action inside the S2 number card. Deliberately quieter than
   the primary green button used elsewhere: S2 has no primary action by design,
   and this must not read as one. Green on the card's own ground, full width,
   with the 44px target the rest of this flow keeps. */
.pc__vcard {
  display: flex; align-items: center; justify-content: center;
  min-height: 44px; margin-top: 4px;
  border: 1px solid var(--green-btn); border-radius: 4px;
  color: var(--green-btn); background: transparent;
  font-family: var(--pc-sans); font-size: 14px; font-weight: 700;
  text-decoration: none;
}
.pc__vcard:hover { background: var(--green-btn); color: var(--cream); }

/* Per-page failure on the review screen, and the received-pages strip on the
   confirmation. Gold throughout: this flow has no error colour. */
.work__thumbitem--failed { border: 2px solid var(--gold); }
.work__thumbnote {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 11.5px; color: var(--pc-muted, #5C6B61); text-align: center; padding: 0 8px;
}
.work__thumbfail {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: rgba(201,162,39,.95); color: var(--green-deep);
  font-size: 10.5px; font-weight: 600; line-height: 1.3; padding: 5px 6px; text-align: center;
}
.work__retake {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  min-height: 44px; padding: 0 14px; border-radius: 4px; border: 0;
  background: var(--gold-light); color: var(--green-deep);
  font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
}

.work__received {
  display: flex; gap: 8px; list-style: none; margin: 4px 0 0; padding: 0;
  justify-content: center; flex-wrap: wrap;
}
.work__receiveditem { position: relative; width: 74px; height: 96px; }
.work__receivedimg {
  width: 100%; height: 100%; object-fit: cover; display: block;
  border-radius: 4px; background: var(--off-white);
  border: 1px solid rgba(227,181,58,.45);
}
.work__receivednum {
  position: absolute; left: 4px; bottom: 4px;
  padding: 1px 6px; border-radius: 100px; background: rgba(20,55,42,.85);
  font-size: 9.5px; font-weight: 700; color: var(--off-white);
}
.work__receivedcap {
  margin: 0; font-size: 12.5px; line-height: 1.5; color: rgba(255,255,255,.6);
}

/* "Already sent" on the capture screen, shown when reopening a link that has
   pages against it. Sits above the viewfinder so it is the first thing read. */
.work__already {
  margin: 0 22px; padding: 12px 14px;
  background: rgba(253,252,249,.07); border: 1px solid rgba(227,181,58,.28);
  border-radius: 6px; display: flex; flex-direction: column; gap: 8px;
}
.work__alreadylabel {
  font-size: 9.5px; font-weight: 600; letter-spacing: .14em; color: var(--gold-light);
}
/* A SCROLLER, NOT A WRAP. Wrapping was fine at 46px and is not at 84px, and the
   thumbnails must not shrink as more pages arrive: a student who has sent five
   pages needs to recognise them, and five squeezed into one row is five things
   nobody can tell apart. Four fit; the fifth is a swipe away. */
.work__alreadylist {
  display: flex; gap: 8px; list-style: none; margin: 0; padding: 0;
  flex-wrap: nowrap; overflow-x: auto;
}
/* 84x112 is 3:4, the same sheet ratio as the page graphic. flex: none because
   in a nowrap row a flex item shrinks by default, which is exactly the
   squeezing this is meant to prevent. */
.work__alreadyitem { width: 84px; height: 112px; flex: none; }
.work__alreadyimg {
  width: 100%; height: 100%; object-fit: cover; display: block;
  border-radius: 4px; background: var(--off-white);
  border: 1px solid rgba(227,181,58,.35);
}
.work__alreadynote { margin: 0; font-size: 12px; line-height: 1.5; color: rgba(255,255,255,.6); }
.txr__hint { margin: 4px 0 0; font-size: 12px; color: var(--muted); }
/* THE RECORD'S NUMBER, SHOWN RATHER THAN OFFERED FOR EDIT. It is the value of a
   field that lives in the Student Information group, so this reads at the size
   a value reads at and not at the size of a label. */
.txr__mobile { margin: 0; font-size: 15px; color: var(--ink); }
/* THE ADD MODAL'S ECHO, BEFORE A NUMBER IS TYPED. It reads "Typed in Student
   Information" rather than sitting empty, because an empty line here looks like
   a number that failed to load. --muted rather than --faint: this is a sentence
   somebody is meant to read, and --faint is 3.06:1 on this card. */
.txr__mobile--empty { color: var(--muted); font-size: 13.5px; }

/* ── SUBMISSIONS LIBRARY (S3 to S10) ──────────────────────────────────────────

   DESKTOP ONLY, per the brief, and deliberately not half-adapted for a phone.
   The one media query at the end narrows the grid rather than restacking into a
   mobile layout: a bad phone layout is worse than an honest desktop one that
   admits it wants a wider window.

   NO NEW COLOURS, RADII OR TYPE SIZES. Every value below is already in
   handoff/design-tokens.md and almost all of them resolve through the :root
   tokens at the top of this file. The three literals that are not tokens
   (#14372A on the photo ground, and the two gold alphas on the suggested chip)
   are values the token inventory already lists.

   NO RED. The portal has no error colour and this feature adds none. A failed
   image, a failed tagging pass and an empty result all use gold or neutral.  */

.page--library { max-width: 1280px; }

.lib__crumbs { font-size: 11.5px; color: var(--faint); margin-bottom: 14px; }
.lib__crumbs a { color: var(--muted); }
.lib__crumbs a:hover { color: var(--green-btn); }
.lib__dot { color: var(--faint); }

.lib__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap;
}
.lib__headlinks { display: flex; align-items: center; gap: 10px; }
.lib__headlink {
  padding: 8px 14px; background: #fff; border: 1px solid rgba(27,67,50,.16);
  border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: var(--green-btn); white-space: nowrap; cursor: pointer; transition: .15s;
}
.lib__headlink:hover { border-color: var(--gold); color: var(--gold-dark); }
.lib__headlink--btn { font: inherit; font-size: 12px; font-weight: 600; }
.lib__headlink--on { background: var(--green-btn); border-color: var(--gold); color: #fff; }

/* ── the filter bar ──────────────────────────────────────────────────────── */

.lib__filters { margin-top: 20px; }
.lib__searchrow { display: flex; gap: 8px; }
.lib__search {
  flex: 1 1 auto; padding: 11px 14px; background: #fff;
  border: 1px solid var(--line); border-radius: 4px;
  font-family: var(--sans); font-size: 16px; color: var(--body);
}
.lib__search::placeholder { color: #B7BEB8; }
.lib__search:focus {
  outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(201,162,39,.15);
}
.lib__searchbtn, .lib__apply {
  padding: 11px 20px; background: var(--green-btn); border: 1.5px solid var(--gold);
  border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: #fff; letter-spacing: .04em; cursor: pointer; transition: background .15s;
}
.lib__searchbtn:hover, .lib__apply:hover { background: var(--green-deep); }

.lib__filterrow {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px;
}
.lib__filterlabel { margin-right: 2px; }
.lib__select, .lib__date {
  padding: 7px 10px; background: #fff; border: 1px solid rgba(27,67,50,.2);
  border-radius: 4px; font-family: var(--sans); font-size: 16px; color: var(--body);
  max-width: 260px;
}
.lib__select:focus, .lib__date:focus { outline: none; border-color: var(--gold); }
.lib__dates { display: inline-flex; align-items: center; }
.lib__datelabel {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted);
}
.lib__dateto { color: var(--faint); font-size: 11.5px; }

/* ── subject buckets ─────────────────────────────────────────────────────── */

.lib__sectionhead {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-top: 30px;
}
.lib__browseall {
  font-size: 11.5px; font-weight: 600; color: var(--green-btn);
  border-bottom: 1px solid rgba(201,162,39,.55); padding-bottom: 1px;
}
.lib__browseall:hover { color: var(--gold-dark); }
.lib__note { margin: 8px 0 0; font-size: 12px; line-height: 1.6; color: var(--faint); max-width: 720px; }

.lib__buckets {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
  gap: 14px; margin-top: 16px;
}
.lib__bucket {
  display: flex; flex-direction: column; background: #fff;
  border: 1px solid var(--line); border-radius: 6px; padding: 18px 20px;
  transition: border-color .15s, box-shadow .15s;
}
.lib__bucket:hover { border-color: var(--gold); box-shadow: 0 8px 22px rgba(201,162,39,.12); }
/* UNFILED TAKES THE PORTAL'S GOLD LEFT EDGE, the same mark the queue puts on a
   row that needs a person. It is a state to clear, not a subject to browse. */
.lib__bucket--unfiled { border-left: 3px solid var(--gold); }
.lib__bucket-title { font-family: var(--serif); font-size: 18px; color: var(--ink); line-height: 1.3; }
.lib__bucket-flag { margin-top: 4px; }
.lib__bucket-count { font-family: var(--serif); font-size: 32px; line-height: 1; color: var(--green-deep); margin-top: 10px; }
.lib__bucket-unit { font-size: 9.5px; font-weight: 600; color: var(--faint); letter-spacing: .12em; text-transform: uppercase; margin-top: 4px; }
.lib__bucket-meta { font-size: 11.5px; color: var(--muted); margin-top: 6px; }

/* ── needs attention ─────────────────────────────────────────────────────── */

.lib__attention {
  margin-top: 28px; background: var(--green-deep); border-radius: 8px;
  padding: 18px 22px;
}
.lib__attention-label { color: var(--gold-light); }
.lib__attention-row { display: flex; gap: 34px; flex-wrap: wrap; margin-top: 10px; }
.lib__attention-item { display: flex; align-items: baseline; gap: 8px; }
.lib__attention-n { font-family: var(--serif); font-size: 26px; color: var(--gold-light); line-height: 1; }
.lib__attention-t { font-size: 12px; color: rgba(255,255,255,.75); }
.lib__attention-item:hover .lib__attention-t { color: #fff; }

/* ── applied filter chips ────────────────────────────────────────────────── */

.lib__applied { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.lib__applied-chip {
  display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px;
  border-radius: 100px; background: rgba(27,67,50,.06);
  border: 1px solid rgba(27,67,50,.14);
  font-size: 11px; font-weight: 600; color: var(--green-deep);
}
.lib__applied-chip:hover { border-color: var(--gold); color: var(--gold-dark); }
.lib__x { font-size: 13px; line-height: 1; color: var(--faint); }
.lib__clearall { font-size: 11.5px; font-weight: 600; color: var(--muted); }
.lib__clearall:hover { color: var(--gold-dark); }

/* ── the grid ────────────────────────────────────────────────────────────── */

.lib__group-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin: 28px 0 12px;
  border-bottom: 1px solid var(--hair); padding-bottom: 8px;
}
.lib__group-all {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--sans); font-size: 11.5px; font-weight: 600; color: var(--green-btn);
}
.lib__group-all:hover { color: var(--gold-dark); }

.lib__grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
  gap: 16px; margin-top: 14px;
}
.lib__tile {
  position: relative; background: #fff; border: 1px solid var(--line);
  border-radius: 6px; overflow: hidden; transition: border-color .15s, box-shadow .15s;
}
.lib__tile:hover { border-color: rgba(27,67,50,.25); }
/* SELECTED: 2px gold, the gold active shadow, and a filled box. */
.lib__tile--on { border: 2px solid var(--gold); box-shadow: 0 8px 22px rgba(201,162,39,.12); }

.lib__pick { position: absolute; top: 8px; left: 8px; z-index: 2; display: none; }
.lib__tile--picking .lib__pick { display: block; }
.lib__pick-box { width: 18px; height: 18px; accent-color: var(--green-btn); cursor: pointer; }

/* THE PHOTOGRAPH. contain, never cover: this is a student's handwriting and
   cropping it to fill the tile can cut off the working that is the point of the
   page. The ground is the portal's deep green, matching how the capture and
   confirmation screens already letterbox a student photo. */
.lib__shot {
  position: relative; display: block; aspect-ratio: 3 / 4;
  background: #14372A; overflow: hidden;
}
.lib__img { width: 100%; height: 100%; object-fit: contain; display: block; }
.lib__broken {
  display: none; position: absolute; inset: 0;
  flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  padding: 14px; text-align: center;
}
/* A FAILED IMAGE IS NOT A FAILED PAGE. The tile stays selectable and everything
   read off the page still opens. Gold and neutral, no error colour. */
.lib__shot--broken .lib__img { visibility: hidden; }
.lib__shot--broken .lib__broken { display: flex; }
.lib__broken-text {
  font-size: 9.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(227,181,58,.95);
}
.lib__retry {
  padding: 5px 12px; background: transparent; border: 1px solid rgba(227,181,58,.5);
  border-radius: 100px; font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--gold-light); cursor: pointer;
}
.lib__retry:hover { background: rgba(227,181,58,.12); }

.lib__open { display: block; padding: 11px 12px 0; }
.lib__who { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink); }
.lib__meta { display: block; font-size: 11px; color: var(--faint); margin-top: 3px; line-height: 1.5; }
.lib__pageno { font-weight: 600; color: var(--muted); }

.lib__tags { display: flex; flex-wrap: wrap; gap: 5px; padding: 9px 12px 12px; }

/* S7 TAG CHIP STATES.
   Confirmed is a filled neutral chip with a check.
   Suggested is a dashed gold outline with a plus. The dash reads as provisional
   without introducing a colour, and does not collide with the gold FILL the
   portal already uses to mean "this row needs action". */
.lib__chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 9px; border-radius: 100px;
  font-family: var(--sans); font-size: 9px; font-weight: 600; letter-spacing: .06em;
  white-space: nowrap; line-height: 1.4;
}
.lib__chip--on {
  background: rgba(27,67,50,.07); border: 1px solid rgba(27,67,50,.12); color: var(--green-deep);
}
.lib__chip--on::before { content: '\2713'; font-size: 9px; color: var(--green-btn); }
.lib__chip--sug {
  background: transparent; border: 1px dashed rgba(168,132,42,.45);
  color: var(--gold-dark); cursor: pointer; transition: background .15s, border-color .15s;
}
.lib__chip--sug:hover { background: rgba(201,162,39,.12); border-color: var(--gold); }
.lib__plus { font-size: 11px; line-height: 1; }
/* Awaiting tagging / Tagging failed. Neutral, not an error: the page is fine. */
.lib__chip--state {
  background: rgba(201,162,39,.12); border: 1px solid rgba(168,132,42,.35); color: var(--gold-dark);
}

/* ── paging ──────────────────────────────────────────────────────────────── */

.lib__more { display: flex; align-items: center; gap: 16px; margin-top: 26px; }
.lib__morebtn {
  padding: 10px 20px; background: #fff; border: 1px solid rgba(27,67,50,.25);
  border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: var(--green-btn); cursor: pointer; transition: .15s;
}
.lib__morebtn:hover { border-color: var(--gold); color: var(--gold-dark); }
.lib__morebtn[disabled] { opacity: .55; cursor: default; }
/* THE COUNT IS ALWAYS POSITION OF TOTAL. A grid that said "48" would tell the
   reader the archive is small, which is the one thing it must never do. */
.lib__showing { font-size: 11.5px; color: var(--faint); }

/* ── the selection bar (S5) ──────────────────────────────────────────────── */

.lib__bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 14px 26px; background: var(--green-deep);
  box-shadow: 0 -18px 44px rgba(20,55,42,.2);
}
.lib__bar[hidden] { display: none; }
.lib__bar-count { font-size: 12.5px; color: rgba(255,255,255,.75); }
.lib__bar-count b { font-family: var(--serif); font-size: 20px; color: var(--gold-light); }
.lib__bar-actions { display: flex; align-items: center; gap: 10px; }
.lib__bar-btn {
  padding: 9px 16px; background: transparent; border: 1px solid rgba(227,181,58,.45);
  border-radius: 4px; font-family: var(--sans); font-size: 12px; font-weight: 600;
  color: var(--gold-light); cursor: pointer; transition: .15s;
}
.lib__bar-btn:hover { background: rgba(227,181,58,.12); border-color: var(--gold-light); }
.lib__bar-btn--go { background: var(--gold); border-color: var(--gold); color: var(--green-deep); }
.lib__bar-btn--go:hover { background: var(--gold-light); border-color: var(--gold-light); }

/* PERMANENT DELETE. Set apart by a rule and a quieter treatment, never by a
   colour: this portal has no error colour and inventing a red for one button
   would make it the loudest thing on a screen full of student work. The weight
   it needs comes from the confirm dialog, which is where the decision is
   actually made. */
.lib__bar-btn--del {
  margin-left: 6px; padding-left: 22px; border-color: rgba(255,255,255,.28);
  color: rgba(255,255,255,.82); position: relative;
}
.lib__bar-btn--del::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 1px; height: 22px; background: rgba(255,255,255,.22);
}
.lib__bar-btn--del:hover { background: rgba(253,252,249,.1); border-color: rgba(255,255,255,.6); color: #fff; }

/* THE DELETE CONFIRM reuses .sdlg, the dialog this product already has, rather
   than introducing a second one that would drift from it. The only difference
   it needs is the primary button: a delete that cannot be undone should not
   wear the same green as Approve and send.

   INK, NOT RED. It is the darkest thing in the dialog, which is enough weight
   for an irreversible action, and it keeps the promise that this portal has no
   error colour. */
.sdlg__go--ink { background: var(--ink); border-color: var(--ink); color: var(--off-white); }
.sdlg__go--ink:hover { background: #000; border-color: #000; }
.sdlg__go:disabled { opacity: .55; cursor: default; }

/* The same action on the single page viewer, in that panel's link idiom. */
.lib__headlink--del { border-color: rgba(27,67,50,.3); color: var(--muted); }
.lib__headlink--del:hover { border-color: var(--ink); color: var(--ink); }
.lib__bar-clear {
  background: none; border: 0; padding: 4px 6px; cursor: pointer;
  font-family: var(--sans); font-size: 11.5px; color: rgba(255,255,255,.6);
}
.lib__bar-clear:hover { color: #fff; }

/* The bar is fixed over the page, so the last row of tiles needs somewhere to
   go. Applied only while a selection exists, so an unselected grid does not
   carry dead space at the bottom. */
.page--library.is-selecting { padding-bottom: 96px; }

/* THE DOWNLOAD STATES. Started says the build is running and that you can keep
   browsing, which is the whole reason the zip is a job rather than a streamed
   response. Completed names the file and offers Save. Sits above the selection
   bar rather than replacing it, because the selection is still live. */
.lib__zip {
  position: fixed; left: 26px; bottom: 84px; z-index: 45; max-width: 460px;
  display: flex; align-items: center; gap: 11px;
  padding: 13px 16px; background: var(--green-deep); border-radius: 6px;
  border: 1px solid rgba(227,181,58,.28);
  box-shadow: 0 18px 44px rgba(20,55,42,.2);
  font-size: 12.5px; line-height: 1.55; color: rgba(255,255,255,.82);
}
.lib__zip--done { border-color: rgba(227,181,58,.5); }
.lib__zip-spin {
  flex: 0 0 auto; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(227,181,58,.28); border-top-color: var(--gold-light);
  animation: lib-spin .8s linear infinite;
}
@keyframes lib-spin { to { transform: rotate(360deg); } }
.lib__zip-save {
  flex: 0 0 auto; padding: 7px 15px; background: var(--gold); border-radius: 4px;
  font-size: 11.5px; font-weight: 600; color: var(--green-deep); text-decoration: none;
}
.lib__zip-save:hover { background: var(--gold-light); }
.lib__zip-close {
  flex: 0 0 auto; background: none; border: 0; padding: 0 2px; cursor: pointer;
  font-size: 16px; line-height: 1; color: rgba(255,255,255,.6);
}
.lib__zip-close:hover { color: #fff; }
/* No animation for a reader who asked for none. */
@media (prefers-reduced-motion: reduce) {
  .lib__zip-spin { animation: none; }
}

/* ── ADD TO COLLECTION popover (S8), shared by the grid and the viewer ─────── */

.lib__pop {
  position: absolute; bottom: calc(100% + 10px); left: 0; z-index: 50; width: 264px;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 18px 44px rgba(20,55,42,.2); padding: 12px; text-align: left;
}
.lib__pop-head {
  font-size: 9.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 8px;
}
.lib__pop-list { max-height: 210px; overflow-y: auto; }
.lib__pop-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 8px 9px; background: none; border: 0; border-radius: 4px;
  font-family: var(--sans); font-size: 12.5px; color: var(--body);
  text-align: left; cursor: pointer;
}
.lib__pop-row:hover { background: rgba(27,67,50,.06); }
.lib__pop-n { font-size: 11px; color: var(--faint); }
.lib__pop-none { margin: 0 0 6px; font-size: 12px; color: var(--faint); }
.lib__pop-new { display: flex; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--hair); }
.lib__pop-new input {
  flex: 1 1 auto; min-width: 0; padding: 7px 9px; background: #fff;
  border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  font-family: var(--sans); font-size: 16px; color: var(--body);
}
.lib__pop-new button {
  flex: 0 0 auto; padding: 7px 11px; background: var(--green-btn);
  border: 1px solid var(--gold); border-radius: 4px;
  font-family: var(--sans); font-size: 11px; font-weight: 600; color: #fff; cursor: pointer;
}
/* The bar is the popover's positioning context. */
.lib__bar-actions { position: relative; }

/* ── S8 COLLECTIONS ───────────────────────────────────────────────────────── */

.lib__newcoll { display: flex; gap: 8px; margin-top: 20px; max-width: 520px; }
.lib__newcoll-name { flex: 1 1 auto; }
.lib__colltable { margin-top: 20px; }
.lib__collrow {
  display: grid; grid-template-columns: 2fr 80px 90px 1fr auto; gap: 14px; align-items: center;
}
.lib__collname a { font-size: 13.5px; font-weight: 600; color: var(--green-btn); }
.lib__collname a:hover { color: var(--gold-dark); }
.lib__collwhen { font-size: 12px; color: var(--faint); }
.lib__collacts { display: flex; gap: 6px; justify-content: flex-end; }

/* ── S9 TAXONOMY ──────────────────────────────────────────────────────────── */

.lib__tax { display: flex; gap: 22px; margin-top: 22px; align-items: flex-start; }
.lib__taxsubs {
  flex: 0 0 236px; background: #fff; border: 1px solid var(--line); border-radius: 6px;
  overflow: hidden; max-height: 620px; overflow-y: auto;
}
.lib__taxsub {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--hair); font-size: 12.5px; color: var(--body);
}
.lib__taxsub:last-child { border-bottom: 0; }
.lib__taxsub:hover { background: rgba(27,67,50,.04); }
.lib__taxsub--on { background: rgba(201,162,39,.12); border-left: 3px solid var(--gold); }
.lib__taxsub-n { font-size: 11px; color: var(--faint); }
.lib__taxtopics { flex: 1 1 auto; min-width: 0; }
.lib__taxhead {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin-bottom: 14px;
}
.lib__taxtitle { font-family: var(--serif); font-size: 22px; font-weight: 400; color: var(--ink); margin: 0; }
.lib__taxadd { display: flex; gap: 8px; }
.lib__taxadd-name { width: 240px; }
.lib__taxrow { display: grid; grid-template-columns: 3fr 90px 120px auto; gap: 14px; align-items: center; }
.lib__taxname { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink); }
/* A retired topic is dimmed, never hidden: it keeps every page tagged with it
   and a person has to be able to find it again to restore it. */
.lib__taxrow--off .lib__taxname { color: var(--faint); }
.lib__taxacts { display: flex; gap: 6px; justify-content: flex-end; }

@media (max-width: 900px) {
  .lib__tax { flex-direction: column; }
  .lib__taxsubs { flex: 1 1 auto; width: 100%; max-height: 260px; }
}

@media (max-width: 900px) {
  /* DESKTOP ONLY, and it says so rather than pretending. The grid narrows and
     the filters wrap; nothing is restacked into a phone layout. */
  .lib__grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  .lib__buckets { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
}

/* ── S6 PAGE DETAIL ───────────────────────────────────────────────────────────

   FULL SCREEN, and a real page rather than an overlay. The design considered an
   overlay and dropped it: the metadata is the reason for opening a page, so the
   panel is always open and never behind a toggle.                            */

.libview__body { background: var(--green-deep); margin: 0; }
/* ⚠ A GREEN-GROUNDED FLEX SHELL WITH A FIXED HEIGHT AND A SEARCH FIELD, which
   is every ingredient of the dead band at once: a fixed viewport height is what
   turns a stale resolution into a strip of ground rather than a page that is
   merely short. svh does not move for the keyboard, so there is no stale
   resolution to have. See body:has(> .tabbar). */
.libview { display: flex; flex-direction: column; height: 100dvh; height: 100svh; }

.libview__bar {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 12px 20px; background: var(--green-deep);
  border-bottom: 1px solid rgba(255,255,255,.14); flex-shrink: 0;
}
.libview__close {
  font-size: 12.5px; font-weight: 600; color: rgba(255,255,255,.75);
  text-decoration: none; white-space: nowrap;
}
.libview__close:hover { color: #fff; }
.libview__where { font-size: 12.5px; color: rgba(255,255,255,.6); }
.libview__where .lib__dot { color: rgba(255,255,255,.35); }
.libview__nav { display: flex; align-items: center; gap: 12px; }
/* THE COUNT IS POSITION OF TOTAL. 14 of 614, never 14. */
.libview__count { font-size: 12px; color: rgba(255,255,255,.72); white-space: nowrap; }
.libview__count--loose { color: rgba(255,255,255,.5); font-style: italic; }
.libview__step {
  width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255,255,255,.22); border-radius: 4px;
  color: var(--gold-light); font-size: 17px; text-decoration: none; transition: .15s;
}
.libview__step:hover { background: rgba(227,181,58,.12); border-color: rgba(227,181,58,.5); }
.libview__step--off { opacity: .3; pointer-events: none; }

.libview__body-row { display: flex; flex: 1 1 auto; min-height: 0; }

/* the photograph */
.libview__stage {
  flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; padding: 18px 20px;
}
.libview__shot {
  position: relative; flex: 1 1 auto; min-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.libview__img { max-width: 100%; max-height: 100%; object-fit: contain; }
.libview__broken {
  display: none; flex-direction: column; align-items: center; gap: 10px;
  text-align: center; max-width: 380px;
}
/* A FAILED IMAGE IS NOT A FAILED PAGE. Everything read off it still opens. */
.libview__shot--broken .libview__img { display: none; }
.libview__shot--broken .libview__broken { display: flex; }
.libview__broken-note { margin: 0; font-size: 12px; line-height: 1.6; color: rgba(255,255,255,.6); }

/* the sibling strip */
.libview__strip { flex-shrink: 0; padding-top: 14px; }
.libview__strip-label { color: rgba(227,181,58,.6); }
.libview__strip-row { display: flex; gap: 8px; margin-top: 8px; overflow-x: auto; padding-bottom: 4px; }
.libview__sib {
  position: relative; flex: 0 0 auto; width: 52px; height: 68px;
  border: 1px solid rgba(255,255,255,.22); border-radius: 3px; overflow: hidden;
  background: #14372A; transition: .15s;
}
.libview__sib img { width: 100%; height: 100%; object-fit: contain; display: block; }
.libview__sib:hover { border-color: rgba(227,181,58,.5); }
.libview__sib--on { border: 2px solid var(--gold); }
.libview__sib-n {
  position: absolute; bottom: 0; right: 0; padding: 1px 4px;
  background: rgba(20,55,42,.85); font-size: 9px; font-weight: 700; color: var(--gold-light);
}

/* THE PANEL. Fixed at 404px, per the design, and always open. */
.libview__panel {
  flex: 0 0 404px; width: 404px; background: var(--cream);
  overflow-y: auto; padding: 20px 22px 40px;
}
.libview__panel-head { border-bottom: 1px solid var(--hair); padding-bottom: 12px; }
.libview__who { font-family: var(--serif); font-size: 22px; font-weight: 400; color: var(--ink); margin: 4px 0 0; }

.libview__meta { margin: 14px 0 0; }
.libview__meta > div {
  display: grid; grid-template-columns: 92px 1fr; gap: 10px;
  padding: 6px 0; font-size: 12.5px; align-items: baseline;
}
.libview__meta dt { color: var(--faint); }
.libview__meta dd { margin: 0; color: var(--body); }
/* "read by the pass" and "set by hand": provenance on an extracted value, so a
   reader knows whether a person or the model put it there. */
.libview__read {
  display: inline-block; margin-left: 6px; font-size: 9px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--gold-dark);
}
.libview__none { color: var(--faint); font-style: italic; }
.libview__mini {
  margin-left: 8px; padding: 3px 9px; background: transparent;
  border: 1px solid rgba(168,132,42,.45); border-radius: 100px;
  font-family: var(--sans); font-size: 10px; font-weight: 600; letter-spacing: .04em;
  color: var(--gold-dark); cursor: pointer; transition: .15s;
}
.libview__mini:hover { background: rgba(201,162,39,.12); border-color: var(--gold); }
.libview__mini[disabled] { opacity: .5; cursor: default; }
.libview__typepick, .libview__select {
  margin-top: 6px; padding: 6px 9px; background: #fff;
  border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  font-family: var(--sans); font-size: 16px; color: var(--body); max-width: 100%;
}

.libview__block { margin-top: 22px; }
.libview__block-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.libview__note {
  margin: 8px 0 0; padding: 11px 13px; background: var(--off-white);
  border-left: 3px solid rgba(201,162,39,.55); border-radius: 0 4px 4px 0;
  font-size: 12.5px; line-height: 1.65; color: var(--body);
}
.libview__state { margin-top: 10px; font-size: 12px; }
.libview__why { color: var(--gold-dark); font-size: 11.5px; }

/* the tag chips, S7's two states plus the LOW marker */
.libview__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.libview__tag {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: 100px;
  font-family: var(--sans); font-size: 10.5px; font-weight: 600; line-height: 1.4;
}
.libview__tag--on {
  background: rgba(27,67,50,.07); border: 1px solid rgba(27,67,50,.12); color: var(--green-deep);
}
.libview__tag--on::before { content: '\2713'; color: var(--green-btn); }
.libview__tag--sug {
  background: transparent; border: 1px dashed rgba(168,132,42,.45); color: var(--gold-dark);
}
.libview__tag-sub { font-weight: 400; opacity: .65; }
/* CONFIDENCE IS NEVER A NUMBER. This marker, in the viewer only, is the whole of
   how a low suggestion is surfaced. */
.libview__low {
  padding: 1px 5px; border-radius: 100px; background: rgba(201,162,39,.18);
  font-size: 8px; font-weight: 700; letter-spacing: .1em; color: var(--gold-dark);
}
.libview__tag-act {
  background: none; border: 0; padding: 0 1px; cursor: pointer;
  font-family: var(--sans); font-size: 13px; line-height: 1; color: inherit; opacity: .6;
}
.libview__tag-act:hover { opacity: 1; }
.libview__tag-act[disabled] { opacity: .3; cursor: default; }

.libview__tagadd { margin-top: 10px; }
.libview__subjectrow {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--hair);
}
.libview__wrong { font-size: 11.5px; color: var(--faint); }

.libview__transcript {
  margin: 8px 0 0; font-size: 12.5px; line-height: 1.75; color: var(--body);
  white-space: pre-wrap;
}

/* position: relative so the Add to collection popover anchors to these buttons
   rather than to the page. */
.libview__actions { display: flex; gap: 8px; margin-top: 24px; position: relative; }
/* In the viewer the actions sit at the BOTTOM of a scrolling panel, so the
   popover opens upward from them like it does above the selection bar. */
.lib__pop--up { bottom: calc(100% + 10px); }

@media (max-width: 900px) {
  /* THE PANEL IS 404px BY DESIGN and the library is desktop only, but a 404px
     fixed column on a 390px phone overflows the viewport sideways, which is the
     exact defect test/mobile-overflow.test.js exists to catch. Somebody will open
     a page link on their phone whatever the brief says.

     So it stacks rather than overflowing. This is NOT a mobile layout and does
     not pretend to be one: the image sits above the panel, the panel takes the
     full width, and the page scrolls. It is the difference between a screen that
     is cramped and a screen that is broken. */
  .libview { height: auto; min-height: 100vh; }
  .libview__body-row { flex-direction: column; }
  .libview__panel { flex: 1 1 auto; width: auto; max-width: 100%; overflow-y: visible; }
  .libview__stage { height: 70vh; }
}

/* ══ ADMIN TOOLS: THE STICKY SECTION RAIL ══════════════════════════════════
   design_handoff_admin_rail, option 1a. Mobile only; 1b and 1c were rejected.

   Written at the END of the file on purpose. Every selector here is new, so
   nothing can be outranked, and the two times a mobile fix on this page did
   nothing on the phone it was because an override sat EARLIER in the file than
   the rule it meant to beat (the day pills, then .tagform__row). A new block
   at the end cannot repeat that. */
/* ══ THE STICKY HEADER UNIT ═════════════════════════════════════════════════
 *
 * ⚠ THE HEADING IS INSIDE THE STICKY BLOCK, WHICH REVERSES A RECORDED RULING.
 * The note that stood here said "Admin tools" stays in normal flow and scrolls
 * away, so only the chip row was sticky and the rail was mobile only. Trey has
 * ruled the other way: the heading and the rail are ONE unit that persists
 * together through the whole scroll, on BOTH surfaces, and the heading never
 * detaches leaving the rail floating alone.
 *
 * ⚠ ONE STICKY ELEMENT, NOT TWO. The failure being fixed is two separately
 * sticky boxes: the rail pinned at the top while the heading scrolled out from
 * under it, which is what "the heading detaches leaving the rail floating"
 * describes. Only .shead is sticky; the h1 and the nav are its children and
 * cannot pin independently of it.
 *
 * ⚠ AND IT IS OPAQUE. A sticky bar that content scrolls THROUGH rather than
 * UNDER reads as a rendering fault. The background is the page's own, and the
 * z-index puts it above the cards without competing with the nav or a sheet.
 */
.shead {
  position: sticky; top: 0; z-index: 30;
  /* ⚠ OPAQUE, AND WRITTEN AS THE LITERAL. --cream IS #FAF7F0, but a custom
     property that is redefined or unset anywhere up the tree resolves to
     nothing and paints TRANSPARENT, which is the one failure mode this band
     cannot have: any unpainted pixel shows page content scrolling through it.
     A background nobody can accidentally unset is worth more here than a token. */
  background: #FAF7F0;
  /* ⚠ THE TOP PULL CLOSES THE SEAM, and the ::before below paints it. The pull
     covers the scroll container's own top padding, which a sticky child would
     otherwise leave above itself with page content scrolling through it. The
     paint is separate because a pull alone cannot be trusted to equal the
     container's SIDE padding, and when it did not, ten pixels down each edge
     went unpainted. */
  margin: -28px -26px 0; padding: 28px 26px 0;
  border-bottom: 1px solid rgba(27, 67, 50, .14);
  box-shadow: 0 4px 12px rgba(20, 55, 42, .05);
}

/* ⚠ THE GROUND IS PAINTED BY THIS, NOT BY THE NEGATIVE MARGIN. A margin pull
   only works if it is EXACTLY the container's side padding, and it was not:
   measured at 390 the unit spanned x=10 to x=380 against a 390 viewport, so
   there were ten unpainted pixels down each edge and page content showed
   through them. The pull was guessing a number that lives in another rule, and
   guessing wrong is invisible until somebody looks at a phone.
   
   This paints an opaque ground the full width of the VIEWPORT, centred on the
   unit, so it cannot be wrong about the container's padding because it never
   reads it. Behind the heading and the rail, inside the sticky element, so it
   travels with them and covers every pixel between the green bar and the bottom
   of the chip row at every scroll position.
   
   Not a mask and not a gradient: one flat colour, fully opaque. */
.shead::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: 50%; transform: translateX(-50%);
  width: 100vw;
  background: #FAF7F0;
  z-index: -1;
}
.shead__top { display: flex; align-items: baseline; gap: 10px; padding: 11px 0 8px; }
.shead__top .h1 { margin: 0; }
/* The mock's contextual figure, on the right of the heading, changing with the
   section. Its own colour so it reads as a note rather than as a second title. */
.shead__note { margin-left: auto; font-size: 11.5px; color: var(--muted); white-space: nowrap; }

/* ⚠ THE RAIL IS A PHONE COMPONENT, AND SHOWING IT ON DESKTOP WAS A REGRESSION.
   I set this to `display: block` to satisfy a ruling that the heading and rail
   are one sticky unit on both surfaces. Every chip STYLE, though, lives in the
   `@media (max-width: 760px)` block below: .srail__row, .srail__item,
   .srail__chip and .srail__pill are all inside it. So the desktop got the
   unstyled <ul> and rendered eight raw bullet points under the heading.

   Checked against the drawing rather than guessed: Admin Tools Desktop.dc.html
   contains no rail at all. Not one of the eight chip labels appears in it, and
   there is no rail markup. What the desktop mock draws on that line is the
   heading with the four status pills beside it. So the rail is a phone
   component, it renders on the phone only, and the sticky unit on desktop is
   the heading and its pills.

   The chip styles stay where they are, since nothing outside the phone uses
   them. */
.srail { display: none; }

@media (max-width: 760px), (max-height: 499px) {
  /* The phone's own measurements for the same unit. */
  /* ⚠ THE RAIL IS NO LONGER STICKY ITSELF, AND THAT IS THE WHOLE FIX. It used
     to be its own sticky box with its own background, border and shadow, so
     while the page scrolled the rail pinned at the top and the heading scrolled
     out from under it: two sticky boxes, one of which was the unit's other
     half. Only .shead pins now, and this is an ordinary child of it.

     The display:block here is also what limits the rail to the phone: the base
     rule is display:none, and this is the only place that turns it back on. */
  .srail {
    display: block; position: static; z-index: auto;
    background: transparent; border-bottom: 0; box-shadow: none;
    margin: 0; padding-top: 0;
  }
  /* ⚠ THE PHONE'S EDGES, AND THE TOP PULL WITH THEM. This rule used to read
     `margin: 0 -16px`, which is (0,1,0) exactly like the base rule and declared
     LATER, so on the one surface the seam was reported on it reset margin-top
     and padding-top to zero and erased the pull completely. The base rule was
     correct and this rule was quietly deleting it. Scoped to .page--admin at
     (0,2,0) now so it cannot be beaten by a bare .shead rule written later.

     20px is the phone container's own top padding, so the unit covers exactly
     the strip that page content was scrolling through. */
  /* ⚠ THE SPACE IS REMOVED, NOT COVERED. The unit used to pull up over the
     scroll container's 20px top padding and repaint it, which closed the
     see-through but left the space there: the container still began 20px below
     the nav and the unit still had to reach back over it. Any device whose nav
     is taller than this one's, a notched phone with a safe-area inset among
     them, moves that boundary and the reach no longer lands.

     The container's top padding goes to zero on the phone instead, so the unit
     STARTS at the container's top edge, which is the nav's bottom edge. There is
     no between for anything to appear in, at any scroll position and whatever
     --sat resolves to.

     ⚠ THE PULL MUST GO WITH IT. A -20px margin against a container with no top
     padding would drag the unit up UNDER the nav. */
  .page--admin.page--admin { padding-top: 0; }
  .page--admin .shead {
    margin: 0 -16px; padding: 0 16px;
    background: #FAF7F0;
  }
  /* ⚠ 16px ABOVE THE HEADING. The history, because this has now been wrong in
     both directions: it began at 11px plus 20px of pull compensation, 31px of
     air over "Admin tools", which was far too much; it was cut to 11px and then
     to 6px, and at 6 the heading crowds the green nav bar it sits under.
     16px is the page's own side inset, so the room above the heading matches the
     room beside it rather than being a number picked to look right. The heading
     and the rail keep their own spacing exactly as it was; only the space above
     the heading changes, so the unit still moves as one piece. */
  .page--admin .shead__top { padding: 16px 0 8px; }
  .shead__top { padding: 11px 0 8px; }

  .srail__row {
    display: flex; gap: 7px; padding: 0 0 10px; margin: 0; list-style: none;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* ⚠ THE FADE IS GONE, AND IT WAS NOT A COSMETIC CHOICE TO KEEP. A static
       24px mask at the right edge sits over whatever is under it, INCLUDING the
       last chip once you have scrolled all the way to it: Pay rendered shaded
       and clipped at the exact moment you had reached it. A mask cannot know
       whether the row is scrolled to its end without script, so the honest fix
       is to stop hiding the row's edge at all. The row still scrolls, and the
       trailing pad below leaves the last chip clear of the edge. */
    scroll-padding-inline-end: 16px;
    padding-right: 16px;
  }
  .srail__row::-webkit-scrollbar { display: none; }

  /* THE LINK IS THE TAP TARGET, THE SPAN IS THE PILL. 34px of visible pill
     inside a 45px target: 5px above and 6px below. The handoff asks for both
     numbers and they cannot live on one box. */
  /* The li is the flex item and the thing that refuses to shrink; the anchor
     inside it is the tap target. */
  .srail__item { flex-shrink: 0; display: flex; scroll-snap-align: start; }
  .srail__chip {
    display: flex; align-items: center;
    padding: 5px 0 6px; text-decoration: none;
  }
  .srail__pill {
    display: inline-flex; align-items: center; min-height: 34px; padding: 0 13px;
    border: 1px solid rgba(27,67,50,.2); border-radius: 100px;
    background: transparent; color: #5C6B62;
    font-family: var(--sans); font-size: 12.5px; font-weight: 600;
    /* NEVER WRAP AND NEVER SQUASH. flex-shrink:0 above is what holds the label
       on one line at 320px; white-space is the belt to that braces. */
    white-space: nowrap;
  }
  .srail__chip[aria-current="true"] .srail__pill {
    background: #14372A; color: #FDFCF9; border-color: transparent;
  }
  .srail__chip:focus-visible { outline: 0; }
  .srail__chip:focus-visible .srail__pill { outline: 2px solid var(--gold); outline-offset: 2px; }

  /* A JUMP MUST NOT LAND UNDER THE RAIL. Measured, not guessed: the sticky chip
     row is 45px and the top bar is NOT fixed on this page (.topbar is static and
     the mobile chrome is the bottom .tabbar), so the rail is the only thing
     covering the target. 45px of rail plus 12px of air = 57px. */
  /* ⚠ THE RAIL'S EIGHT FIRST, then the targets an in-page link jumps to. A
     scroll margin on a target nothing renders is dead weight that reads as
     coverage, so the conditional sections keep theirs only while they still
     have anchors pointing at them: the status chips link to #stranded and
     #email-failures, and the settings strip links to all four of its cells. */
  #status, #tools, #announcements, #tutors-roster, #students-roster,
  #settings, #trust, #pay,
  #auto-send, #add-tutor, #add-student,
  #survey-requests, #unassigned, #sessions-to-book,
  #standard-rate, #school-year, #library-tagging, #notifications,
  #stranded, #email-failures {
    /* ⚠ 120px, MEASURED AGAINST THE UNIT THAT NOW COVERS THESE. This was 57px,
       computed for a sticky element that was the chip row alone: 45px of row
       plus a 1px border plus air. The heading is inside the sticky unit now, so
       the thing covering a jump target is 109px tall, and 57 would have dropped
       every anchor jump under it. 120 leaves 11px of air, the same margin the
       old number left over its own smaller bar. */
    scroll-margin-top: 120px;
  }
}


/* ══ NOTIFICATIONS CARD ═════════════════════════════════════════════════════
   The per-device push switch, on /admin and on the dashboard.

   NO RED. The design tokens have no error colour, and "off" is a state rather
   than a fault. Off is the faint track, on is filled, which is how every other
   switch in the portal reads.

   THE SWITCH IS THE ONE NEW RULE here; everything else reuses card, card--pad,
   card__blurb and push__note as they already were. */
.push__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px;
}
.push__headings { flex: 1; min-width: 0; }
.push__title {
  font-family: var(--serif); font-weight: 400; font-size: 18px;
  line-height: 1.25; color: var(--ink); margin: 0;
}

/* THE STATE WORD, and it renders in every one of the eight states including the
   three that have no control. The switch alone cannot carry the state: colour
   is not available to everyone, and half the states have nothing to colour. */
.push__state {
  display: block; margin-top: 4px;
  font-size: 12.5px; font-weight: 600; color: var(--faint);
}
.push__state--on      { color: var(--green-btn); }
.push__state--off     { color: var(--gold-dark); }
.push__state--neutral { color: var(--faint); }

/* THE CONTROL SLOT. Three children, at most one shown, chosen in one write by
   push-card.js's setState. */
.push__controls { flex: none; display: flex; align-items: center; }
.push__controls > [hidden] { display: none; }

/* 44px OF TAP TARGET AROUND A 31px SWITCH. The padding is on the button and the
   colour is on the track inside it, so the thing you can hit is bigger than the
   thing you can see. */
.push__switch,
.push__pad {
  display: inline-flex; align-items: center;
  background: none; border: 0; padding: 7px 0; margin: 0;
}
.push__switch { cursor: pointer; }
/* ⚠ [hidden] LOSES TO THE AUTHOR display ABOVE. Both switches render with a bare
   `hidden` until the script decides which one this browser should show, and
   without this both painted. Same defect as the booking sheet, found by the same
   sweep. */
.push__switch[hidden] { display: none !important; }

.push__track {
  width: 52px; height: 31px; border-radius: 100px; box-sizing: border-box;
  background: rgba(27, 67, 50, .16);
  border: 1px solid rgba(27, 67, 50, .2);
  display: flex; align-items: center; justify-content: flex-start;
  padding: 0 3px;
  transition: background-color .12s ease;
}
.push__track--on {
  background: var(--green-btn); border-color: var(--green-btn);
  justify-content: flex-end;
}
.push__knob {
  width: 25px; height: 25px; border-radius: 50%;
  background: var(--off-white); flex: none;
  box-shadow: 0 1px 3px rgba(20, 55, 42, .25);
}

/* STATE 1. A dashed track while the browser is being asked, so the row does not
   change height between the first paint and the answer. */
.push__track--checking {
  background: rgba(27, 67, 50, .1);
  border-style: dashed; border-color: rgba(27, 67, 50, .22);
}
.push__track--checking .push__knob { display: none; }

/* STATE 4. Mid-request, and not a control: this is inside the span, never
   inside a button. */
.push__track--busy {
  border-color: transparent; justify-content: center;
}
.push__track--busy .push__knob {
  width: 15px; height: 15px; background: none; box-shadow: none;
  border: 2px solid rgba(27, 67, 50, .2); border-top-color: var(--green-btn);
  animation: push-spin .7s linear infinite;
}
@keyframes push-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .push__track--busy .push__knob { animation: none; }
}

/* THE GAP BELOW THE ESCAPE-HATCH LINE, which is this card's top margin. It was
   0: the line sat welded to the card's top border, which is what made the line
   read as belonging to the card rather than sitting between two things.

   NOT ON /admin, where this same partial is one of a column of cards with their
   own rhythm and an extra margin here would only be this one card sitting lower
   than its neighbours. */
.page:not(.page--admin) > #notifications { margin-top: var(--dash-foot-gap); }

.push__note {
  margin: 10px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted);
}
.push__note:empty { display: none; }

/* NOTHING BELOW THE NOTE. The footer (divider, device count, Send a test) and
   the build / last-tap readout were removed from this card on Trey's ruling;
   see the head of views/partials/push-card.ejs for what was there and why it is
   not coming back as a fix. Their rules go with them rather than sitting here
   matching nothing, which is how a stylesheet starts describing a page that no
   longer exists. */

@media (max-width: 760px), (max-height: 499px) {
  /* THE SWITCH STAYS ON THE TITLE'S ROW AT EVERY WIDTH, and the way it does
     that is that nothing here stacks .push__row. It is small, it is the whole
     point of the card, and stacking it would put the control below a paragraph
     explaining it. Written as a note rather than a rule because the correct
     declaration is no declaration; the note is what stops one being added. */

  /* CLEARANCE ABOVE THE TAB BAR, and it is deliberately NOT part of .page's
     bottom padding.

     That padding is calc(60px + max(14px, var(--sab))) and it means one
     thing: reserve exactly the bar so no row sits UNDER it. The number is a
     contract shared with .sheet__scrim and .sheet__panel, and
     mobile-overflow.test.js asserts all three derive from the bar's own
     declaration -- "pages reserve exactly the bar they have to clear". Padding
     breathing room into it would make that number a mixture of two different
     jobs and break the assertion that keeps the three in step.

     So the gap between the last card and the bar's top edge is a margin on the
     card, in the same token as the gaps above it. The bar's reservation stays
     exactly the bar. */
  .page:not(.page--admin) > #notifications { margin-bottom: var(--dash-foot-gap); }
}


/* ══ ASSIGNED STUDENTS ══════════════════════════════════════════════════════
   design_handoff_roster, option 1a. The card at the foot of the dashboard that
   answers "who am I assigned to".

   IT REPLACES A DISCLOSURE, and the shape is the point of the replacement. The
   old one was a bare underlined <summary> immediately below the request-a-survey
   link -- same weight, same treatment, same place -- so it read as a second
   troubleshooting link rather than as a roster, and opening it gave an unstyled
   run of rows with no way to find one name among fourteen. This is always
   visible, is built out of chips rather than rows, and gives every name a shape
   before it gives it letters.

   THE MONOGRAM IS WHAT MAKES IT WORTH BUILDING: Libre Caslon initials on deep
   green, so a tutor recognises a chip by silhouette before reading it, which is
   what a roster looked at repeatedly is for. No photo, no per-student colour, no
   new asset and no new palette. The same green-and-gold circle the rolodex and
   training rosters use for tutors, at 28px and in the serif rather than the UI
   font -- the one new rule here, and the design's own.

   .assigned__*, NOT .roster__*, AND THAT IS NOT A STYLE PREFERENCE. The Admin
   tools roster already owns .roster__head, .roster__row and .roster__name half a
   file above. This block is later in the cascade, so naming it the obvious thing
   would have silently restyled the Students section on /admin -- the one place
   this build is under instruction not to touch -- from a page that never
   mentions this card.

   NO RED ANYWHERE, and no status. The card carries exactly one caption, the
   owner-only REQUESTED tag, and it is drawn in gold like every other quiet
   affirmative on this page. */
.assigned {
  /* THE FOOT RHYTHM'S TOKEN, NOT A LITERAL OF ITS OWN, because this card was
     inserted INTO that rhythm. bbb894e made the gaps above and below the
     escape-hatch line one value on the reasoning that the requirement is that
     they are EQUAL; the gap below the line used to be the notifications card's
     top margin, and it is this card's now, because this card is what comes next.
     A literal here would have re-opened by 18px the exact hole that commit
     closed: 16 above the line and something else below it. */
  margin-top: var(--dash-foot-gap);
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14);
  border-radius: var(--rt-radius-card);
  /* The footer band is full-bleed to the card's edges, so without this it would
     square off the two bottom corners. Clipping, not a containing block: only
     transform, filter and friends create one of those, and there is nothing
     positioned in here anyway. */
  overflow: hidden;
}
.assigned__head { padding: 17px 18px 14px; border-bottom: 1px solid rgba(27, 67, 50, .09); }
/* Set rather than inherited: .eyebrow is declared twice in this file and the
   winner carries a bottom margin, so the gap to the note below would depend on
   which one that is. */
.assigned__head .eyebrow { display: block; margin: 0; letter-spacing: .12em; }
.assigned__note { margin: 8px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--faint); }

/* The chips wrap; the card grows down. align-items keeps a shorter item -- the
   dashed "+N more", which has no monogram -- centred on the line rather than
   stretched to it. */
.assigned__body {
  padding: 14px 18px 16px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
}

/* THE CHIP IS THE HISTORY LINK, so this is an <a> and everything here is the
   link's own box. */
.assigned__chip {
  position: relative;
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 38px; padding: 0 12px 0 5px;
  border: 1px solid rgba(27, 67, 50, .16);
  border-radius: var(--rt-radius-pill);
  background: var(--cream);
  text-decoration: none;
  /* NEVER WIDER THAN THE CARD. A chip is sized by its name, and flex-wrap only
     moves a chip to the next line -- it cannot shrink one that is on its own
     and still too wide, which at 320px is a name of about forty characters.
     Capped here and allowed to break inside the name below, so an unusually
     long one grows the pill downward instead of pushing the page sideways.
     Nothing is clipped: the whole name is always readable. */
  max-width: 100%;
}
/* THE 44px TAP TARGET, WITHOUT A 44px PILL. The design asks for a visible pill
   of 38px and a target of 44, and padding cannot do both: padding sits inside
   the border, so it would grow the pill with the target. This overlay extends
   the hit area 3px above and below and paints nothing. The 7px row gap leaves
   1px between two stacked targets, so they never overlap. */
.assigned__chip::after { content: ''; position: absolute; left: 0; right: 0; top: -3px; bottom: -3px; }
.assigned__chip:hover { border-color: rgba(27, 67, 50, .34); }
.assigned__chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.assigned__mono {
  flex-shrink: 0;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--green-deep); color: var(--gold-light);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 12px; line-height: 1;
}
.assigned__name { min-width: 0; overflow-wrap: anywhere; font-size: 13px; font-weight: 600; color: var(--ink); }

/* THE OWNER-ONLY REQUESTED CHIP. The tag rides INSIDE the chip rather than
   beside it, so it can never wrap away from the name it belongs to. A heavier
   gold outline, because the chip is the only thing on this card that is saying
   anything at all. */
.assigned__chip--req { border: 1.5px solid var(--gold); }
.assigned__reqtag {
  font-size: 9px; font-weight: 700; letter-spacing: .08em; color: var(--gold-dark);
}

/* ── THE OVERFLOW FOOTER ──────────────────────────────────────────────────
   design_handoff_roster_overflow, option 2a. It REPLACES a dashed "+N more"
   chip that lived inside the grid, and the move out of the grid is the fix
   rather than a restyle.

   THREE FAULTS, ALL OF THEM POSITIONAL OR VERBAL. The chip did not move when it
   fired, so thirty names flowed in AFTER it and left it stranded mid-grid with a
   focus ring on it. It did not relabel, so it sat there still offering thirty
   more. And a dashed pill among ten solid pills reads as an eleventh student
   until you are close enough to read it.

   A FOOTER CANNOT BE STRANDED. It is outside the grid, so revealed chips flow
   above it; the DOM order is identical collapsed and expanded, and the only
   thing that changes is a class on the section. "Last in the card in both
   states" is therefore a property of the markup, not something the script has to
   maintain. */
.assigned__foot {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 46px; padding: 0 18px;
  border: 0; border-top: 1px solid rgba(27, 67, 50, .09);
  background: rgba(27, 67, 50, .03);
  font-family: var(--sans); font-size: 12.5px; font-weight: 600;
  color: var(--green-btn); cursor: pointer;
}
.assigned__foot:hover { background: rgba(27, 67, 50, .06); }
/* INSET, unlike every other focus ring on this card, and for a reason rather
   than by preference: the footer is full-bleed inside a card that clips its
   overflow to keep the band off the rounded corners, so an outset ring would be
   drawn straight into the clip and never seen. Same trick, same reason, as
   .tabbar__tab. */
.assigned__foot:focus-visible { outline: 2px solid var(--gold); outline-offset: -3px; }
.assigned__caret { font-size: 10px; line-height: 1; color: var(--green-btn); }

/* BOTH LABELS AND BOTH CARETS ARE IN THE MARKUP and this picks one, so the
   wording lives in the template only and the script never touches text.
   display:none rather than visibility or opacity, so the hidden one is out of
   the accessible name too and the button reads as exactly one label. */
.assigned__foot [data-when="collapsed"] { display: none; }
.assigned__foot [data-when="expanded"] { display: inline; }
.assigned--collapsed .assigned__foot [data-when="collapsed"] { display: inline; }
.assigned--collapsed .assigned__foot [data-when="expanded"] { display: none; }

/* THE CHIPS A COLLAPSED CARD HIDES. Every chip is server-rendered, always; this
   is the only thing standing between the eleventh name and the reader, and a tap
   removes the class rather than fetching anything.

   A CLASS, NEVER THE `hidden` ATTRIBUTE. .assigned__chip sets an author
   display:inline-flex, and an author display beats the user agent's
   [hidden]{display:none} outright whatever the specificity -- which is exactly
   how a previous build shipped deactivated roster rows that carried the
   attribute and sat in plain sight anyway. Two classes here, so it also outranks
   .assigned__chip itself. */
.assigned--collapsed .assigned__chip--extra { display: none; }


/* ══ THE HOURS EDITOR (booking loop, step 0a) ═══════════════════════════════

   A row per weekday: a switch, the day, and one or more from/to pairs. Ranges,
   not slots -- a tutor states 3:00 to 8:00 once and the availability run
   divides it -- because asking anyone to tick twelve hours a day is how
   availability data goes stale.

   NOTHING HERE IS position: fixed OR sticky, and nothing here sits near a rule
   that is. This block is a form inside .card on an ordinary scrolling page. */
.avail__eyebrow {
  margin: 0; font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 600;
  letter-spacing: .14em; color: var(--gold-dark);
}
.avail__intro {
  margin: 8px 0 0; font-family: var(--rt-font-ui); font-size: 13.5px;
  line-height: 1.65; color: var(--rt-secondary);
}
.avail__days {
  margin-top: 20px; background: var(--off-white);
  border: 1px solid var(--line); border-radius: 6px; overflow: hidden;
}

/* THE ROW. align-items:flex-start rather than center, because a day with two
   windows is two lines tall and the switch and the day name belong at the top
   of it rather than floating in the middle. */
.avail__day {
  display: flex; align-items: flex-start; gap: 16px; padding: 12px 14px;
  border-bottom: 1px solid var(--hair);
}
.avail__day:last-child { border-bottom: 0; }
/* An off day is tinted and says so. Its inputs are still in the document and
   still post -- that is what keeps its hours for when it comes back -- they are
   simply not shown. */
.avail__day--off { background: rgba(27, 67, 50, .03); }
.avail__day--off .avail__windows { display: none; }
.avail__day .avail__off { display: none; }
.avail__day--off .avail__off {
  display: block; flex: 1; align-self: center;
  font-family: var(--rt-font-ui); font-size: 13.5px; color: var(--rt-secondary);
}
.avail__day--off .avail__dayname { color: var(--rt-secondary); }

/* THE SWITCH. 44x26 pill in a 44px-tall label, so the tap target clears the
   floor even though the pill itself does not. */
.avail__switch {
  flex-shrink: 0; display: inline-flex; align-items: center;
  min-height: 44px; cursor: pointer;
}
.avail__switchinput {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.avail__track {
  display: flex; align-items: center; box-sizing: border-box;
  width: 44px; height: 26px; padding: 0 3px; border-radius: 100px;
  background: rgba(27, 67, 50, .16); border: 1px solid rgba(27, 67, 50, .2);
}
.avail__knob {
  width: 20px; height: 20px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.avail__switchinput:checked + .avail__track {
  justify-content: flex-end; background: var(--green-btn); border-color: var(--green-btn);
}
/* The ring goes on the pill, because the input it belongs to is clipped to a
   pixel and a focus ring on that is invisible. */
.avail__switchinput:focus-visible + .avail__track {
  outline: 2px solid var(--green-deep); outline-offset: 2px;
}
.avail__sronly {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

.avail__dayname {
  flex-shrink: 0; width: 44px; align-self: center;
  font-family: var(--rt-font-ui); font-size: 14px; font-weight: 700; color: var(--rt-ink);
}

.avail__windows { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.avail__window { display: flex; align-items: center; gap: 9px; }
/* 16px, which is the size below which iOS zooms the page on focus. Not a
   preference: the zoom does not zoom back out. */
.avail__time {
  flex: 1; min-width: 0; box-sizing: border-box; min-height: 44px;
  padding: 0 11px; font-family: var(--rt-font-ui); font-size: 16px;
  color: var(--rt-ink); background: var(--cream);
  border: 1px solid var(--field); border-radius: 4px;
}
.avail__time:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 2px; }
.avail__to {
  flex-shrink: 0; font-family: var(--rt-font-ui); font-size: 13px; color: var(--rt-secondary);
}

/* THE TWO SCRIPT-ONLY CONTROLS, hidden until public/availability.js marks the
   form enhanced. A control that is present and does nothing is worse than one
   that is absent: the form works without script, and these are the parts of it
   that cannot. */
.avail__remove, .avail__add { display: none; }
.avail--on .avail__remove {
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; width: 44px; height: 44px; padding: 0;
  background: none; border: 0; border-radius: 4px; cursor: pointer;
  font-size: 18px; line-height: 1; color: var(--rt-secondary);
}
.avail--on .avail__remove:hover { color: var(--rt-ink); background: rgba(27, 67, 50, .05); }
.avail--on .avail__add {
  display: inline-flex; align-items: center; align-self: flex-start;
  min-height: 44px; padding: 0; background: none; border: 0; cursor: pointer;
  font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--green-btn); text-decoration: underline; text-underline-offset: 3px;
}

.avail__foot {
  margin-top: 16px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.avail__save {
  min-height: 44px; display: inline-flex; align-items: center; padding: 0 20px;
  background: var(--green-btn); color: var(--off-white); border: 0; border-radius: 4px;
  font-family: var(--rt-font-ui); font-size: 13.5px; font-weight: 600; cursor: pointer;
}
.avail__save:hover { background: var(--green-deep); }
/* Hours from March are worse than no hours, so the stamp sits beside the save
   rather than in a corner. "Not set yet" is a different fact from a date and is
   set differently. */
.avail__stamp {
  font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary);
}
.avail__stamp--never { font-style: italic; }

/* Phones: the day name and the switch keep their line and the windows drop
   below, because two time inputs and the word "to" cannot share a row with a
   44px switch and a 44px day column at 360px. */
@media (max-width: 460px) {
  .avail__day { flex-wrap: wrap; gap: 10px 14px; }
  .avail__windows { flex: 1 0 100%; }
  .avail__day--off .avail__off { flex: 1 0 auto; align-self: center; }
}

/* ══ THE DASHBOARD'S AVAILABILITY CARD ═════════════════════════════════════
   The door to the hours editor, and the one screen that tells a tutor their
   hours are missing. The unset state carries the 4px gold left border the kill
   banners use, because a tutor with no hours cannot be offered to a parent and
   that is the same class of fact those banners carry. Gold, never red: this
   palette has no error colour. */
/* THE FOOT RHYTHM'S TOKEN, NOT THE 18px LITERAL THIS CARD USED TO CARRY.
   The card moved from under the three-step flow, where 18px was its own business,
   into the run that ends the page: escape-hatch line, roster card, this, then the
   notifications card. Every gap in that run is --dash-foot-gap because the
   requirement is that they are EQUAL, and an 18px member would have put a
   two-pixel step in the middle of it -- the same shape of hole, smaller, that
   bbb894e closed when the gap above the line was 16 and the gap below it was 0.
   test/dashboard-foot-spacing.test.js holds the whole chain to the one token. */
/* design_handoff_availability_card, option B with C as its empty state. Every
   colour below is the token that already holds the bundle's hex, so this adds no
   new value to the palette: #FDFCF9 --off-white, #D4D9D3 --line, #5C6B62
   --rt-secondary, #8A948C --faint, #1B4332 --green-btn, #C9A227 --gold, #7C6210
   --gold-ink. The one literal is the gold border at .45 alpha, which the bundle
   specifies and --rt-border-gold does not hold: that token is .35 and is used by
   the training pills, so matching it here would have moved them. */
.availcard {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin-top: var(--dash-foot-gap); padding: 18px 20px;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px;
}
/* THE GOLD RULE, NOT A RED ONE, and the bundle is explicit about why: hours not
   set is an incomplete profile, not a fault. Same argument the Rolodex makes
   with "No hours set" and weeklyEarningsCell() with "No rate set" rather than
   $0 -- two different facts, and the second would be a lie about the first.
   The radius opens on the left so the rule is the card's edge rather than a
   stripe inside a rounded corner. */
.availcard--unset {
  border: 1px solid rgba(168, 132, 42, .45);
  border-left: 3px solid var(--gold);
  border-radius: 0 6px 6px 0;
}
.availcard__main { min-width: 0; flex: 1 1 240px; }
.availcard__label {
  font-family: var(--rt-font-ui); font-size: 10.5px; font-weight: 600;
  letter-spacing: .14em; color: var(--rt-secondary);
}
.availcard--unset .availcard__label { color: var(--gold-ink); }
/* B's second line: the date, and nothing else. --faint is correct here and
   nowhere near a control: it is a fact you read, not a thing you press. */
.availcard__date {
  margin-top: 5px; font-family: var(--rt-font-ui); font-size: 12.5px;
  line-height: 1.6; color: var(--faint);
}
/* C's second line: the consequence. Gold ink rather than --faint, because this
   one is the reason the card is ruled gold and reading it is the point. */
.availcard__line {
  margin-top: 5px; font-family: var(--rt-font-ui); font-size: 12.5px;
  line-height: 1.6; color: var(--gold-ink);
}
/* 40px HERE AND 44px ON A PHONE, which is the bundle's own note followed to the
   letter: it asks for 40 on a profile card and says take it to 44 in the 760px
   block if the surface has a phone view. This one is phone-first -- it is on the
   dashboard foot, under the tab bar's thumb -- so it does. The override is at
   the foot of this file, AFTER this rule, for the source-order reason recorded
   at .rlist__filter: a media query adds no specificity. */
.availcard__go {
  flex-shrink: 0; min-height: 40px; display: inline-flex; align-items: center;
  padding: 0 16px; border: 1px solid rgba(27, 67, 50, .28); border-radius: 4px;
  font-family: var(--rt-font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--green-btn); text-decoration: none;
}
.availcard__go:hover { background: rgba(27, 67, 50, .05); color: var(--green-deep); }
/* FILLED IN THE TWO STATES THAT NEED ACTION, outlined in the one that does not.
   The fill is what makes the verb read as the thing to do rather than as a link
   you could take or leave, and it travels with the gold rule so the card says
   the same thing twice in two channels rather than disagreeing with itself. */
.availcard__go--fill {
  background: var(--green-btn); border-color: var(--green-btn); color: var(--off-white);
}
.availcard__go--fill:hover { background: var(--green-deep); color: var(--off-white); }

/* THE PHONE TARGET, and it sits here rather than in either 760px block further
   down this file for the reason recorded at .rlist__filter: a media query adds
   no specificity, so an override written before the rule it overrides ties and
   loses on source order, silently, at exactly the width it was written for. */
@media (max-width: 760px), (max-height: 499px) {
  .availcard__go { min-height: 44px; }
}

/* THE DEFAULT PILL, on the note line under a zone select.
   The BETA/SOON pill's shape, with the timezone bundle's own numbers: 10px and
   .1em tracking rather than .fwbeta's 9px and .14em, and --gold-ink rather than
   --rt-gold-dark, which is the darker gold the bundle specifies for this marker.
   Close enough to .fwbeta to read as the same pattern, and not close enough to
   share the class without changing what BETA looks like everywhere.

   IT SITS ON THE NOTE LINE BECAUSE IT CANNOT SIT IN THE LIST. An open native
   select is drawn by the OS and an <option> takes no background, radius or
   colour; the list carries the plain lowercase "(default)" instead.

   Inline rather than a flex row, so it wraps with the sentence it precedes and
   the .note rhythm underneath is untouched. */
/* THE SELECT AND ITS PILL ON ONE ROW. The select takes the width and the pill
   takes what it needs; both stay on the baseline the .lbl above them sets. */
.tzrow { display: flex; align-items: center; gap: 8px; }
.tzrow .sel { flex: 1; min-width: 0; }
/* ⚠ NO TRAILING MARGIN IN THE ROW. The 9px below is the gap before the sentence
   this pill used to precede, and .tzpill is still used that way by
   charge-rate.ejs for COMPANY STANDARD. The row supplies its own gap. */
.tzrow .tzpill { flex: none; margin-right: 0; }
.tzpill {
  display: inline-block; vertical-align: middle; margin-right: 9px;
  padding: 3px 10px; border-radius: 100px;
  background: rgba(201, 162, 39, .18); border: 1px solid rgba(168, 132, 42, .4);
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--gold-ink);
}

/* The family zone on a roster row's meta line, appended after the tutor and only
   when it is not Arizona. Gold at 12.5px bold is enough to catch a scanning eye
   without becoming a column; a default family's row is unchanged. */
.roster__tz { font-weight: 700; color: var(--gold-ink); }

/* The effective CHARGE RATE, after the zone, and only when it is not the company
   standard. Deliberately the same treatment as the zone rather than a louder
   one: both are facts about a family that is not on a default, and a rate drawn
   to stand out from the zone beside it would read as the more urgent of the two
   when it is not. */
.roster__rate { font-weight: 700; color: var(--gold-ink); }

/* The hours form's read-only zone echo, immediately under the intro. Body copy
   rather than a callout: it is a statement of fact about the fields below, not a
   warning, and the palette has no colour for a warning anyway. */
.avail__zone {
  margin: 10px 0 0; font-family: var(--rt-font-ui); font-size: 13px;
  line-height: 1.6; color: var(--rt-secondary);
}
.avail__zone b { color: var(--rt-ink); font-weight: 700; }

/* ══ SESSIONS TO BOOK (booking loop, step 6) ═══════════════════════════════

   At the top of Admin tools with the two kill banners, and built from the same
   parts: the card, the eyebrow, the 4px gold left border. It is the same class
   of thing -- a list that should be empty -- and looking like one is the point.

   UNLIKE THOSE TWO IT RENDERS WHEN EMPTY, because "nothing waiting" is worth
   knowing at a glance where "no archived sessions" is not news.

   MOBILE MATTERS MORE THAN DESKTOP HERE, which is the opposite of every other
   admin surface. The push arrives on a phone, so the follow-through happens on
   a phone: below 760px the row becomes a card, the button goes full width at
   44px, and the audit column is dropped rather than wrapped. It is the least
   load-bearing of the three and the first thing that should go.

   GOLD, NEVER RED. A session tomorrow that is not on the calendar is the thing
   to do first, not an error. */
.stb {
  margin: 18px 0 0; padding: 16px 18px;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px;
}
.stb__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.stb__label {
  font-family: var(--rt-font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .12em; color: var(--rt-secondary);
}
.stb__count { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary); }
.stb__empty {
  margin: 10px 0 0; font-family: var(--rt-font-ui); font-size: 13px;
  line-height: 1.6; color: var(--rt-secondary);
}

.stb__rows { list-style: none; margin: 12px 0 0; padding: 0; }
.stb__row {
  display: flex; align-items: flex-start; gap: 16px;
  padding: 14px 0; border-top: 1px solid var(--hair);
}
.stb__row:first-child { border-top: 0; padding-top: 4px; }
/* Drawn INSIDE the row rather than added to it, so a flagged row keeps the same
   height and left edge as every other one and the list does not step in and out
   down the page. The same 4px gold the kill banners use. */
.stb__row--soon { box-shadow: inset 4px 0 0 var(--gold); padding-left: 14px; }

.stb__what { flex: 1; min-width: 0; }
.stb__who { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.stb__student {
  font-family: var(--rt-font-ui); font-size: 14px; font-weight: 700; color: var(--rt-ink);
}
.stb__tutor { font-family: var(--rt-font-ui); font-size: 12.5px; color: var(--rt-secondary); }
.stb__tag {
  font-family: var(--rt-font-ui); font-size: 9.5px; font-weight: 700; letter-spacing: .1em;
  padding: 3px 7px; border-radius: 100px;
  background: rgba(201, 162, 39, .16); color: #7A6218;
}
.stb__when {
  margin-top: 4px; font-family: var(--rt-font-ui); font-size: 13.5px; color: var(--rt-ink);
}
.stb__when--asked { color: var(--rt-secondary); }
/* The parent's own words. Set apart so it reads as a quotation rather than as a
   field, because that is what it is. */
.stb__note {
  margin: 8px 0 0; padding-left: 10px; border-left: 2px solid var(--line);
  font-family: var(--rt-font-ui); font-size: 12.5px; line-height: 1.6;
  color: var(--rt-secondary);
}

.stb__audit {
  flex-shrink: 0; width: 150px;
  font-family: var(--rt-font-ui); font-size: 11.5px; line-height: 1.6; color: var(--rt-secondary);
}
.stb__act { flex-shrink: 0; }
.stb__btn {
  min-height: 44px; padding: 0 16px; border-radius: 4px;
  border: 1px solid var(--green-btn); background: var(--green-btn); color: var(--off-white);
  font-family: var(--rt-font-ui); font-size: 11.5px; font-weight: 700;
  letter-spacing: .06em; cursor: pointer;
}
.stb__btn:hover { background: var(--green-deep); border-color: var(--green-deep); }
/* FIND A TIME is quieter than MARK BOOKED, because it names a job still to do
   rather than one being closed. Same size and the same 44px target: the two are
   equally likely to be the row you are on. */
.stb__btn--find { background: none; color: var(--green-btn); }
.stb__btn--find:hover { background: rgba(27, 67, 50, .06); color: var(--green-deep); }

@media (max-width: 760px), (max-height: 499px) {
  .stb__row { flex-direction: column; align-items: stretch; gap: 10px; }
  /* The audit line goes. See the note at the top of this block: it is the least
     load-bearing of the three columns and the first thing that should not wrap. */
  .stb__audit { display: none; }
  .stb__act form { display: block; }
  .stb__btn { width: 100%; }
}

/* ══ THE BOOKING PAGES (booking loop, steps 2-4) ═══════════════════════════

   BUILT ON THE PARENT CONSENT PAGE'S SYSTEM. These pages carry .pc as well as
   .bk, so every token, the masthead, the column, the 560px desktop card, the
   focus ring, the 52px primary button and the confirmation's gold mark all come
   from that block above and are not restated here. What follows is only what
   this page adds.

   ONE DERIVED TOKEN, in the way the consent spec derives --pc-gold-ink and
   --pc-gold-tint: --bk-unavailable, the ground of a row that cannot be chosen.
   Its text stays --pc-muted at 5.5:1. The spec has no disabled colour and
   dimming the type to make one would put the one row whose whole justification
   is being read below the readable floor. Unreadable is not a disabled state.

   NO RED ANYWHERE, and nothing below can produce any. The failed-submit state is
   the consent page's gold validation treatment.

   NOTHING HERE IS position: fixed EXCEPT the sheet's scrim and panel, which are
   the sheet. They belong to a parent-facing page that has no tab bar, no nav and
   no safe-area padding of its own, and they are the only fixed rules in this
   block. */
.bk {
  --bk-unavailable: #F3EFE4;
}

.bk__eyebrow {
  margin: 0; font-family: var(--pc-mono); font-size: 10.5px; line-height: 1;
  letter-spacing: .12em; text-transform: uppercase; color: var(--pc-muted);
}

/* THE PROPOSED SLOT. A white-paper card on the paper ground, the same treatment
   the consent page gives its "Texts come from" card. */
.bk__slotcard {
  padding: 20px; border-radius: 6px; border: 1px solid var(--pc-hairline);
  background: #FFFFFF;
}
.bk__slotdate {
  margin: 0; font-family: var(--pc-serif); font-size: 22px; line-height: 1.2;
  color: var(--ink);
}
.bk__slottime {
  margin: 6px 0 0; font-family: var(--pc-serif); font-size: 19px; color: var(--green-deep);
}
.bk__slotnote { margin: 8px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--pc-muted); }

.bk__form { display: flex; flex-direction: column; gap: 12px; }
.bk__inflight { margin: 0; font-size: 13px; line-height: 1.5; color: var(--pc-muted); text-align: center; }

/* 4c · THE FAILED SUBMIT. The consent page's validation treatment exactly: 2px
   gold, gold tint fill, gold ink. Gold, never red -- this palette has no error
   colour and this is the same class of moment, something that needs doing again.

   "Nothing has been held yet, so nothing is lost" is the load-bearing sentence:
   without it a parent cannot tell whether retrying books twice, and the
   assumption they will make is the worst one. */
.bk__failed {
  padding: 16px; border-radius: 6px;
  border: 2px solid var(--gold); background: var(--pc-gold-tint);
  display: flex; flex-direction: column; gap: 8px;
}
.bk__failedtitle { margin: 0; font-size: 15px; font-weight: 700; color: var(--pc-gold-ink); }
.bk__failedbody { margin: 0; font-size: 14px; line-height: 1.55; color: var(--pc-gold-ink); }
/* Call is promoted to a real button here, the ONLY screen in the flow where it
   is. A parent whose connection just failed has good reason to stop trusting the
   page, and the phone always works. */
.bk__callbtn {
  display: flex; align-items: center; justify-content: center;
  min-height: 48px; margin-top: 4px; border-radius: 4px;
  border: 1px solid var(--green-btn); background: #FFFFFF;
  color: var(--green-btn); font-size: 15px; font-weight: 700; text-decoration: none;
}

/* ── THE SHEET ─────────────────────────────────────────────────────────────
   Over the page, not a page. The landing page stays visible behind it, so
   asking the question does not feel like giving up the slot, and Close returns
   to it. */
/* ⚠ SIZE THE SCRIM TO ITS CONTENT, NOT TO A FIXED HEIGHT, and lay it out as a
   COLUMN justified to flex-end. The bundle is explicit about both: the sheet's
   natural height is around 362px, so a 300px scrim with `bottom: 0` pushes the
   panel 62px ABOVE the scrim's top edge and it covers the page behind it with
   nothing dimmed. inset:0 plus justify-content:flex-end puts the dim band above
   the panel at whatever height the panel turns out to be.

   ⚠ AND NO overflow: hidden HERE. It is the obvious fix for a panel that
   overshoots and it clips the grab handle instead. */
.bk__sheet {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; justify-content: flex-end;
}
/* ⚠ [hidden] IS A USER AGENT RULE AND LOSES TO ANY AUTHOR display. The line
   above sets display:flex, so the closed sheet rendered anyway: a full viewport
   scrim at 68% over the page and a 46px empty panel strip at the foot of it.
   From the parent's side that is the card washed out with an empty box under
   it, and the scrim is what their taps landed on.

   ⚠ THIS IS THE THIRD TIME THIS EXACT RULE HAS BEEN LEARNED HERE. The roster
   rows, then .nav__panel, now the booking sheet. .sheet already carries the
   guard; this one was written eleven days ago without it. The sweep in
   test/hidden-attribute-sweep.test.js now checks every element a view renders
   with a bare `hidden`, so the fourth time fails in the suite instead. */
.bk__sheet[hidden] { display: none !important; }
/* THE FRAME'S SCRIM, rgba(20,55,42,.42). It was rgba(29,43,35,.68): a third
   darker and a different hue, which made the card behind it read as dismissed
   rather than as waiting. */
.bk__scrim { position: absolute; inset: 0; background: rgba(20, 55, 42, .38); }
/* A REAL BOTTOM SHEET on a phone: the grab handle is what makes it read as one
   rather than as a card that happens to be at the bottom. The sections carry
   their own padding, so the panel carries none but the safe area. */
.bk__panel {
  position: relative; width: 100%; max-height: 88vh; overflow-y: auto;
  box-sizing: border-box; padding: 0 0 var(--sab);
  background: var(--cream);
  border-radius: 12px 12px 0 0; box-shadow: 0 -14px 34px rgba(20, 55, 42, .22);
}
.bk__panel .bkask { padding: 14px 20px 20px; }
/* The page behind the sheet must not scroll. */
.bk--sheetopen { overflow: hidden; }
.bk__pane--off { display: none; }

.bk__with {
  margin: 0; font-family: var(--pc-mono); font-size: 10.5px; line-height: 1;
  letter-spacing: .12em; text-transform: uppercase; color: var(--pc-muted);
}
.bk__sheettitle {
  margin: 0; font-family: var(--pc-serif); font-weight: 400; font-size: 22px;
  line-height: 1.25; color: var(--ink);
}
/* The freshness line. It reports the CALENDAR's age, never the page's clock --
   see routes/booking.js. A button that says Book is a stronger claim than one
   that says Hold, and this is what keeps it honest between syncs. */
.bk__fresh { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--pc-muted); }

/* ── 3a · the day rows ─────────────────────────────────────────────────── */
.bk__days { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.bk__day {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 52px; padding: 0 14px; box-sizing: border-box;
  border-radius: 6px; border: 1px solid var(--pc-hairline); background: #FFFFFF;
  color: var(--ink); text-decoration: none; font-size: 15px;
}
.bk__day:hover { border-color: var(--green-btn); }
/* A tinted BAND, not faded text. Everything stays at the spec's muted ink; what
   changes is the ground behind it and the loss of the green count. */
.bk__day--full { background: var(--bk-unavailable); border-color: var(--bk-unavailable); color: var(--pc-muted); }
.bk__daylabel { min-width: 0; }
.bk__daylabel b { font-weight: 700; }
.bk__counts { flex-shrink: 0; display: flex; align-items: center; gap: 8px; }
.bk__count { font-size: 13.5px; color: var(--green-btn); white-space: nowrap; }
.bk__day--full .bk__count { color: var(--pc-muted); }
/* The usual day keeps its tag. It is the one row where the count is not the only
   thing worth knowing. */
.bk__usual {
  font-family: var(--pc-mono); font-size: 9.5px; letter-spacing: .1em;
  padding: 3px 7px; border-radius: 100px;
  background: rgba(201, 162, 39, .16); color: #7A6218;
}

/* ── 3b · that day's times ─────────────────────────────────────────────────
   The day is the heading, so the panels carry only the hour. That is what lets
   them be 60px targets at 21px: two or three unmissable ones instead of a list
   where every row repeats its date. */
.bk__times { display: flex; flex-direction: column; gap: 8px; }
.bk__time {
  display: flex; align-items: center; justify-content: center;
  min-height: 60px; border-radius: 6px; border: 1px solid var(--pc-hairline);
  background: #FFFFFF; cursor: pointer;
  font-family: var(--pc-serif); font-size: 21px; color: var(--ink);
}
/* Selection is a 2px --green-deep border, the consent page's validation border
   weight, so a chosen panel and a flagged one read at the same strength. The
   2px REPLACES the 1px; box-sizing is what stops the panel shifting a pixel. */
.bk__time--on { border: 2px solid var(--green-deep); box-sizing: border-box; }
.bk__time input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.bk__time:has(input:focus-visible) { outline: 2px solid var(--green-deep); outline-offset: 2px; }

.bk__back { margin: 0; }
.bk__sheetactions { margin: 0; display: flex; }
/* 44px minimum on every tappable link, per the spec. */
.bk__quiet {
  display: flex; align-items: center; min-height: 44px;
  font-size: 14px; color: var(--green-btn);
  text-decoration: underline; text-underline-offset: 2px;
}
.bk__quiet:hover { color: var(--green-deep); }
.bk__btnlink { display: flex; align-items: center; justify-content: center; text-decoration: none; }

/* ── 1c · the preferences chips ────────────────────────────────────────────
   Chips rather than a date picker: a parent does not know our calendar, and a
   specific date they choose is a guess we then have to negotiate. 48px, the
   spec's input height. */
.bk__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.bk__chip {
  display: inline-flex; align-items: center; min-height: 48px; padding: 0 14px;
  border-radius: 6px; border: 1px solid var(--pc-hairline); background: #FFFFFF;
  font-size: 14px; color: var(--ink); cursor: pointer;
}
.bk__chip input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.bk__chip:has(input:checked) { border: 2px solid var(--green-deep); background: #FFFFFF; }
.bk__chip:has(input:focus-visible) { outline: 2px solid var(--green-deep); outline-offset: 2px; }
/* 16px so iOS does not zoom on focus, the same rule every input on the consent
   page follows. */
.bk__note {
  width: 100%; box-sizing: border-box; padding: 12px; border-radius: 4px;
  border: 1px solid var(--pc-hairline); background: #FFFFFF; color: var(--ink);
  font-family: var(--pc-sans); font-size: 16px; line-height: 1.5;
}

/* ── 5a · Booked ───────────────────────────────────────────────────────────
   Four blocks. "Booked." is .pc__h1--done at 31px; the date sits under it at
   25px as the detail rather than competing for the same job. */
.bk__doneline { margin: 0; font-size: 15px; line-height: 1.6; color: var(--pc-muted); }
.bk__donedate {
  margin: 4px 0 0; font-family: var(--pc-serif); font-size: 25px; line-height: 1.2;
  color: var(--ink);
}
.bk__donetime { margin: 2px 0 0; font-family: var(--pc-serif); font-size: 25px; color: var(--green-deep); }
.bk__donedetail { margin: 8px 0 0; font-size: 14px; line-height: 1.55; color: var(--pc-muted); }
.bk__donerule { display: block; width: 30px; height: 2px; background: var(--gold); }
.bk__donenotes { display: flex; flex-direction: column; gap: 12px; }
.bk__donenotes p { margin: 0; font-size: 15px; line-height: 1.6; color: var(--ink); }
.bk__donenotes a, .bk__inline { color: var(--green-btn); text-decoration: underline; text-underline-offset: 2px; }

/* THE FOOTER BAND, and it is the one deliberate divergence from the consent
   page: unlike consent this page is ASKING for something, so a phone number is
   the fallback when it cannot help. On the Booked screen it loses the call-us
   line, because the number is already in the copy where it is needed, and two
   would be the doubling cut from the report email. */
.bk__foot { padding: 0 20px 36px; text-align: center; }
.bk__foot p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--pc-muted); }
.bk__foot a { color: var(--green-btn); text-decoration: underline; text-underline-offset: 2px; }
.bk__foot--done p { font-size: 12px; }

/* THE IN-FLIGHT DIM. Everything above the button drops to 40% and stops
   accepting taps, so the time cannot change mid-request. The button itself is
   excluded: it keeps its size and its position, because the parent's thumb is
   already there and a control that jumps under a finger reads as a misfire. */
.bk--booking .pc__card > *:not(.bk__form):not(.bkg__form),
.bk--booking .bk__pane > *:not(.bk__form) { opacity: .4; pointer-events: none; }
.bk--booking .bk__times, .bk--booking .bk__slotcard { opacity: .4; pointer-events: none; }
.pc__btn--busy { opacity: .8; }

@media (min-width: 641px) and (min-height: 500px) {
  /* The sheet centres rather than rising from the bottom edge, because on a
     desktop there is no bottom edge a thumb is near. */
  .bk__sheet { flex-direction: row; align-items: center; justify-content: center; }
  /* 1a'S PANEL: a centred 470px dialog, not a sheet. No grab handle, a gold
     rule instead, and the sections stop carrying their own padding. */
  .bk__panel {
    width: calc(100% - 52px); max-width: 470px; max-height: 82vh;
    border-radius: 6px; padding: 30px 30px 26px;
    box-shadow: 0 18px 48px rgba(20, 55, 42, .3);
  }
  .bk__panel .bkask { padding: 0; }
  .bk__scrim { background: rgba(20, 55, 42, .42); }
  .bk__foot { padding-bottom: 60px; }
}

/* ══ THE BOOKING GRID ══════════════════════════════════════════════════════
   design_handoff_booking_grid. TWO FRAMES, ONE DOM, AND THE WIDTH CHOOSES.

     2a, THE PHONE, AND IT IS THE BASE. One day per row, times as chips across
        it. The desktop grid puts days ACROSS because a parent who can only do
        Sundays reads down a column; a phone cannot hold seven columns, so the
        axis turns. Days run down, which is what a phone does anyway, and the
        times run across each row where there is width to spare.
     1a, CARD WIDTH, from 641px up. Seven weekday columns, Friday included.

   ⚠ "TWO COLUMNS AT 390px" WAS THE WRONG ANSWER AND IT SHIPPED. The first
   handoff said it without drawing it, and it splits one week across two blocks:
   a parent loses the weekday reading that the whole grid exists for. 2a is the
   correction and it is drawn in full.

   ⚠ NO CELL IS EVER BLANK AT EITHER WIDTH. Past is lightest -- not a refusal,
   just gone. Full and Off share a treatment because both mean you cannot have
   this day. On a PHONE the past days collapse into one sentence instead of four
   rows of nothing, and a closed day keeps its row and its word with NO CHIP
   SHAPE, so nothing closed can be mistaken for tappable.

   SIX DERIVED TOKENS, in the way --bk-unavailable is derived. Every one is from
   the bundle's token table.

   NOTHING HERE IS position: fixed. The grid scrolls with the page; the only
   fixed rules on this page are the call sheet's, which predate it.

   ⚠ NO FONT IS FETCHED FROM ANYWHERE. The frames load Libre Caslon Text from
   Google Fonts; this portal SELF-HOSTS the same family at the top of this file,
   from /static/fonts, and --serif and --pc-serif both name it. Same typeface,
   one fewer third party, and nothing for a CSP or an offline phone to refuse. */
.bk {
  --bkg-paper: #FDFCF9;               /* white paper: cells, chips, the footer  */
  --bkg-past: rgba(27, 67, 50, .035); /* the lightest no                        */
  --bkg-dead: #A9B1AA;                /* past dates, and nothing else           */
  --bkg-closedline: #E4DDCC;          /* the border on a Full or Off cell       */
  --bkg-rowline: #E8E1D2;             /* the divider between phone rows         */
  --bkg-chipline: rgba(27, 67, 50, .22); /* the edge of a free chip             */
  --bkg-gutter: 20px;                 /* 2a's card gutter; 26px at card width   */
}

/* ── the page, and the one card on it ────────────────────────────────────── */
.bkpage { padding: 12px 10px 32px; display: flex; flex-direction: column; align-items: center; }
.bkcard {
  position: relative; width: 100%; max-width: 980px; box-sizing: border-box;
  background: var(--cream); border: 1px solid rgba(27, 67, 50, .16);
  border-radius: 12px; overflow: hidden;
}

/* ── ONE MASTHEAD, AND IT IS 2a's ─────────────────────────────────────────
   Trey's ruling, 17 September 2026. The frames did not agree: 1a drew a
   left-aligned 19px wordmark with a NEXT SESSION eyebrow and a 3px rule, 2a a
   centred 26px shield with a 17px wordmark, no eyebrow and a 4px rule. 2a is
   the lockup the parent-facing chrome ALREADY uses -- the survey pages, the
   work pages and the offline page all carry this shield -- so 1a was the drift.
   It reflows rather than changing at the breakpoint. */
.bkcard__mast { background: var(--green-deep); padding: 18px 22px; text-align: center; }
.bkcard__shield { display: block; margin: 0 auto 7px; width: 26px; height: auto; border: 0; }
.bkcard__wordmark {
  font-family: var(--pc-serif); font-size: 17px; font-weight: 700;
  letter-spacing: .4px; color: var(--bkg-paper);
}
/* Two words, two faces. The storyboard's own lockup. */
.bkcard__wordmark2 { font-weight: 400; font-style: italic; color: var(--gold-light); }
/* A 4px BAND OF ITS OWN, not a border on the masthead. */
.bkcard__rule { height: 4px; background: var(--gold); }

/* ── the head ─────────────────────────────────────────────────────────────
   ⚠ .bkg__intro, NOT .bkg__head. The weekday cells above the grid are
   .bkg__head, and naming the head container the same thing gave each element
   the other's rules: the eyebrow, the heading and the subtitle inherited
   text-align:center and a 10px/700/.1em type ramp, and the weekday cells
   inherited 30px of padding. Both render close enough to look right in a
   screenshot, which is exactly why it is written down here. */
.bkg__intro { padding: 20px var(--bkg-gutter) 0; }
.bkg__kicker { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .14em; color: #A8842A; }
/* ⚠ WEIGHT 700, AND IT IS A DELIBERATE DEPARTURE FROM THE DRAWN FRAME. Both
   frames draw this heading at 400; Trey ruled 700 on 17 September 2026, at both
   breakpoints. Libre Caslon Text Bold is self-hosted at the top of this file, so
   this is the real cut rather than a synthesised one. Written down because a
   future pass comparing the page to the frame will find it and would otherwise
   "correct" it back. */
.bkg__title {
  margin: 9px 0 0; font-family: var(--pc-serif); font-weight: 700;
  font-size: 25px; line-height: 1.2; color: var(--ink); text-wrap: pretty;
}
.bkg__sub { margin: 9px 0 0; font-size: 14px; line-height: 1.6; color: var(--pc-muted); text-wrap: pretty; }

/* ── the grid ─────────────────────────────────────────────────────────────── */
.bkg__grid { padding: 0 var(--bkg-gutter); }
.bkg__form { display: flex; flex-direction: column; }

.bkg__block { display: flex; flex-direction: column; padding-top: 20px; }
.bkg__label { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--pc-gold-ink); }
/* ⚠ THE DAYS THAT HAVE GONE, IN ONE SENTENCE. Phone only: at card width the
   Past cells are drawn and cost part of one row rather than four screens of
   nothing before a parent reaches anything bookable. */
.bkg__passed { margin: 7px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--pc-muted); }

/* THE WEEKDAY HEADER ROW IS CARD WIDTH ONLY. On a phone the column no longer
   stands for a weekday, so the row would be a lie; the row's own gutter says
   which day it is instead. */
.bkg__heads { display: none; }
.bkg__head {
  font-size: 10px; font-weight: 700; letter-spacing: .1em;
  color: var(--pc-muted); text-align: center; padding-bottom: 3px;
}

/* ⚠ ON A PHONE THE WEEK IS A CARD OF ROWS, NOT A GRID. This is the rule whose
   absence produced the defect: with no grid and no row treatment every cell
   stacked as a line of bare text and the weekday heads ran together as
   SUNMONTUEWEDTHUFRISAT. */
.bkg__week {
  margin-top: 10px;
  background: var(--bkg-paper); border: 1px solid var(--pc-hairline);
  border-radius: 6px; overflow: hidden;
}

/* ── a day, as a row ──────────────────────────────────────────────────────── */
.bkg__cell {
  display: flex; gap: 12px; padding: 10px 13px; box-sizing: border-box;
  border-bottom: 1px solid var(--bkg-rowline);
}
.bkg__cell:last-child { border-bottom: 0; }
/* ⚠ A CLOSED DAY KEEPS ITS ROW AND ITS WORD, and the word sits as plain text in
   the chip area with NO CHIP SHAPE, so nothing closed can be mistaken for
   tappable. These rows are shorter because they hold one line, not chips. */
.bkg__cell--full, .bkg__cell--off { align-items: center; padding: 11px 13px; }
/* ⚠ AND A PAST DAY IS NOT A ROW AT ALL HERE. It is in the sentence above. */
.bkg__cell--past { display: none; }

/* The 48px gutter: weekday over date, and USUAL under both. */
.bkg__cellhead { flex-shrink: 0; width: 48px; padding-top: 6px; }
.bkg__cell--full .bkg__cellhead, .bkg__cell--off .bkg__cellhead { padding-top: 0; }
.bkg__dow { display: block; font-size: 9px; font-weight: 700; letter-spacing: .08em; color: var(--pc-muted); }
.bkg__num { display: block; font-family: var(--pc-serif); font-size: 19px; line-height: 1.1; color: var(--ink); }

/* ⚠ USUAL SITS UNDER THE DATE, IN THE GUTTER. Inline with the chips it reads as
   tappable, and it would push a chip onto a second line. THE SAME PILL AS
   PARENT BOOKED AND DEFAULT: gold carries warnings on this portal but it also
   carries neutral facts, and this is the pill family that means "a note". */
.bkg__usual {
  /* ⚠ A BLOCK-LEVEL FLEX BOX THAT SHRINK-WRAPS, NOT AN inline-flex. An
     inline-level box sits in a line box, and the line box's strut adds
     half-leading above it: the pill measured 6px under the date against the 4px
     the frame draws, and it rode on the date's baseline metrics rather than
     sitting squarely under it. `display: flex` with `width: fit-content` takes
     it out of the inline formatting context entirely, so the margin above it is
     the whole of the gap and its left edge is the gutter's content edge, flush
     with the date. */
  display: flex; width: fit-content; align-items: center; margin-top: 4px;
  padding: 2px 6px; border-radius: 100px; background: rgba(201, 162, 39, .18);
  font-size: 8.5px; font-weight: 700; letter-spacing: .06em; color: var(--pc-gold-ink);
}

.bkg__word { flex: 1; min-width: 0; font-size: 13px; color: var(--pc-muted); }

/* ── the hours, as chips across the row ───────────────────────────────────
   ⚠ align-items AND align-content, BOTH flex-start, AND BOTH ARE LOAD BEARING.
   A bare `display:flex; flex-wrap:wrap` leaves align-items at `normal`, which
   STRETCHES: the Tuesday gutter is taller than the others because it carries
   USUAL, so that flex line's cross size grows and its chips render 55px against
   every other row's 44px. The chips declare min-height correctly; it is the
   parent that overrides them. */
.bkg__times {
  flex: 1; min-width: 0;
  display: flex; flex-wrap: wrap; align-items: flex-start; align-content: flex-start; gap: 6px;
}
.bkg__slot {
  min-height: 44px; box-sizing: border-box;
  display: inline-flex; align-items: center; padding: 0 14px;
  border-radius: 4px; background: var(--bkg-paper); border: 1px solid var(--bkg-chipline);
  font-size: 14px; font-weight: 600; color: var(--ink); cursor: pointer;
}
.bkg__slot--on {
  background: var(--green-deep); border-color: var(--green-deep);
  color: var(--bkg-paper); font-weight: 700;
}
/* ⚠ THE RADIO IS VISUALLY HIDDEN, NOT display:none, so it stays focusable and
   the keyboard can still move through the hours. */
.bkg__slot input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.bkg__slot:has(input:focus-visible) { outline: 2px solid var(--green-deep); outline-offset: 2px; }

/* ── None of these work, in the two places the two frames put it ──────────
   Rendered twice and one is display:none at any width: CSS cannot move a child
   between containers, and `order` cannot lift it out of a band it has to sit
   outside of. display:none removes an element from the accessibility tree, so
   exactly ONE is exposed to a reader, a keyboard or a pointer at any width. */
.bkg__nonewrap--phone { margin: 0; padding: 18px var(--bkg-gutter) 0; text-align: center; }
.bkg__nonewrap--card { display: none; }
.bkg__none {
  min-height: 44px; display: inline-flex; align-items: center;
  font-size: 13.5px; font-weight: 600; color: var(--green-btn); text-decoration: none;
}
/* ⚠ THE UNDERLINE GOES ON THE WORDS, NOT THE TAP BOX. A border-bottom on a 44px
   inline-flex draws the rule at the BOTTOM OF THE TARGET, well clear of the
   text it belongs to. */
.bkg__none span { border-bottom: 1px solid rgba(27, 67, 50, .35); padding-bottom: 2px; }
.bkg__none:hover { color: var(--green-deep); }

/* ── the foot band ─────────────────────────────────────────────────────────
   ⚠ THE BUTTON SITS ON ITS OWN TINTED BAND, NOT IN THE FLOW. After two weeks of
   rows the primary would otherwise be a long way below the last chip; on a band
   it reads as the page's action rather than as one more row. */
.bkg__foot {
  margin-top: 18px; padding: 14px var(--bkg-gutter) 20px;
  border-top: 1px solid var(--pc-hairline); background: var(--bkg-paper);
}
.bkg__cta {
  width: 100%; min-height: 52px; padding: 0 14px; border: 0; border-radius: 4px;
  background: var(--green-btn); color: var(--bkg-paper);
  font-family: inherit; font-weight: 700; font-size: 13.5px; line-height: 1.3;
  letter-spacing: .04em; text-align: center; cursor: pointer;
}
.bkg__cta:hover { background: var(--green-deep); }
/* ⚠ WITH NOTHING PICKED IT IS DEAD BUT READABLE, AND IT SAYS WHAT TO DO. A
   parent who scrolls to the bottom first should learn there how this page
   works, and THAT PURPOSE REQUIRES THE LABEL CLEAR THE CONTRAST FLOOR. The hold
   spec's dead-primary ink (#A9B1AA) measured 2.05:1 on this fill and is wrong
   on a label carrying an instruction; --pc-muted on rgba(27,67,50,.08) clears
   4.5:1. Only ever applied by script -- see public/booking.js -- because a
   parent with no script must get the live button the browser can validate. */
.bkg__cta--quiet {
  background: rgba(27, 67, 50, .08); border: 1px solid rgba(27, 67, 50, .18);
  color: var(--pc-muted); cursor: default;
}
.bkg__cta--quiet:hover { background: rgba(27, 67, 50, .08); }
.bkg__cta--busy { opacity: .8; }
.bkg__inflight { margin: 8px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--pc-muted); text-align: center; }
/* ⚠ NO FRESHNESS LINE, AT EITHER WIDTH. It read "Checked at 8:07 PM on Sept
   16th". Trey's ruling, 17 September 2026: removed, a deliberate departure from
   frames that both draw it. The tap is still re-checked against the calendar as
   it is at that moment, by availabilityRun.isOfferable; the line was the
   disclosure, the check is the guarantee, and only the disclosure is gone.
   On a phone "None of these work" already sits above this band, so the band
   holds the button alone. */

/* THE HOUR WENT WHILE THEY WERE DECIDING. Gold, never red: this palette has no
   error colour and nothing failed, the answer moved. */
.bk__gone {
  margin: 16px var(--bkg-gutter) 0; padding: 12px 14px; border-radius: 4px;
  border: 1px solid rgba(168, 132, 42, .4); background: var(--pc-gold-tint);
  font-size: 13.5px; line-height: 1.55; color: var(--pc-gold-ink);
}
/* The failed submit and the empty-fortnight call panel take the same gutter,
   because they sit in the card where the grid would be. */
.bk--grid .bk__failed, .bk:not(.bk--grid) .bkask { margin: 16px var(--bkg-gutter) 0; }
.bk:not(.bk--grid) .bkask { padding-bottom: 24px; }

/* ── the call sheet ───────────────────────────────────────────────────────
   A parent whose answer is not on the grid has a reason, and a reason is a
   conversation. Call and text are EQUAL WEIGHT and both the real number.

   ⚠ ON A PHONE IT IS A REAL BOTTOM SHEET: grab handle, then the two numbers as
   STACKED 52px buttons. Side by side at 390px they are 175px each for a
   fourteen character label. */
.bkask { display: flex; flex-direction: column; }
/* 1a's gold rule, card width only: 2a opens on the grab handle instead, and a
   second horizontal mark under it is two things saying "this is the top". */
.bkask__rule { display: none; }
.bkask__grab { padding: 11px 0 0; display: flex; justify-content: center; }
.bkask__grab span { width: 38px; height: 4px; border-radius: 100px; background: rgba(27, 67, 50, .2); }
.bkask__title {
  margin: 0; font-family: var(--pc-serif); font-weight: 400; font-size: 22px;
  line-height: 1.2; color: var(--ink);
}
.bkask__body { margin: 8px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--pc-muted); }
/* STACKED, not side by side. */
.bkask__acts { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.bkask__btn {
  min-height: 52px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 4px; background: var(--green-btn); color: var(--bkg-paper);
  font-weight: 700; font-size: 13.5px; letter-spacing: .04em; text-decoration: none;
}
.bkask__btn--alt {
  background: var(--bkg-paper); border: 1px solid rgba(27, 67, 50, .3); color: var(--green-btn);
}
.bkask__note { margin: 12px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--pc-muted); text-align: center; }
.bkask__back { margin: 10px 0 0; text-align: center; }
.bkask__backlink {
  min-height: 44px; display: inline-flex; align-items: center;
  font-size: 13px; font-weight: 600; color: var(--green-btn); text-decoration: none;
}
.bkask__backlink span { border-bottom: 1px solid rgba(27, 67, 50, .35); padding-bottom: 2px; }

/* THE IN-FLIGHT DIM. Everything above the button drops to 40% and stops
   accepting taps, so the hour cannot change mid-request. */
.bk--booking .bkg__grid { opacity: .4; pointer-events: none; }

/* ══ 1a, FROM 641px UP ═════════════════════════════════════════════════════
   The axis turns back: seven weekday columns, one cell per day, hours stacked
   inside the cell. Everything the phone does to a row is undone here. */
@media (min-width: 641px) and (min-height: 500px) {
  /* ⚠ 26px IS A 980px CARD'S GUTTER. At 390 it would leave 304px of row. */
  .bk { --bkg-gutter: 26px; }
  .bkpage { padding: 40px 20px 60px; }
  /* 1a's card chrome: a 6px radius on a #DDD6C6 hairline. */
  .bkcard { border-color: var(--pc-hairline); border-radius: 6px; }

  .bkg__intro { padding: 30px var(--bkg-gutter) 0; }
  .bkg__kicker { font-size: 10.5px; color: var(--pc-gold-ink); }
  .bkg__title { margin-top: 11px; font-size: 31px; line-height: 1.15; }  /* weight 700 inherited from the base; ruled, not drawn */
  .bkg__sub { margin-top: 5px; font-size: 15.5px; }

  .bkg__grid { padding: 26px var(--bkg-gutter) 0; }
  .bkg__block { padding-top: 0; }
  .bkg__block--next { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--pc-hairline); }
  .bkg__label { font-size: 10px; letter-spacing: .1em; }
  /* The sentence is the phone's answer to the Past rows; here the cells are
     drawn and the sentence would say the same thing twice. */
  .bkg__passed { display: none; }

  .bkg__heads,
  .bkg__week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px; }
  .bkg__heads { margin-top: 8px; }
  /* The header row owns the 8px above the cells, and the week loses the card
     treatment it wears on a phone. */
  .bkg__week {
    margin-top: 0; align-items: start;
    background: none; border: 0; border-radius: 0; overflow: visible;
  }

  .bkg__cell {
    display: block; min-height: 104px; padding: 7px; border-radius: 5px;
    background: var(--bkg-paper); border: 1px solid var(--pc-hairline);
  }
  .bkg__cell:last-child { border-bottom: 1px solid var(--pc-hairline); }
  /* Past is lighter because it is a different kind of no, and it carries NO
     border at all: a box drawn around a day that has gone reads as a thing to
     try. Drawn here, unlike on a phone. */
  .bkg__cell--past { display: block; background: var(--bkg-past); border: 0; }
  .bkg__cell--full,
  .bkg__cell--off {
    padding: 7px; background: var(--bk-unavailable); border-color: var(--bkg-closedline);
  }

  /* The gutter becomes the cell's head: the numeral centred over a hairline. */
  .bkg__cellhead {
    width: auto; padding: 0 0 7px;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    min-height: 24px; flex-wrap: wrap; border-bottom: 1px solid var(--bkg-closedline);
  }
  .bkg__cell--full .bkg__cellhead, .bkg__cell--off .bkg__cellhead { padding: 0 0 7px; }
  /* The header row says the weekday at this width, so the cell does not. */
  .bkg__dow { display: none; }
  .bkg__num { display: inline; font-size: 19px; line-height: normal; }
  .bkg__cell--past .bkg__num { color: var(--bkg-dead); }
  .bkg__cell--full .bkg__num, .bkg__cell--off .bkg__num { color: var(--pc-muted); }
  /* Back to an inline box here: 1a puts USUAL BESIDE the numeral in a centred
     cell head, not under it in a gutter. */
  .bkg__usual {
    display: inline-flex; width: auto; margin-top: 0; flex-shrink: 0;
    padding: 2px 6px; font-size: 8px; background: rgba(201, 162, 39, .2);
  }

  /* The word centres under the hairline rather than sitting beside a gutter. */
  .bkg__word { display: block; margin-top: 8px; text-align: center; font-size: 12px; }
  .bkg__cell--past .bkg__word { color: var(--bkg-dead); }

  /* The hours stack rather than running across, and the cell is the target. */
  .bkg__times { display: block; }
  .bkg__slot {
    display: flex; justify-content: center; margin-top: 6px; padding: 0;
    background: var(--cream); border-color: var(--pc-hairline);
    font-size: 13.5px; font-weight: 400; color: var(--green-deep);
  }
  .bkg__slot--on { background: var(--green-deep); border-color: var(--green-deep); color: var(--bkg-paper); font-weight: 700; }

  /* None of these work moves into the foot band's row, beside the freshness. */
  .bkg__nonewrap--phone { display: none; }
  .bkg__nonewrap--card { display: inline; }
  .bkg__none { font-size: 14px; }
  .bkg__none span { padding-bottom: 1px; }

  .bkg__foot { margin-top: 24px; padding: 20px var(--bkg-gutter) 24px; }
  .bkg__cta { padding: 0 16px; font-size: 13.5px; letter-spacing: .05em; }
  .bkg__inflight { margin-top: 12px; font-size: 13px; }
  /* ⚠ STILL AT THE RIGHT END OF THE BAND, which is where the freshness line's
     `flex: 1` used to push it. The ruling removed one element; it did not move
     this one, so the alignment is restated rather than inherited from a rule
     that has gone. */
  .bkg__nonewrap--card { display: block; margin-top: 12px; text-align: right; }

  .bk__gone, .bk--grid .bk__failed, .bk:not(.bk--grid) .bkask { margin-top: 20px; }

  /* 1a's dialog: centred, 470px, on its gold rule rather than a grab handle. */
  .bkask__rule { display: block; width: 44px; height: 3px; background: var(--gold); }
  .bkask__grab { display: none; }
  .bkask__title { margin-top: 20px; font-size: 28px; }
  .bkask__body { margin-top: 14px; font-size: 15.5px; line-height: 1.7; }
  .bkask__acts { margin-top: 24px; flex-direction: row; gap: 10px; flex-wrap: wrap; }
  .bkask__btn { flex: 1; min-width: 190px; font-size: 14px; }
  .bkask__note { margin-top: 14px; font-size: 13px; line-height: 1.6; text-align: left; }
  .bkask__back { margin-top: 18px; }
  .bkask__backlink { font-size: 14px; }
  .bkask__backlink span { padding-bottom: 1px; }
}

/* ══ INQUIRIES ═════════════════════════════════════════════════════════════
   design_handoff_inquiries, frames 1c (phone) and 1e (desktop). ONE INQUIRY AT
   A TIME, as a deep green card with the actions already out.

   ⚠ IT IS THE LOUDEST THING THIS PRODUCT PUTS ON A DASHBOARD, and the argument
   is narrow: an inquiry is the only item on that page where ten minutes versus
   tomorrow decides whether it converts. Nothing else here earns this treatment.

   ⚠ GOLD ON GREEN FOR CALL, #E3B53A at 6.9:1. Green on green is invisible,
   which is the same swap the pre-session email made.

   ⚠ NO RED ANYWHERE, and there is nothing in this section for red to mean. Gold
   carries every warning, which is the portal's rule.

   ⚠ AND NO TRASH TREATMENT. Trey's ruling, 17 September 2026: the frames draw a
   trash control and a typed confirmation; neither is built. The fifth outcome
   files a lead instead, so there is no destructive control here to style. */
.inq { margin: 0 0 18px; }
.inq__card { background: var(--green-deep); border-radius: 6px; overflow: hidden; }

.inq__head {
  padding: 13px 16px; display: flex; align-items: baseline; gap: 10px;
  border-bottom: 1px solid rgba(227, 181, 58, .22);
}
.inq__eyebrow {
  flex-shrink: 0; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-light);
}
/* THE AGE IS THE PRESSURE: "3h" reads differently from "2d". */
.inq__age { margin-left: auto; flex-shrink: 0; font-size: 11px; color: rgba(255, 255, 255, .62); }

.inq__who { padding: 16px 16px 0; }
.inq__parent { font-family: var(--serif); font-size: 24px; line-height: 1.2; color: var(--off-white); }
.inq__sub { margin-top: 5px; font-size: 13px; color: #C8D3CD; }
/* THE MESSAGE, UNEDITED AND IN QUOTES. Most are one word, and that word is what
   tells you who to hand the lead to. */
.inq__msg { margin: 13px 0 0; font-size: 15px; line-height: 1.6; color: var(--off-white); }
.inq__refer { margin: 9px 0 0; }
.inq__referpill {
  display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 100px;
  background: rgba(201, 162, 39, .18); border: 1px solid rgba(168, 132, 42, .4);
  font-size: 8.5px; font-weight: 700; letter-spacing: .07em; color: var(--gold-light);
}

/* CONTACT DETAILS ARE A BLOCK, NOT BUTTONS: the buttons carry the actions and
   this is the number in view before you decide to call. */
.inq__detail {
  margin: 14px 16px 0; padding-top: 12px; border-top: 1px solid rgba(227, 181, 58, .18);
  font-size: 12px; line-height: 1.7; color: #C8D3CD;
}
.inq__detailline { display: block; }

.inq__acts { padding: 16px; }
.inq__call {
  min-height: 52px; display: flex; align-items: center; justify-content: center;
  background: var(--gold-light); border-radius: 4px; text-decoration: none;
  font-size: 13.5px; font-weight: 700; letter-spacing: .05em; color: var(--green-deep);
  box-sizing: border-box;
}
.inq__acts2 { margin-top: 7px; display: flex; gap: 7px; }
.inq__act {
  flex: 1; min-height: 48px; display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(227, 181, 58, .5); border-radius: 4px; text-decoration: none;
  font-size: 13px; font-weight: 700; letter-spacing: .05em; color: var(--gold-light);
  box-sizing: border-box;
}

/* ── the action row ─────────────────────────────────────────────────────────
   ENGAGED is a full-width 48px button on the phone, matching the CALL stack
   above it. "No answer" and "Skip" are quiet controls beside each other, and
   both keep a 44px target with the underline on the WORDS rather than on the
   tap box: a border-bottom on a 44px inline-flex draws the rule at the bottom
   of the target, well clear of the text. */
.inq__row { padding: 0 16px 16px; }
.inq__engageform { padding-top: 13px; border-top: 1px solid rgba(227, 181, 58, .22); }
.inq__engaged {
  width: 100%; min-height: 48px; display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid rgba(227, 181, 58, .55); border-radius: 4px;
  font-family: inherit; font-size: 12.5px; font-weight: 700; letter-spacing: .06em;
  color: var(--gold-light); cursor: pointer; box-sizing: border-box;
}
.inq__engaged:hover { background: rgba(227, 181, 58, .12); }
.inq__minor { margin-top: 9px; display: flex; align-items: center; }
.inq__quiet {
  min-height: 44px; display: inline-flex; align-items: center; padding: 0;
  background: none; border: 0; font-family: inherit;
  font-size: 12.5px; font-weight: 600; color: #C8D3CD; cursor: pointer; text-decoration: none;
}
.inq__quiet span { border-bottom: 1px solid rgba(255, 255, 255, .3); padding-bottom: 2px; }
.inq__quiet--skip { margin-left: auto; padding: 0 9px; }
.inq__hint { margin: 9px 0 0; font-size: 11px; line-height: 1.5; color: rgba(255, 255, 255, .55); }

/* ⚠ THIS ROW TAKES THE CARD'S OWN HORIZONTAL INSET. It is a sibling of the card
   with no padding of its own, so without a gutter here it runs wider than the
   green card on both sides. The dashboard's own column supplies the outer
   padding, so this is zero and the buttons align to the card's edges. */
.inq__foot { margin-top: 11px; display: flex; gap: 8px; }
.inq__footbtn {
  flex: 1; min-width: 0; min-height: 48px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  background: var(--off-white); border: 1px solid rgba(27, 67, 50, .25); border-radius: 5px;
  font-size: 12.5px; font-weight: 600; color: var(--green-btn); text-decoration: none;
}
.inq__footbtn:hover { border-color: var(--green-btn); }

/* ── NEXT UP ────────────────────────────────────────────────────────────────
   ⚠ ABSENT ON THE PHONE, and that is frame 1c's own correction: the card already
   shows the oldest inquiry and its count, so listing the rest by name duplicated
   the count and pushed the one-two-three strip off the screen. The two buttons
   above are what remains. Desktop keeps the list, because a list beside a panel
   is what that layout is for. */
.inq__next { display: none; }
.inq__nexthead { display: flex; align-items: baseline; gap: 10px; }
.inq__nextlabel { font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.inq__nextlog {
  margin-left: auto; font-size: 11.5px; font-weight: 600; color: var(--green-btn); text-decoration: none;
}
.inq__block {
  margin-top: 8px; display: flex; gap: 11px; padding: 14px 16px;
  background: var(--off-white); border: 1px solid rgba(27, 67, 50, .12); border-radius: 6px;
  text-decoration: none;
}
.inq__block:hover { border-color: rgba(27, 67, 50, .3); }
/* ⚠ THE DOT SITS IN A GUTTER EVERY ROW RESERVES, so nothing shifts when it
   clears. A cleared one keeps its space and loses its colour. */
.inq__dot {
  flex-shrink: 0; width: 7px; height: 7px; margin-top: 6px; border-radius: 50%;
  background: var(--gold);
}
.inq__dot--seen { background: none; }
.inq__blockbody { flex: 1; min-width: 0; }
.inq__blocktop { display: flex; align-items: baseline; gap: 8px; }
.inq__blockname { flex: 1; min-width: 0; font-size: 15px; font-weight: 700; color: var(--ink); }
.inq__blockage { flex-shrink: 0; font-size: 10.5px; color: var(--muted); }
.inq__blockstudent { display: block; margin-top: 2px; font-size: 12.5px; color: var(--ink); }
.inq__blockschool { display: block; margin-top: 1px; font-size: 11.5px; color: var(--muted); }
.inq__blockmsg { display: block; margin-top: 6px; font-size: 12.5px; line-height: 1.5; color: var(--ink); }

/* ── the empty state ────────────────────────────────────────────────────────
   ⚠ ABSENT, NOT COLLAPSED. There is no header, no chevron and nothing to
   expand, because a section that folds implies something folded inside it. */
.inqempty {
  padding: 22px 18px; background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .12); border-radius: 6px; text-align: center;
}
.inqempty__title { margin: 0; font-family: var(--serif); font-size: 19px; color: var(--ink); }
.inqempty__note { margin: 7px auto 0; max-width: 340px; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.inqempty__log { margin: 14px 0 0; }
.inqempty__log .inq__footbtn { display: inline-flex; flex: none; padding: 0 20px; }

/* ── the outcome sheet ──────────────────────────────────────────────────────
   A bottom sheet on the phone and a centred modal on desktop, with the same
   five rows. ⚠ FIVE, NOT THE FRAME'S FOUR: the fifth files a lead that was
   never real, which is the ruling's replacement for the trash control. */
.inqout {
  position: fixed; inset: 0; z-index: 240;
  display: flex; flex-direction: column; justify-content: flex-end;
}
/* [hidden] is a user agent rule and loses to the display above. This one is
   never rendered hidden, but the guard is cheap and the repo has learned this
   lesson three times. */
.inqout[hidden] { display: none !important; }
.inqout__scrim { position: absolute; inset: 0; background: rgba(20, 55, 42, .38); }
.inqout__panel {
  position: relative; width: 100%; max-height: 88vh; overflow-y: auto;
  box-sizing: border-box; padding: 0 0 var(--sab);
  background: var(--cream); border-radius: 12px 12px 0 0;
  box-shadow: 0 -14px 34px rgba(20, 55, 42, .22);
}
.inqout__grab { padding: 11px 0 0; display: flex; justify-content: center; }
.inqout__grab span { width: 38px; height: 4px; border-radius: 100px; background: rgba(27, 67, 50, .2); }
.inqout__head { padding: 14px 20px 0; }
.inqout__eyebrow { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.inqout__title {
  margin: 8px 0 0; font-family: var(--serif); font-weight: 400; font-size: 22px;
  line-height: 1.2; color: var(--ink);
}
.inqout__note { margin: 7px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.inqout__rows { padding: 15px 20px 0; display: flex; flex-direction: column; gap: 7px; }
.inqout__row {
  width: 100%; min-height: 52px; display: flex; align-items: center; gap: 12px;
  padding: 0 15px; box-sizing: border-box; text-align: left;
  background: var(--off-white); border: 1px solid rgba(27, 67, 50, .22); border-radius: 4px;
  font-family: inherit; cursor: pointer;
}
.inqout__row:hover { border-color: rgba(27, 67, 50, .45); }
.inqout__mark {
  flex-shrink: 0; width: 19px; height: 19px; border-radius: 50%; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--off-white);
  background: rgba(27, 67, 50, .12); border: 1px solid rgba(27, 67, 50, .22);
}
/* Signed up is the only one that carries a colour, because it is the only one
   that is an outcome you wanted. */
.inqout__mark--signed_up { background: #1B7F4B; border-color: #1B7F4B; }
.inqout__mark--deciding { background: rgba(201, 162, 39, .3); border-color: rgba(168, 132, 42, .5); }
.inqout__body { flex: 1; min-width: 0; }
.inqout__label { display: block; font-size: 14px; font-weight: 700; color: var(--ink); }
.inqout__sub { display: block; margin-top: 1px; font-size: 11px; color: var(--muted); }
.inqout__back { margin: 0; padding: 13px 20px 20px; text-align: center; }
.inqout__backlink {
  min-height: 44px; display: inline-flex; align-items: center;
  font-size: 12.5px; font-weight: 600; color: var(--muted); text-decoration: none;
}
.inqout__backlink span { border-bottom: 1px solid rgba(27, 67, 50, .3); padding-bottom: 2px; }

/* ── the log and its rates ──────────────────────────────────────────────────
   ⚠ THE RATES LIVE HERE AND NOT ON THE DASHBOARD. A rate is something you
   review monthly; it is not something you act on at 5pm. */
.inqlog__head { margin-bottom: 18px; }
.inqlog__eyebrow { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.inqlog__title { margin: 8px 0 0; font-family: var(--serif); font-weight: 400; font-size: 27px; color: var(--ink); }
.inqlog__sub { margin: 5px 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); }

.inqrates {
  margin-bottom: 20px; padding: 16px; background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 6px;
}
.inqrates__label { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.inqrates__headline { margin-top: 11px; display: flex; align-items: baseline; gap: 10px; }
.inqrates__pct { font-family: var(--serif); font-size: 32px; line-height: 1; color: var(--green-deep); }
.inqrates__of { flex: 1; min-width: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
/* The bar is a picture of the counts and nothing else: every width below comes
   off the same array the legend reads. */
.inqrates__bar { margin-top: 14px; display: flex; height: 9px; border-radius: 100px; overflow: hidden; }
.inqrates__seg { display: block; height: 100%; }
.inqrates__legend { margin: 12px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 7px; }
.inqrates__row { display: flex; align-items: center; gap: 9px; font-size: 12px; }
.inqrates__key { flex-shrink: 0; width: 9px; height: 9px; border-radius: 2px; }
.inqrates__name { flex: 1; color: var(--ink); }
.inqrates__share { flex-shrink: 0; color: var(--muted); }
.inqrates__count { flex-shrink: 0; min-width: 18px; text-align: right; font-weight: 700; color: var(--ink); }
.inqrates__foot {
  margin: 13px 0 0; padding-top: 12px; border-top: 1px solid rgba(27, 67, 50, .09);
  font-size: 11.5px; line-height: 1.6; color: var(--muted);
}

.inqlog { display: flex; flex-direction: column; gap: 10px; }
.inqlog__empty { margin: 0; font-size: 13.5px; color: var(--muted); }
.inqlog__row {
  padding: 14px 16px; background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .12); border-radius: 6px;
}
.inqlog__main { min-width: 0; }
.inqlog__name { margin: 0; display: flex; align-items: center; gap: 9px; font-size: 15px; font-weight: 700; color: var(--ink); }
.inqlog__age { margin-left: auto; font-size: 10.5px; font-weight: 400; color: var(--muted); }
.inqlog__sub2 { margin: 3px 0 0; font-size: 12.5px; color: var(--ink); }
.inqlog__msg { margin: 6px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink); }
.inqlog__contact { margin: 7px 0 0; font-size: 12px; color: var(--muted); }
.inqlog__contact a { color: var(--green-btn); }
.inqlog__stamp { margin: 2px 0 0; font-size: 11.5px; color: var(--muted); }
.inqlog__outcome {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(27, 67, 50, .09);
  display: flex; align-items: flex-end; gap: 9px; flex-wrap: wrap;
}
.inqlog__outcome .sel { flex: 1; min-width: 180px; }
.inqlog__outcome .lbl { width: 100%; }
.inqlog__save {
  flex-shrink: 0; min-height: 44px; padding: 0 16px; box-sizing: border-box;
  background: var(--green-btn); border: 0; border-radius: 4px;
  font-family: inherit; font-size: 12px; font-weight: 700; letter-spacing: .05em;
  color: var(--off-white); cursor: pointer;
}
.inqlog__student { min-height: 44px; display: inline-flex; align-items: center; font-size: 12.5px; color: var(--green-btn); }

/* ⚠ WIDTH AND A TALL SIDE, NEVER WIDTH ALONE. A phone rotated to landscape is
   about 844 x 390: on width alone it crosses 761 and the whole app flips to the
   desktop layout mid-gesture. The short side is the mechanism rather than a
   capability query, because an iPad with no trackpad reports `hover: none` and
   `pointer: coarse` exactly as a phone does, and the ruling says an iPad at that
   width still gets desktop. test/phone-shell.test.js refuses a desktop block
   keyed on width alone, and it caught this one. */
@media (min-width: 761px) and (min-height: 500px) {
  /* ── DESKTOP, FRAME 1e ────────────────────────────────────────────────────
     The card and the queue side by side, which is the fix for this option's one
     real cost: you can see that one parent wants SAT prep while another wants
     physics, and decide who to call first, without leaving the card. */
  .inq { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
  .inq__card { flex: 1.65 1 380px; }
  .inq__next { display: block; flex: 1 1 260px; }
  /* The phone's two buttons are the queue's stand-in and go when the real queue
     arrives; the log is reachable from the queue's own header. */
  .inq__foot { display: none; }
  /* ALL THREE ACTIONS ON ONE ROW. At 390 CALL takes a row and TEXT and EMAIL
     split the next. */
  .inq__acts { display: flex; gap: 8px; flex-wrap: wrap; }
  .inq__call { flex-shrink: 0; min-height: 44px; padding: 0 18px; }
  .inq__acts2 { margin-top: 0; flex: 0 1 auto; }
  .inq__act { padding: 0 16px; min-height: 44px; }
  /* THE ACTION ROW IS ONE LINE, with the explanation between the controls and
     the quiet pair at the right edge. */
  .inq__row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .inq__engageform { padding-top: 0; border-top: 0; }
  .inq__engaged { width: auto; min-height: 44px; padding: 0 16px; }
  .inq__minor { margin-top: 0; margin-left: auto; gap: 14px; }
  .inq__quiet--skip { margin-left: 0; }
  .inq__hint { order: 3; flex-basis: 100%; margin-top: 0; }
  .inq--empty { display: block; }

  /* The sheet becomes a centred modal, same five rows, nothing else changes. */
  .inqout { flex-direction: row; align-items: center; justify-content: center; }
  .inqout__panel {
    width: calc(100% - 52px); max-width: 470px; max-height: 82vh;
    border-radius: 6px; padding-bottom: 0; box-shadow: 0 18px 48px rgba(20, 55, 42, .3);
  }
  .inqout__grab { display: none; }
  .inqout__head { padding-top: 24px; }
}

/* NOTE ON PLACEMENT. Everything below the SURVEY SUBMIT marker is read as the
   in-flight overlay's own section by test/survey-submitting.test.js, which
   walks from that marker to the END OF THE FILE and refuses any selector that
   is not the overlay's. These two blocks were first appended after it and were
   caught by exactly that. New sections go ABOVE the overlay, not below it. */

/* ══ THE CALENDAR ══════════════════════════════════════════════════════════
   REBUILT VERBATIM from design_handoff_calendar_build/Calendar Flow.dc.html,
   view 1. Every number below is transcribed from that file's inline styles
   rather than chosen here: padding, gaps, type sizes, weights, tracking and
   colours. Where a value looks arbitrary it is because the mock says so.

   The six amendment rulings win over the mock and are marked A1..A6 at the
   rules they change.

   THE MOCK'S FRAME IS A 390px DEVICE with 18px side padding applied PER
   SECTION, not once on a wrapper. Transcribed the same way, so each block
   carries the padding the mock gave it.

   NOTHING HERE IS position: fixed OR sticky. The tab bar owns the bottom edge
   and the nav owns the top; after the pinning incident in CLAUDE.md this
   feature adds no third fixed element.

   NO RED. The palette is green, gold and cream. The two attendance colours
   belong to the attendance build and are deliberately not declared. */

/* Overrides .page's 26px side padding: the mock pads each section itself. */
.cal { max-width: 860px; padding: 0 0 24px; }

/* ── THE CALENDAR LEAVES THE PAGE SHELL, ON DESKTOP ONLY ───────────────────
   design_handoff_calendar_v2, and it is the first of the four things that
   bundle says were wrong with what shipped.

   ⚠ THE PROBLEM WAS NEVER THE GRID'S WIDTH. Every other portal page is a
   centred column -- 860px for a tutor, 1060px for an admin -- and the calendar
   inherited it, so no width the grid asked for could win against the cap above
   it. It was not too narrow for its container; it was in the wrong container.

   width 100%, about 28px of side padding, no max-width at all. On a 2400px
   monitor that is 2340px of grid instead of 1180.

   ⚠ THE EXCEPTION IS THIS PAGE AND THIS WIDTH. It is keyed on shape like every
   other desktop block here, so a phone in landscape keeps the mobile shell it
   has always had, and it is scoped to .page--cal so no other page can pick it
   up by being near it. SPEC records the exception. */
@media (min-width: 761px) and (min-height: 500px) {
  .page--cal { max-width: none; padding-left: 28px; padding-right: 28px; }
  .page--cal .cal { max-width: none; }

  /* ⚠ ADMIN TOOLS DOES NOT TAKE THIS OPT-OUT, and the absence is deliberate
     enough to be written down, because it was here and it was removed.

     The calendar's opt-out above is a real exception: a week grid has no useful
     capped form. Admin tools was given the same treatment for a while, on the
     instruction that the page should be nearly full width with no dead side
     margins, and it shipped that way in e800c7f. Trey has since reversed it:
     the page takes the standard portal shell cap, centred, like every other
     portal page, and the leftover is side margin by design.

     So there is nothing to write here. `.page--admin` keeps the 1080px cap it
     declares for itself, which is the same number .page--queue and .page--sent
     use, and every section inside it inherits that column.

     ⚠ AND THE MOCK IS NOT A WIDTH. Its artboard is 1682px, which is a drawing
     scale and not a requirement: proportion and hierarchy scale into the capped
     column. A future round that reads "match the mock" as "match 1682px" would
     un-cap this page for the third time. */
}

/* ── HEADER ─────────────────────────────────────────────────────────────── */
.cal__head {
  padding: 16px 18px 0;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 10px;
}
.cal__headtext { min-width: 0; }
.cal__eyebrow {
  display: block;
  font-size: 10px; font-weight: 600; letter-spacing: .12em; color: var(--gold-ink);
}
.cal__heading {
  margin: 3px 0 0;
  font-family: var(--serif); font-size: 23px; font-weight: 400; line-height: 1.2;
  color: var(--ink);
}
.cal__toggle {
  flex-shrink: 0;
  display: flex; gap: 2px; background: rgba(27,67,50,.06); border-radius: 5px; padding: 3px;
}
.cal__pill {
  min-height: 34px; display: inline-flex; align-items: center; padding: 0 10px;
  border-radius: 4px; font-size: 11.5px; font-weight: 600; color: var(--muted);
}
.cal__pill--on { background: var(--green-deep); font-weight: 700; color: #FDFCF9; }

/* ── ADMIN FILTERS. Pill triggers, and NOT on the day view (the mock gates
      them on `admin && v !== 'day'`). ─────────────────────────────────────── */
.cal__filters { padding: 12px 18px 0; display: flex; gap: 6px; }
.cal__filter { flex: 1; min-width: 0; display: block; }
.cal__select {
  flex: 1; width: 100%; min-width: 0;
  display: inline-flex; align-items: center; justify-content: space-between; gap: 6px;
  /* ⚠ 44px, NOT the mock's 38px, and this is the ONE number in this block that
     is not transcribed. The bundle contradicts itself: the chip is drawn at
     38px and the README's own acceptance list says "Every tap target >= 44px".
     Taken in the safer direction, and flagged in the readback rather than
     chosen quietly. */
  min-height: 44px;
  border-radius: 100px; box-sizing: border-box;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.2);
  font-family: var(--sans); font-weight: 600; color: var(--ink);
  /* 16px, not the mock's 11.5px: iOS zooms the viewport when a control under
     16px takes focus. The only place a real control departs from the mock's
     type, and it departs upward. */
  font-size: 16px;

  /* ── THE CLOSED VALUE IS CENTRED ──────────────────────────────────────────
     Ruled by Trey, Sept 12th 2026.

     ⚠ BOTH PROPERTIES, AND NEITHER IS REDUNDANT. text-align governs the closed
     control in WebKit; Chrome reads text-align-last for the selected value and
     leaves text-align to the option list. Written together, the closed pill is
     centred in both engines.

     ⚠ AND THIS IS THE CLOSED STATE ONLY, WHICH IS THE WHOLE OF WHAT CSS OWNS
     HERE. The OPEN list on iOS is the system picker: the OS draws it, styles it
     and aligns it, and nothing in this file reaches it. Do not add rules trying
     to, and do not read a centred pill as a promise about the wheel.

     ⚠ THE PADDING IS SYMMETRIC AND RESERVES THE ARROW, which is what makes the
     centre true rather than merely the centre of the box. appearance stays auto
     so the PLATFORM draws its own chevron at the right-hand edge; centring
     inside a box that includes the arrow's ground would put the optical centre
     off to the right and let a long value sit under the glyph. Equal padding on
     both sides means the text is centred in the space it can actually occupy.

     ⚠ 20px, AND THE NUMBER IS A COMPROMISE THAT IS WORTH KNOWING ABOUT.
     MEASURED in Chrome at 390: with 11px both sides, a centred "Billy
     Neveldine" at 114px does NOT reach the arrow, because Chrome reserves its
     own indicator space outside the author's padding box. So on Chrome nothing
     needed reserving at all. iOS Safari is the engine this was reported from
     and the one no headless run here can speak for; its indicator is about
     18px, so 20px clears it with a little margin.

     THE COST, stated because it is real: the pills are flex: 1 in a row that
     also holds Clear once a filter is on, so the content box is 132px
     unfiltered and 104px with Clear showing. Every real name fits the first;
     the longest of them clip by a few pixels in the second. The alternative is
     11px, which fits them all and trusts every engine to reserve its own arrow
     the way Chrome does. Ruled this way because the brief asks for no overlap at
     any value length, and a clip is a smaller fault than a label under a glyph.

     ⚠ AND THE OPEN LIST IS NOT OURS. On iOS the expanded picker is the system's
     own UI: the OS draws it, styles it, aligns it, and nothing in this file
     reaches it. Do not add rules trying to, and do not read a centred pill as a
     promise about the wheel. */
  text-align: center;
  text-align-last: center;
  padding: 0 20px;
}
/* Set: fills dark, so a filtered week never looks like the whole schedule. */
.cal__select--on { background: var(--green-deep); border-color: var(--green-deep); font-weight: 700; color: #FDFCF9; }
.cal__clear {
  flex-shrink: 0; min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 11px; border-radius: 100px; font-size: 11.5px; font-weight: 600; color: var(--green-btn);
}
.cal__filterbtn {
  min-height: 44px; padding: 0 11px; border: 0; border-radius: 100px;
  background: none; font-family: var(--sans); font-size: 11.5px; font-weight: 600;
  color: var(--green-btn); cursor: pointer;
}

/* ── DAY: the week strip ────────────────────────────────────────────────── */
.cal__strip { padding: 14px 18px 0; display: flex; gap: 5px; }
.cal__stripday {
  flex: 1; min-width: 0;
  min-height: 56px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; border-radius: 5px;
  background: rgba(27,67,50,.04);
}
.cal__stripday--past { background: var(--off-white); }
.cal__stripday--on { background: var(--green-deep); }
.cal__stripdow { font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--muted); }
.cal__stripnum { font-family: var(--serif); font-size: 15px; color: var(--ink); }
.cal__stripday--past .cal__stripnum { color: var(--ink); }
.cal__stripday .cal__stripnum { color: var(--muted); }
.cal__stripday--past .cal__stripnum, .cal__stripday--on .cal__stripnum { color: var(--ink); }
.cal__stripday--on .cal__stripdow { color: var(--gold-light); }
.cal__stripday--on .cal__stripnum { color: #FDFCF9; }

.cal__dayeyebrow {
  margin: 0; padding: 16px 18px 0;
  font-size: 10px; font-weight: 600; letter-spacing: .12em; color: var(--gold-ink);
}
/* ⚠ THE 8px BELONGS TO THE CARDS, NOT TO THE COLUMN, and that is the axis's
   doing. As a flex `gap` it also fell between the free hour rows, so three
   empty hours at the top of a day cost 156px and pushed the tutor's first real
   session towards the fold. The cards keep their 8px; the hairline rows stack
   at their own height and cost only what they are.

   .cal__list and .calrow--big are the DAY VIEW'S alone -- the week card and the
   month's opened day render the compact row -- so this reaches nothing else. */
.cal__list { padding: 12px 18px 0; display: flex; flex-direction: column; }

/* ── A FREE HOUR ON THE DAY VIEW ─────────────────────────────────────────────
   Ruled in: an empty hour is tappable and opens the create sheet on that day
   and that hour. It sits between the day's cards, carrying its numeral in the
   same 50px column the cards put their time in, so the hours line up down the
   page and the axis reads as one.

   ⚠ 44px, AND THAT IS A FLOOR RATHER THAN A LOOK. The cards are 8px apart and
   this is the smallest thing on the day; anything shorter is a target the
   thumb misses. The rest of the row is a hairline, so a free hour is quiet
   beside a booked one: the day should still read as what is ON, with the gaps
   available rather than advertised.

   NO FILL, NO BORDER, NO PLUS. The pointer on a desktop and the tap target on
   a phone are the whole affordance, exactly as on the week grid. */
.calfree {
  min-height: 44px; display: flex; align-items: center; gap: 11px;
  padding: 0 15px; box-sizing: border-box;
  cursor: pointer;
}
.calfree__hour {
  flex-shrink: 0; width: 50px;
  font-family: var(--serif); font-size: 16px; line-height: 1.1; color: var(--disabled);
}
/* ⚠ THE MERIDIEM, and it is not decoration. The axis now runs 7:00 AM to
   midnight, so it holds two 7s, two 8s, two 9s, two 10s and two 11s; a column
   of bare numerals across that range says nothing about which one you are
   looking at. Stacked under the numeral, which is the shape a booked row
   already uses for "4:00" over "PM", so the two line up down the page. */
.calfree__mer {
  display: block; margin-top: 1px;
  font-family: var(--sans); font-size: 9px; font-weight: 600; letter-spacing: .04em;
  color: var(--disabled);
}
.calfree__rule { flex: 1; height: 1px; background: rgba(27,67,50,.09); }

/* ── THE DAY ROW. A card: no border, radius 6, its own 8px gap. ─────────── */
.calrow--big {
  background: var(--off-white); border-radius: 6px; padding: 13px 15px;
  margin-bottom: 8px;
  display: flex; gap: 11px; align-items: center;
  /* The compact row's hairline and 46px floor are the WEEK card's, where rows
     butt against each other inside one card. A day card is its own card with an
     8px gap, so it carries neither. */
  border-top: 0; min-height: 0;
}
.calrow--big .calrow__time { flex-shrink: 0; width: 50px; display: block; }
.calrow--big .calrow__clock {
  display: block; font-family: var(--serif); font-size: 16px; line-height: 1.1; color: var(--green-deep);
}
/* The meridiem sits UNDER the hour, not beside it. */
.calrow--big .calrow__mer { display: block; font-size: 10px; font-weight: 600; color: var(--muted); }
.calrow--big .calrow__body { flex: 1; min-width: 0; }
.calrow--big .calrow__name { display: block; font-size: 14.5px; font-weight: 600; color: var(--ink); }
/* ⚠ EVERY META LINE IS A FIXED 16px BOX, whether or not it holds a pill.
   design_handoff_booking_grid part two: without it the pill's border and padding
   make that one row 3px taller than its neighbours, and this calendar has had a
   row-height drift once already. The height is the box's, not the content's. */
.calrow--big .calrow__meta {
  display: flex; align-items: center; gap: 6px;
  /* ⚠ A FIXED HEIGHT, NOT A MINIMUM, and the difference was measured: with
     min-height the pill's own box pushed its row to 18px against 17px for the
     rows beside it, which is the 1px version of exactly the drift this rule
     exists to stop. The box is 16px and the content fits inside it. */
  height: 16px; margin-top: 3px; font-size: 11.5px; color: var(--muted);
}
/* ⚠ THE PILL NEVER SHRINKS AND THE SUBJECT TRUNCATES, never wraps. The same
   rule the student name already follows. At 358px the pill and the subject
   together are 200px in a 154px column, and a line that wraps to three runs the
   row 39 percent taller than the ones around it. */
.calrow__booked {
  flex-shrink: 0; padding: 2px 8px; border-radius: 100px;
  border: 1px solid rgba(168, 132, 42, .4); background: rgba(201, 162, 39, .18);
  font-size: 8.5px; font-weight: 700;
  /* line-height 1, which is what makes the pill 14.5px and lets it sit inside
     the 16px box above without being the thing that sets its height. */
  letter-spacing: .07em; line-height: 1; color: var(--gold-ink);
  white-space: nowrap;
}
.calrow__metatext { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calrow__chev { flex-shrink: 0; font-size: 14px; color: var(--muted); }

/* ── WEEK: a card per day, no time axis. ────────────────────────────────── */
.cal__week { padding: 14px 18px 0; display: flex; flex-direction: column; gap: 9px; }
.cal__daycard { background: var(--off-white); border-radius: 6px; overflow: hidden; }
.cal__dayhead {
  margin: 0; padding: 9px 15px;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  background: rgba(27,67,50,.05); color: var(--muted);
}
.cal__dayhead--today { background: rgba(201,162,39,.18); color: var(--gold-ink); }
.cal__daylabel { min-width: 0; font-size: 10px; font-weight: 700; letter-spacing: .1em; }
.cal__daycount { flex-shrink: 0; font-size: 10px; font-weight: 600; }

/* The compact row, shared by the week and the month's opened day. */
.calrow {
  min-height: 46px; display: flex; align-items: center; gap: 10px;
  padding: 0 15px; box-sizing: border-box;
  border-top: 1px solid rgba(27,67,50,.06);
  background: var(--off-white);
}
.calrow__time { flex-shrink: 0; width: 44px; }
.calrow__clock { font-family: var(--serif); font-size: 15px; color: var(--green-deep); }
.calrow__mer { display: none; }
.calrow__body { flex: 1; min-width: 0; }
.calrow__name {
  font-size: 13.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;
}
.calrow__meta { display: none; }
.calrow__right { flex-shrink: 0; font-size: 11px; color: var(--muted); }
.calrow--big .calrow__right { display: none; }
.cal__dayempty, .cal__empty {
  margin: 0; min-height: 46px; display: flex; align-items: center;
  padding: 0 15px; box-sizing: border-box;
  border-top: 1px solid rgba(27,67,50,.06);
  font-size: 12.5px; color: var(--muted);
}
.cal__empty { border-top: 0; }

/* THE JUST-ADDED ROW keeps a gold left rule for the rest of the visit. Nothing
   is stored on the row; see the partial. Gold, like every other notable state
   here, and it is a rule rather than a fill so it reads as a marker rather than
   as a status. */
.calrow--fresh { box-shadow: inset 3px 0 0 var(--gold); }

/* GOLD, NEVER RED. A same-day cancellation keeps its place and says which
   lane it is in; the pill is the whole signal. */
.calrow--cancelled { background: var(--gold-tint); }
.calrow__tag { flex-shrink: 0; font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink); }

/* A6: the summary states the count and nothing more. No "N days clear". */
.cal__summaryrow { padding: 14px 18px 0; display: flex; align-items: center; gap: 12px; }
.cal__summary { flex: 1; min-width: 0; margin: 0; font-size: 11.5px; color: var(--muted); }

/* CONNECT CALENDAR: a line, not a card. A one-time setup should not own a block
   on a screen a tutor opens daily. */
.cal__connect {
  flex-shrink: 0; min-height: 44px; display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 600; color: var(--green-btn);
}

/* ── THE FOOTER ROW, ONE ROW ON ALL THREE VIEWS ───────────────────────────
   Recent changes left, Connect calendar right, below the schedule content and
   the same on Day, Week and Month. partials/cal-foot.ejs renders it once.

   ⚠ margin-left: auto ON THE RIGHT-HAND ITEM, NOT justify-content ON THE ROW,
   and the reason is the case that is easiest to miss: Recent changes is ADMIN
   ONLY, so for a tutor this row has exactly one child. space-between with one
   child puts that child on the LEFT, which would leave every tutor looking at
   the one layout this row exists to fix. An auto margin pushes it right whether
   or not anything precedes it.

   Same padding as .cal__summaryrow above, so the row sits on the page's
   existing 18px gutter rather than inventing one. The bottom padding is its
   own: it is the last thing in the scroller, and .cal__tail below carries the
   clearance for the pinned bar. */
.cal__foot {
  padding: 14px 18px 2px;
  display: flex; align-items: center; gap: 12px;
}
.cal__foot .cal__connect { margin-left: auto; }

/* ── THE DESKTOP CALENDAR, SECOND PASS ────────────────────────────────────
   design_handoff_calendar_v2. 2a the week, 2b the admin day, 2d the month.

   ⚠ THIS REPLACED THE HOUR-CELL WEEK GRID (.wgrid), which is the third of the
   four faults the bundle names: seven of seventeen rows carried nothing while
   the afternoon stacked three sessions into one 60px cell and truncated a name.
   A cell grid can only draw the hour a session starts in; these are placed
   blocks, at their own minute and as tall as they are long.

   ⚠ EVERY DESKTOP BLOCK IS KEYED ON SHAPE, not width alone, like the rest of
   this stylesheet since QC 8. A phone in landscape keeps the phone's views.

   NO RED. Status is deep green, gold and a faint outline; the four attendance
   colours are attendance's and stay inside .att-scope. */
.dweek, .dlanes, .dmonth { display: none; }

@media (min-width: 761px) and (min-height: 500px) {
  /* The phone's own week cards and month grid step aside for 2a and 2d, which
     every viewer gets. */
  .cal--shell .cal__week, .cal--shell .cal__monthwrap { display: none; }
  /* ⚠ THE DAY LIST STEPS ASIDE ONLY WHERE THE LANES REPLACE IT, which is the
     ADMIN day and nothing else. A tutor's desktop day is the list it has always
     been: lanes answer "who is free at five", which is not a question a tutor
     asks about their own calendar. */
  .cal--lanes .cal__list { display: none; }

  .dweek, .dmonth { display: block; }
  .dlanes { display: block; }

  /* ── 2a: THE WEEK ──────────────────────────────────────────────────────── */
  .dweek {
    margin: 14px 0 0;
    background: var(--off-white);
    border: 1px solid rgba(27,67,50,.14);
    border-radius: 8px;
    overflow: hidden;
  }
  .dweek__head { display: flex; border-bottom: 1px solid rgba(27,67,50,.12); }
  /* The hour gutter's width, shared by the head and the body so the columns
     line up with their labels. */
  .dweek__gut { flex-shrink: 0; width: 66px; }
  .dweek__gut--rows { position: relative; }
  .dweek__hour {
    position: absolute; right: 11px; transform: translateY(-50%);
    font-size: 10.5px; font-weight: 600; color: var(--muted);
    white-space: nowrap;
  }
  /* ⚠ minmax(0,1fr), NEVER BARE 1fr. `1fr` is `minmax(auto,1fr)`, so a cell
     holding nowrap text pushes its track wider than its share and the columns
     drift further apart the further right you look. The bundle calls this out
     as the one CSS rule worth carrying, and every grid here obeys it. */
  .dweek__days { flex: 1; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
  .dweek__day {
    padding: 11px 0; text-align: center;
    border-left: 1px solid rgba(27,67,50,.09);
    text-decoration: none;
  }
  .dweek__day--today { background: rgba(201,162,39,.09); }
  .dweek__dow {
    display: block; font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
  }
  .dweek__day--today .dweek__dow { color: var(--gold-ink); }
  .dweek__num {
    display: block; margin-top: 2px;
    font-family: var(--serif); font-size: 19px; color: var(--ink);
  }
  .dweek__count { display: block; font-size: 10px; color: #8A948C; }
  .dweek__day--today .dweek__count { color: var(--gold-ink); }

  /* The collapsed morning: one 33px strip that says what it covers. */
  .dweek__strip {
    min-height: 33px; box-sizing: border-box;
    display: flex; align-items: center; gap: 10px;
    padding: 6px 24px;
    background: rgba(27,67,50,.04);
    border-bottom: 1px solid rgba(27,67,50,.09);
  }
  .dweek__stripspan { font-size: 11px; font-weight: 600; color: var(--muted); }
  .dweek__stripnote { font-size: 11px; color: #8A948C; }
  .dweek__stripgo {
    margin-left: auto; font-size: 11.5px; font-weight: 600; color: var(--green-btn);
  }

  .dweek__body { display: flex; position: relative; }
  .dweek__stage {
    flex: 1; position: relative; touch-action: none;
    background-image: repeating-linear-gradient(
      to bottom,
      rgba(27,67,50,.09) 0 1px,
      transparent 1px var(--row-px, 64px)
    );
    background-position: 0 var(--chip-band, 32px);
  }
  .dweek__cols {
    position: absolute; inset: 0;
    display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  }
  .dweek__col { border-left: 1px solid rgba(27,67,50,.09); }
  .dweek__col--today { background: rgba(201,162,39,.05); }

  /* Invisible, and behind everything: the row already reads as empty and a
     second affordance drawn on every free hour would be sixty of them. */
  .dweek__cell { position: absolute; z-index: 0; }

  /* The out-of-window chip. Generated from the data, so nothing can hide. */
  .dweek__chip {
    position: absolute; top: 2px;
    min-height: 26px; box-sizing: border-box;
    display: flex; align-items: center; justify-content: center;
    background: rgba(27,67,50,.08); border: 1px solid rgba(27,67,50,.16);
    border-radius: 3px;
    font-size: 10.5px; font-weight: 600; color: var(--muted);
    text-decoration: none; z-index: 2;
  }
  /* ⚠ IN THE FOOT BAND, NOT ON THE LAST CARD. This was bottom:2px against a
     stage whose height stopped at the last row, so the chip sat on top of a 9 PM
     card: measured at 1440 it covered 24 of its own 26 pixels. The stage now
     reserves --chip-band-bottom below the rows, exactly as --chip-band reserves
     space above them for the earlier chip, and the chip sits inside it clear of
     every block.

     THE z-index IS RESTATED, not inherited from the base rule by luck: .dblock
     is z-index 1 and this has to win, which is the difference between a control
     and a decoration behind a card. */
  .dweek__chip--later {
    top: auto;
    bottom: calc((var(--chip-band-bottom, 32px) - 26px) / 2);
    z-index: 3;
  }

  /* ── THE BLOCK, SHARED BY 2a AND 2b ────────────────────────────────────── */
  .dblock {
    position: absolute;
    background: var(--green-deep);
    border-left: 3px solid var(--gold-light);
    border-radius: 3px;
    padding: 6px 8px; box-sizing: border-box;
    overflow: hidden; text-decoration: none;
    cursor: grab; user-select: none; z-index: 1;
  }
  /* ⚠ PARENT BOOKED IS A CORNER FOLD, NEVER A DISC. A small coloured circle on
     a session already means attendance on this calendar, and gold is one of its
     four values, so a gold dot would read as LATE. A triangle is not a disc.
     .dblock's own overflow:hidden is what clips it to the block's radius. */
  .dblock__fold {
    position: absolute; top: 0; right: 0; width: 0; height: 0;
    border-top: 14px solid var(--gold-light);
    border-left: 14px solid transparent;
  }
  /* ⚠ AND THE FOLD IS THE ONLY GOLD ON A MARKED BLOCK. The base left rule is
     --gold-light, which is the fold's own colour, so a marked block would
     otherwise carry two gold marks on 46px. Every block now takes its tutor's
     own rule instead, from services/tutorColors, which is where that is solved.

     THIS IS THE FALLBACK FOR A TUTOR WITH NO ORDINAL, which the backfill makes
     rare and does not make impossible. A faint white edge, not green: green is
     the attendance colour on this calendar and a green rule would read as
     present. It says "no colour", which is the truth about that tutor. */
  .dblock--booked { border-left-color: rgba(255, 255, 255, .28); }
  .dblock__time {
    display: block;
    font-size: 9.5px; font-weight: 700; color: var(--gold-light);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* ⚠ THE STUDENT IN FULL. "A block held a first name and two letters" is the
     fourth fault the bundle names: two students called Billy is not enough to
     act on. */
  .dblock__name {
    display: block; margin-top: 2px;
    font-size: 12px; font-weight: 700; color: var(--off-white);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* The tutor in full, and the subject. A substitute is named as the person
     teaching: "Abby Turk, sub". */
  .dblock__meta {
    flex: 1; min-width: 0;
    font-size: 10.5px; color: rgba(255,255,255,.72);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .dblock--group { background: var(--gold); border-left-color: var(--green-deep); }
  .dblock--group .dblock__time { color: #5C4A0E; }
  .dblock--group .dblock__name { color: var(--ink); }
  .dblock--group .dblock__meta { color: #5C4A0E; }
  .dblock--held {
    background: var(--off-white);
    border: 1.5px dashed rgba(168,132,42,.8); border-left-width: 1.5px;
  }
  .dblock--held .dblock__time,
  .dblock--held .dblock__name,
  .dblock--held .dblock__meta { color: var(--gold-ink); }
  /* Cancelled is a faint outline, never red. */
  .dblock--cancelled {
    background: rgba(27,67,50,.1);
    border: 1px solid rgba(27,67,50,.25); border-left-width: 1px;
  }
  .dblock--cancelled .dblock__time { color: var(--gold-ink); }
  .dblock--cancelled .dblock__name { color: var(--muted); text-decoration: line-through; }
  .dblock--cancelled .dblock__meta { color: var(--muted); }
  /* A short block drops the meta line rather than the state below it. */
  .dblock--short .dblock__meta { display: none; }

  /* ⚠ ATTENDANCE ON THE BLOCK, by ruling, superseding the week grid's
     no-marks design. The word goes when there is no room; THE MARK NEVER DOES,
     and the full state is always on the session sheet. */
  .dblock__foot {
    position: absolute; left: 8px; right: 6px; bottom: 5px;
    display: flex; align-items: center; gap: 6px;
  }
  .dblock__att {
    flex-shrink: 0;
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 9px; font-weight: 700; letter-spacing: .06em;
  }
  .dblock__attword { color: var(--gold-light); }
  .dblock--group .dblock__attword, .dblock--held .dblock__attword,
  .dblock--cancelled .dblock__attword { color: var(--gold-ink); }
  .dblock__attdisc {
    width: 15px; height: 15px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 9px; font-weight: 700; line-height: 1;
  }
  .dblock__att--due .dblock__attword { color: var(--gold-light); }
  .dblock--held .dblock__att--due .dblock__attword,
  .dblock--cancelled .dblock__att--due .dblock__attword { color: var(--gold-ink); }
  .dblock__att--present .dblock__attdisc { background: var(--att-present); color: #fff; }
  .dblock__att--late .dblock__attdisc { background: var(--att-late); color: var(--green-deep); }
  .dblock__att--noshow .dblock__attdisc { background: var(--att-noshow); color: #fff; }
  .dblock__att--cancelled .dblock__attdisc { background: var(--att-cancelled); color: #fff; }
  /* No room for the word: the mark alone, and the state on the sheet. */
  .dblock--short .dblock__attword { display: none; }

  /* THE DRAG GHOST. Dashed gold, at the destination, saying where it came
     from. Drawn into one server-rendered element so nothing is created
     mid-gesture. */
  .dghost {
    position: absolute; z-index: 6; pointer-events: none;
    background: rgba(250,247,240,.96);
    border: 1.5px dashed var(--gold);
    border-radius: 3px; padding: 5px 8px; box-sizing: border-box;
  }
  .dghost[hidden] { display: none; }
  .dghost__time { display: block; font-size: 9.5px; font-weight: 700; color: var(--gold-ink); }
  .dghost__name { display: block; margin-top: 2px; font-size: 12px; font-weight: 700; color: var(--ink); }
  .dghost__was { display: block; font-size: 10.5px; color: var(--gold-ink); }
  .dblock--dragging { opacity: .55; box-shadow: 0 8px 22px rgba(20,55,42,.35); cursor: grabbing; z-index: 5; }

  /* ── 2b: TUTOR LANES ───────────────────────────────────────────────────── */
  .dlanes {
    margin: 14px 0 0; position: relative; touch-action: none;
    background: var(--off-white);
    border: 1px solid rgba(27,67,50,.14);
    border-radius: 8px; overflow: hidden;
  }
  .dlanes__head {
    display: flex;
    background: rgba(27,67,50,.03);
    border-bottom: 1px solid rgba(27,67,50,.09);
  }
  .dlanes__who {
    flex-shrink: 0; width: 210px; padding: 9px 16px; box-sizing: border-box;
    font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
  }
  .dlanes__hours { flex: 1; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
  .dlanes__hour {
    padding: 9px 0; text-align: center;
    border-left: 1px solid rgba(27,67,50,.09);
    font-size: 10.5px; font-weight: 600; color: var(--muted);
  }
  .dlane { display: flex; border-bottom: 1px solid rgba(27,67,50,.09); }
  .dlane:last-of-type { border-bottom: 0; }
  .dlanes__whobox {
    display: flex; gap: 11px; align-items: flex-start;
    padding: 13px 16px; letter-spacing: normal;
  }
  .dlane__avatar {
    flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%;
    background: var(--green-deep); color: var(--gold-light);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--serif); font-size: 11.5px;
  }
  .dlane__whotext { min-width: 0; }
  .dlane__name { display: block; font-size: 13.5px; font-weight: 700; color: var(--ink); }
  .dlane__free { display: block; margin-top: 2px; font-size: 11px; color: var(--muted); }
  .dlane__track { flex: 1; position: relative; min-height: 88px; }
  .dlane__band {
    position: absolute; top: 0; bottom: 0;
    background: rgba(27,67,50,.035);
  }
  /* An empty lane is a feature, so it is not dimmed into looking broken. */
  .dlane--empty .dlane__name { color: var(--muted); }
  /* ⚠ ABOVE THE BLOCKS, AND OPAQUE. This carried NO z-index at all while
     .dblock carries 1, so it was painted behind the session blocks and read as
     "no affordance at all" on the day view even though it was in the markup the
     whole time. It is the one control that says a session exists outside the
     window, so it cannot be the thing a session hides.

     THE GROUND IS SOLID for the same reason: at 8% alpha over a dark green
     block it was unreadable even where it did show. */
  /* ⚠ IN THE FLOW OF THE LANE'S HEADER. It was absolute at the track's top right
     with NO z-index while .dblock carries 1, so it was painted behind the
     session blocks: the day view reported as having no overflow affordance at
     all while this was in the markup the whole time. In the header no block can
     reach it, so it needs neither a stacking fight nor an opaque ground to
     survive one. */
  .dlane__outside {
    display: inline-block; margin-top: 3px;
    padding: 3px 7px; border-radius: 3px;
    background: rgba(27,67,50,.08); border: 1px solid rgba(27,67,50,.28);
    font-size: 10px; font-weight: 600; color: var(--ink); text-decoration: none;
  }
  .dblock--lane { top: 12px; height: 64px; }

  /* ── 2d: THE MONTH ─────────────────────────────────────────────────────── */
  .dmonth {
    margin: 14px 0 0;
    background: var(--off-white);
    border: 1px solid rgba(27,67,50,.14);
    border-radius: 8px; overflow: hidden;
  }
  .dmonth__dow, .dmonth__grid {
    display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  }
  .dmonth__dow { background: rgba(27,67,50,.03); border-bottom: 1px solid rgba(27,67,50,.09); }
  .dmonth__dowlabel {
    padding: 9px 0; text-align: center;
    border-left: 1px solid rgba(27,67,50,.09);
    font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
  }
  .dmonth__cell {
    min-height: 182px; box-sizing: border-box;
    padding: 8px 8px 10px;
    border-left: 1px solid rgba(27,67,50,.09);
    border-top: 1px solid rgba(27,67,50,.09);
    text-decoration: none;
  }
  .dmonth__cell--today { background: rgba(201,162,39,.07); }
  .dmonth__cell--picked { background: rgba(201,162,39,.12); }
  .dmonth__top { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }
  .dmonth__num { font-family: var(--serif); font-size: 15px; color: var(--ink); }
  .dmonth__cell--out .dmonth__num { color: var(--disabled); }
  .dmonth__count { font-size: 10px; font-weight: 600; color: #8A948C; }
  .dmonth__list { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; }
  .dmonth__entry {
    display: flex; align-items: stretch;
    border-left: 3px solid var(--green-deep);
    border-radius: 0 3px 3px 0;
    background: var(--off-white);
    overflow: hidden;
  }
  .dmonth__time {
    flex-shrink: 0; display: flex; align-items: center; padding: 3px 5px;
    font-size: 9.5px; font-weight: 700; color: var(--green-deep);
  }
  .dmonth__name {
    flex: 1; min-width: 0; display: flex; align-items: center; padding: 3px 5px;
    font-size: 11px; font-weight: 600; color: var(--ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .dmonth__entry--held { border-left-style: dashed; }
  .dmonth__entry--cancelled .dmonth__name { color: var(--muted); text-decoration: line-through; }
  .dmonth__more {
    min-height: 32px; display: flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 600; color: var(--green-btn);
  }
  .dmonth__key {
    display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
    padding: 12px 18px;
    border-top: 1px solid rgba(27,67,50,.09);
  }
  .dmonth__keyitem {
    display: inline-flex; align-items: center; gap: 7px;
    font-size: 11.5px; color: var(--muted);
  }
  .dmonth__keyswatch {
    width: 22px; height: 12px; border-radius: 0 2px 2px 0;
    border-left: 3px solid var(--green-deep); box-sizing: border-box;
  }
  .dmonth__keynote { font-size: 11px; color: #8A948C; }
}

/* ── THE DESKTOP-ONLY HEADER CONTROLS ────────────────────────────────────────
   Every one of these is absent below the breakpoint, where the phone header is
   correct through QC 5 and is not to be touched. They are printed into the
   markup on both widths and chosen here, the same way .wgrid and .cal__week
   are, because the server cannot know the width. */
.cal__actions { flex-shrink: 0; display: flex; align-items: center; gap: 8px; }
.cal__filterstoggle, .cal__arrow, .cal__newtop, .cal__legend, .cal__legendgap,
.cal__pillletter { display: none; }
/* The desktop half of the two-week split described in the view. Both collapse
   to one value when GRID_WEEK_STARTS_ON goes back to 0. */
.cal__eyebrow--desk, .cal__summary--desk { display: none; }

@media (min-width: 761px) and (min-height: 500px) {
  /* ONE LINE, NOTHING CLIPPED. Everything on the row refuses to shrink and
     refuses to wrap; the heading's own min-width:0 lets it be the thing that
     gives if a name is ever long enough to need it. */
  .cal__head { padding: 24px 26px 0; align-items: flex-end; gap: 16px; }
  .cal__heading { font-size: 30px; line-height: 1.1; margin-top: 5px; white-space: nowrap; }
  .cal__eyebrow { font-size: 10.5px; }
  .cal__actions > * { flex-shrink: 0; white-space: nowrap; }

  .cal__filterstoggle {
    height: 38px; display: inline-flex; align-items: center; gap: 7px;
    padding: 0 13px; box-sizing: border-box;
    background: var(--off-white); border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
    font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: var(--ink);
    cursor: pointer;
  }
  /* Set, so a filtered week never looks like the whole schedule -- the same
     signal .cal__select--on carries on the phone. */
  .cal__filterstoggle--on {
    background: var(--green-deep); border-color: var(--green-deep); color: #FDFCF9;
  }
  .cal__filterscaret { font-size: 8px; color: var(--muted); }
  .cal__filterstoggle--on .cal__filterscaret { color: var(--gold-light); }

  /* THE PANEL THE BUTTON OPENS. Still one form and still the same two selects;
     only the box around them changes. `data-filters-shut` is scoped in here so
     that on a phone, where there is no button to reopen it, the attribute means
     nothing and the row is always the row it was. */
  .cal__filters[data-filters-shut] { display: none; }
  .cal__filters {
    margin: 10px 26px 0; padding: 12px 13px; gap: 8px;
    background: var(--off-white);
    border: 1px solid rgba(27,67,50,.16); border-radius: 5px;
  }
  .cal__select { min-height: 38px; border-radius: 4px; font-size: 13px; }
  .cal__clear { min-height: 38px; }

  /* `D W M` ON ADMIN, FULL WORDS ON TUTOR. The admin row also carries Filters,
     so the letters are what buys the space; a tutor's row has the room and the
     canonical says not to shorten it. */
  .cal__toggle--admin .cal__pillword { display: none; }
  .cal__toggle--admin .cal__pillletter { display: inline; }
  .cal__toggle--admin .cal__pill { padding: 0 9px; }
  .cal__pill { min-height: 32px; font-size: 11.5px; }

  .cal__arrow {
    height: 38px; width: 34px;
    display: inline-flex; align-items: center; justify-content: center;
    box-sizing: border-box;
    background: var(--off-white); border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
    font-size: 14px; color: var(--green-btn);
  }
  .cal__newtop {
    height: 38px; display: inline-flex; align-items: center; padding: 0 15px;
    box-sizing: border-box;
    background: var(--green-btn); border-radius: 4px;
    font-size: 12.5px; font-weight: 700; letter-spacing: .05em; color: #FDFCF9;
  }
  /* The pinned bar is the phone's. Exactly one NEW SESSION on any width. */
  .cal__newbar { display: none; }

  /* ── THE LEGEND ROW ──────────────────────────────────────────────────────
     Keys to colours only the grid draws, so they appear and disappear with it.
     The swatches repeat the grid's own values rather than referencing them,
     which is the one duplication here worth accepting: a key that drifted from
     what it keys would be worse than no key. */
  .cal__legend { display: inline-flex; align-items: center; gap: 16px; }
  .cal__key {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11px; color: var(--muted);
  }
  .cal__swatch { width: 9px; height: 9px; border-radius: 2px; box-sizing: border-box; }
  .cal__swatch--booked { background: var(--green-deep); }
  .cal__swatch--group { background: rgba(201,162,39,.5); }
  .cal__swatch--held { border: 1.5px dashed rgba(168,132,42,.8); }
  /* A faint outline, struck through on the block. Never red. */
  .cal__swatch--cancelled {
    background: rgba(27,67,50,.12); border: 1px solid rgba(27,67,50,.25);
  }
  .cal__legendgap { display: block; flex: 1; }
  .cal__summary { flex: 0 0 auto; }
  .cal__eyebrow--phone, .cal__summary--phone { display: none; }
  .cal__eyebrow--desk { display: block; }
  .cal__summary--desk { display: block; }
  /* ⚠ .cal__changes--desk AND .cal__summaryrow--phoneonly ARE GONE, with the
     markup that carried them. They were one half each of the same workaround:
     Recent changes printed twice, in the legend row for desktop and in its own
     row for the phone, with each hiding the other. There is now one footer row
     on both widths, so there is nothing to hide from anything. */
}

/* ── MONTH ──────────────────────────────────────────────────────────────── */
.cal__monthwrap { padding: 16px 18px 0; }
.cal__dow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 6px; }
.cal__dowlabel { text-align: center; font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--muted); }
.cal__month { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.cal__cell {
  min-height: 48px; border-radius: 4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  box-sizing: border-box; background: var(--off-white);
}
.cal__cell--blank { background: none; }
.cal__cell--past { background: rgba(27,67,50,.03); }
.cal__cellnum { font-family: var(--serif); font-size: 13px; color: var(--ink); }
.cal__cell--past .cal__cellnum { color: var(--disabled); }
.cal__cell--on { background: var(--green-deep); }
.cal__cell--on .cal__cellnum { color: #FDFCF9; font-weight: 700; }
/* A1: a small COUNT under the date, not dots. An empty day shows nothing. */
.cal__cellcount { font-size: 9.5px; font-weight: 600; color: var(--muted); line-height: 1; }
.cal__cell--past .cal__cellcount { color: var(--disabled); }
.cal__cell--on .cal__cellcount { color: rgba(253,252,249,.82); }

.cal__picked { padding: 14px 18px 0; }
.cal__pickedcard { background: var(--off-white); border-radius: 6px; overflow: hidden; }
.cal__pickedhead {
  margin: 0; padding: 10px 15px; background: rgba(201,162,39,.14);
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.cal__pickedlabel { font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink); }
.cal__pickedcount { font-size: 10px; font-weight: 600; color: var(--gold-ink); }

.cal__tail { height: 16px; }

/* Row that opens its session. A mirror row never does; see A5 and ruling 7. */
.calrow--open { cursor: pointer; }
.calrow--open:hover { background: var(--cream); }

/* Recent changes, admin only. The mock hangs it off the legend row; A4 removed
   that row, so it sits on a line of its own in the same 18px gutter. */
.cal__changes { font-size: 11.5px; font-weight: 600; color: var(--green-btn); }

/* The zone line on the gate 2 sheets. A3 removed it from the calendar itself
   and from the change feed; the create, session and cancel screens state which
   clock a time was entered in, which is a different job from captioning a
   listing. */
.cal__zone { margin: 10px 0 0; font-size: 11.5px; color: var(--muted); }

/* ── + NEW SESSION ────────────────────────────────────────────────────────
   Desktop: an ordinary block at the foot of the page. Mobile: a bar pinned
   directly above the tab bar, outside the scroller. See the phone shell below. */
.cal__newbar { padding: 14px 18px 0; }
.cal__new {
  min-height: 52px; display: flex; align-items: center; justify-content: center;
  background: var(--green-deep); border-radius: 4px;
  font-size: 13.5px; font-weight: 700; letter-spacing: .06em; color: #FDFCF9;
}

/* On desktop nothing is constrained: the page scrolls as any other page does
   and the scroller is a plain div. */
.cal__scroll { min-height: 0; }

/* ══ THE PHONE SHELL: HEADER, SCROLLER, PINNED BAR ═════════════════════════
   ⚠ SHELL GEOMETRY. This makes body a scroll container on calendar pages and
   pins a bar above the tab bar, and the tab bar is the one piece of shell this
   product has already been burned by. See CLAUDE.md. A green suite says nothing
   ELSE broke; it does not say the shell works. Device pass required.

   ── WHY :has() AND NOT A MEASURED NAV HEIGHT ─────────────────────────────
   The obvious build is a fixed container with `top: <nav height>`. At this
   width the nav is `height: auto` and WRAPS, so that number does not exist: it
   changes with the tutor's name and with the badge. Reading it needs a script,
   and a layout that breaks when a script fails is worse than one that scrolls.

   Making body a flex column instead lets the nav keep whatever height it turns
   out to have and gives the scroller the rest. No number, no script.

   SCOPED BY :has(.cal--shell), so no other page's scrolling changes. Where
   :has() is unsupported the block does not apply at all and the page scrolls as
   it does today with the bar in flow at the foot: degraded, not broken.

   ── THE BOTTOM MATH ──────────────────────────────────────────────────────
   The tab bar is fixed at 74px + var(--sab), the one number
   three other rules already derive from. This bar clears it with the same
   expression so the two move together, and the inset is what keeps the button
   clear of the iOS home indicator. */
@media (max-width: 760px), (max-height: 499px) {
  /* THE FALLBACK, and the fix for the bug that started this. `.cal` set
     `padding: 0 0 24px` at every width, which overrode the mobile `.page` rule
     that clears the fixed tab bar. The last rows of the week sat under the bar
     with no way to scroll to them. This restores the clearance for any browser
     that does not get the flex shell below. */
  .cal { padding-bottom: calc(60px + max(14px, var(--sab))); }

  body:has(.cal--shell) {
    /* ⚠ 100svh, for the reason set out on body:has(> .tabbar) above: svh does
       not move for the keyboard or the URL bar, so there is no value to go
       stale and nothing to recompute. This carried a JS-published --vh and the
       recompute-on-the-right-event approach failed twice. */
    height: 100dvh;
    height: 100svh;
    /* The PAGE does not scroll; the container does. That is what makes the
       pinned bar unreachable by scrolling rather than merely hard to reach. */
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  /* NO RULE FOR .nav HERE, deliberately. The obvious one is flex-shrink:0 to
     stop the bar being squeezed, and it is not needed: .cal--shell is
     `flex: 1` with `min-height: 0`, so the shell absorbs all the free space and
     shrinks to nothing before the nav gives up a pixel.

     It would also be actively harmful. A media query adds no specificity, so a
     .nav rule down here would become the LAST word on .nav and outrank the
     phone block above on source order alone. That has gone wrong twice in this
     stylesheet and test/training-mobile-layout pins it. */

  /* ⚠ EVERY RULE BELOW IS SCOPED UNDER body:has(...) ON PURPOSE. Written as a
     bare .cal--shell they would apply in a browser without :has() too, zeroing
     the fallback padding above while no flex shell existed to replace it, which
     is the exact bug this whole commit is fixing. */
  body:has(.cal--shell) .cal--shell {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    max-width: none; margin: 0; padding: 0;
  }

  /* THE ONLY SCROLLER. min-height:0 is load-bearing: without it a flex child
     refuses to shrink below its content, so the container grows instead of
     scrolling and the bar goes back off screen. */
  body:has(.cal--shell) .cal__scroll {
    flex: 1; min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* A scroll that reaches either end does not chain to the body. */
    overscroll-behavior: contain;
  }

  /* ⚠ THE DAY'S HEADER IS PINNED, and that is what makes the 3:00 rest possible
     without hiding the date. It was inside the scroller; see the view. flex-
     shrink: 0 or the column compresses it instead of scrolling the list, which
     is the same failure the pinned bar below records. */
  body:has(.cal--shell) .cal__daytop { flex-shrink: 0; }

  body:has(.cal--shell) .cal__newbar {
    flex-shrink: 0;
    padding: 14px 18px;
    background: var(--cream);
    border-top: 1px solid rgba(27,67,50,.12);
    box-shadow: 0 -8px 20px rgba(20,55,42,.07);
    margin-bottom: calc(60px + max(14px, var(--sab)));
  }

  /* The scroller ends where the bar begins, so the last row is never behind it
     and no page-level bottom padding is involved. .cal__tail is the mock's own
     16px of breathing room at the end of the list. */
  .cal__tail { height: 16px; }
}

/* ══ THE CALENDAR'S WRITE SURFACES ═════════════════════════════════════════
   The create and move sheet, the session sheet, the two cancel lanes and the
   change feed. Same tokens as the calendar above; no colour of its own.

   NOTHING HERE IS position: fixed OR sticky, and that is a deliberate
   departure from the design. The bundle pins + NEW SESSION above the tab bar.
   The tab bar is the one piece of shell geometry this product has already been
   burned by (see CLAUDE.md), and a second fixed element on the page whose
   middle scrolls most is not a trade worth making before a device pass says so.
   The button sits at the foot of the page instead, full width and 52px, and the
   tab bar keeps sole ownership of the bottom edge.

   NO RED. Cancelling is gold, because a cancellation is unusual, not an error.
*/

/* + NEW SESSION, full width, in the flow. */
.cal__new {
  display: flex; align-items: center; justify-content: center;
  margin-top: 20px; min-height: 52px; border-radius: 4px;
  background: var(--green-btn); color: #FDFCF9;
  font-size: 13px; font-weight: 700; letter-spacing: .05em;
}
.cal__changes { color: var(--green-btn); font-weight: 600; }

/* A row that opens its session. A mirror row never does. */
.calrow--open { cursor: pointer; }
.calrow--open:hover { background: var(--cream); }
.calrow__chev { font-size: 15px; color: var(--muted); }

/* ── THE CREATE AND MOVE SHEET ────────────────────────────────────────────
   Transcribed from Calendar Flow.dc.html sections 2, 3 and 5. Every number
   below is the mock's; where one looks arbitrary it is because the mock says so.

   A BOTTOM SHEET, not a page: full width, rounded at the top only, lifted off
   the page by a shadow thrown UPWARD, with a grab handle above the title. That
   is what makes it read as something raised over the calendar rather than a
   screen you navigated to.

   ⚠ max-width IS none, AND IT USED TO BE 390px. THAT WAS THE MOCK'S WIDTH, NOT
   A RULE. The canonical is drawn at 390 because that is the artboard, and the
   number was transcribed as a cap. A bottom sheet is full-bleed by definition:
   it is the screen's own width or it is a card.

   The cap is what QC 7 reported as the sheet being off-center with a visible
   left gutter. Every current iPhone is WIDER than 390 CSS px, so the sheet was
   pinned to 390 and centred inside the screen, and the gutter grew with the
   device. Measured, with the tab bar's own inset live:

       viewport 390  gutter 0     viewport 402  gutter 6   (iPhone 16)
       viewport 393  gutter 2     viewport 430  gutter 20  (Pro Max)

   Nothing above 761px changes: the desktop block caps the modal at 700 and
   comes later in the file. */
.cal--sheet {
  max-width: none; padding: 0 0 26px;
  background: var(--cream);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -14px 34px rgba(20,55,42,.16);
}
.sheet2__grab { padding: 12px 0 0; display: flex; justify-content: center; }
.sheet2__grab span { width: 38px; height: 4px; border-radius: 100px; background: rgba(27,67,50,.2); }

.sheet2__head {
  padding: 14px 20px 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}
.sheet2__title {
  margin: 0; font-family: var(--serif); font-size: 21px; font-weight: 400; color: var(--ink);
}
.sheet2__close {
  min-height: 44px; display: inline-flex; align-items: center;
  font-size: 13px; font-weight: 600; color: var(--muted);
}

/* ── STUDENT, AND THE LESSON TYPE WELDED TO ITS BOTTOM ─────────────────────
   One object with two rows sharing a border: who, then what kind. No second
   label; a faint tint and a hairline separate them, and the type row is 30px
   against the student row's 52 so it reads as the subordinate half. */
/* ⚠ THE INSET IS A TOKEN BECAUSE TWO RULES NEED THE SAME NUMBER. The student
   panel hangs from this field and has to start on its content edge; naming the
   value here means the panel references it rather than copying it, and the two
   cannot drift. See [data-droppanel="student"]. */
.sheet2__field { --sheet2-inset: 20px; padding: 8px var(--sheet2-inset) 0; }
.sheet2__label { display: block; font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted); }
.sheet2__box {
  margin-top: 7px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px; overflow: hidden;
}
.sheet2__row {
  min-height: 52px; display: flex; align-items: center; gap: 11px;
  padding: 0 14px; box-sizing: border-box; position: relative;
}
.sheet2__avatar {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  background: var(--green-deep); display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 11px; color: var(--gold-light);
}
.sheet2__who { flex: 1; min-width: 0; }
.sheet2__name { display: block; font-size: 14.5px; font-weight: 600; color: var(--ink); }
.sheet2__subject { display: block; margin-top: 1px; font-size: 11px; color: var(--muted); }
/* ⚠ ONE CARET, ONE RULE, FOR ALL THREE ROWS.
   The lesson-type row had a class of its own that no rule ever matched, so its
   glyph fell back to the inherited 16px body size in the inherited ink and sat
   beside an 11px muted one: a different, heavier arrow on the same object.
   Student, tutor and lesson type now share this class, so they cannot drift in
   size, weight or colour again.

   ALIGNED ON ONE VERTICAL LINE: every row that carries a caret uses the same
   14px right padding, and the caret is the last child of each, so the three sit
   on one up-and-down line down the box. */
.sheet2__caret {
  flex-shrink: 0; font-size: 11px; line-height: 1; font-weight: 400;
  color: var(--muted);
}

/* The real <select> lies invisibly over the row it drives, so the row is the
   mock's and the control is a native picker: keyboard reachable, screen reader
   reachable, no script. The 16px floor still applies to it even unseen, because
   iOS sizes its own picker from the element's computed font. */
.sheet2__select {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: 0; background: none;
  font-family: var(--sans); font-size: 16px; cursor: pointer;
}
.sheet2__type {
  min-height: 30px; display: flex; align-items: center; justify-content: space-between; gap: 6px;
  padding: 0 14px; box-sizing: border-box; position: relative;
  border-top: 1px solid rgba(27,67,50,.14); background: rgba(27,67,50,.03);
}
.sheet2__typetext { font-size: 12.5px; color: var(--ink); }

/* ── GROUP: THE SAME OBJECT WITH ITS TOP ROW GROWN ───────────────────────
   The type row stays welded to the bottom; only the top half changes, from one
   name to a wrap of chips. */
.sheet2__grouphead { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.sheet2__groupcount { font-size: 11px; color: var(--muted); }
.sheet2__chips { padding: 9px; display: flex; flex-wrap: wrap; gap: 6px; }
.sheet2__chip {
  min-height: 44px; display: inline-flex; align-items: center; gap: 8px;
  padding: 0 12px 0 5px; border-radius: 100px; background: var(--green-deep);
  box-sizing: border-box;
}
.sheet2__chipav {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  background: var(--gold-light); display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 11px; color: var(--green-deep);
}
.sheet2__chiptext { display: flex; flex-direction: column; line-height: 1.2; }
.sheet2__chipname { font-size: 12.5px; font-weight: 600; color: #FDFCF9; }
.sheet2__chipsub { font-size: 9.5px; color: rgba(255,255,255,.66); }
.sheet2__chipx {
  flex-shrink: 0; min-width: 24px; display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; color: rgba(255,255,255,.6);
}
/* Dashed, because it is an invitation rather than a chip that exists yet. */
.sheet2__add {
  min-height: 44px; display: inline-flex; align-items: center; padding: 0 14px;
  border-radius: 100px; border: 1px dashed rgba(27,67,50,.3);
  font-size: 12.5px; font-weight: 600; color: var(--green-btn);
  box-sizing: border-box; position: relative;
}
.sheet2__grouphint { margin: 8px 0 0; font-size: 11px; line-height: 1.5; color: var(--muted); }

/* ── CURRENT SCHEDULE ────────────────────────────────────────────────────── */
.sheet2__sched { padding: 20px 20px 0; }
.sheet2__schedlabel { display: block; font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink); }
.sheet2__daybar { margin-top: 2px; display: flex; align-items: center; gap: 2px; }
/* 22px, down from 26. The four pixels each side are what the whole date needed
   to sit inside the row's padding rather than 4px past it: at 17px the text is
   230 and the row has 320, so the arrows and the picker had to give back 8.
   ⚠ THE 44px HEIGHT IS UNTOUCHED. These stay full-height tap targets; only the
   horizontal padding around the chevron came off. */
.sheet2__step {
  flex-shrink: 0; width: 22px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; font-family: var(--sans); font-size: 16px;
  color: var(--green-btn); cursor: pointer;
}
/* ⚠ THE DATE RENDERS WHOLE. RULED, AND IT REVERSES WHAT WAS HERE BEFORE.
   The previous fix let the date ellipsize so the picker could not be pushed off
   the edge, and it worked: nothing overflowed. What it produced instead was
   "Wednesday, S..." on a 360px phone, a header whose whole job is to say which
   day is being booked and which had stopped saying it.

   ⚠ THE TRUNCATION WAS NOT THE FONT SIZE. It was `.sheet2__spacer { flex: 1 }`
   sitting in the same row as `.sheet2__dayname { flex: 1 }`: two equal growers
   split the free space, so the date was handed 113px of the 226 available and
   ellipsized inside it while an empty span held the other half. Measured, not
   reasoned: 270px of text in a 113px box at 360, 390 and 430 alike, which is
   why the wider phones truncated exactly as badly as the narrow one.

   THE ORDER THINGS GIVE IN, now:
     1. the spacer stops competing. It only fills what is left over, which is
        all it was ever for, and the picker is pinned right by margin instead.
     2. the date takes its natural width and is never allowed to shrink.
     3. the font steps 20px to 17px, which is the one deliberate reduction.

   AND THE PICKER GAINED ITS TAP TARGET. It was 34px square, under the 44px
   floor this product holds everywhere else; it is now 44px tall with the tinted
   chip drawn at 34 inside it, so the target grew without the row's width
   budget moving by a pixel. */
.sheet2__dayname {
  flex: 0 0 auto; min-width: 0;
  font-family: var(--serif); font-size: 17px; line-height: 44px; color: var(--ink);
  white-space: nowrap;
}
/* Left in the markup and given nothing to grow against the date for. It still
   separates the arrows from the picker on a wide sheet, where there IS free
   space; on a phone there is none and it takes none. */
.sheet2__spacer { flex: 1 1 0; min-width: 0; }
.sheet2__pick {
  flex-shrink: 0; width: 34px; height: 44px; margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; cursor: pointer; position: relative;
}
/* The tint is the CHIP, not the target. Drawn behind the icon at the size the
   mock gives it while the element around it stays tappable. */
.sheet2__pick::before {
  content: ''; position: absolute; left: 0; right: 0; top: 5px; height: 34px;
  border-radius: 4px; background: rgba(27,67,50,.06);
}
.sheet2__pick svg { position: relative; }
.sheet2__pickinput {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: 0; font-size: 16px; cursor: pointer;
}

/* ── THE READ-ONLY DAY. A CSS grid, 32px of hour number against the slot. ──
   ⚠ FIXED HEIGHT, SCROLLS INSIDE, OPENS ON 3. Six of the seventeen hours show at
   rest, the sheet never changes height, and overscroll-behavior stops the page
   moving when the grid reaches either end. Nothing in here is a control. */
.sheet2__gridwrap { padding: 12px 20px 0; }
.sheet2__grid {
  height: 264px; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--off-white); border-radius: 6px;
  display: grid; grid-template-columns: 32px 1fr;
}
.sheet2__hournum {
  min-height: 44px; box-sizing: border-box; padding: 12px 6px 0 0;
  text-align: right; font-size: 10.5px; color: var(--muted);
  border-bottom: 1px solid rgba(27,67,50,.06);
}
.sheet2__hournum--picked { color: var(--gold-ink); font-weight: 700; }
/* The sheet's column is 34px, so the meridiem stacks under the numeral rather
   than sitting beside it. Same reason as the day view's: 7:00 AM to midnight
   contains each numeral twice. */
.sheet2__hourmer {
  display: block; margin-top: 1px;
  font-size: 7.5px; font-weight: 600; letter-spacing: .04em; color: var(--disabled);
}
.sheet2__slot {
  min-height: 44px; box-sizing: border-box; padding: 3px;
  border-bottom: 1px solid rgba(27,67,50,.06);
  /* The positioning context for blocks placed by their minutes. */
  position: relative;
}
/* ⚠ PLACED BY ITS MINUTES, not stacked at the top of its hour. `top` and
   `height` come from the row as percentages, so a 7:30 start sits half way
   down and a 30 minute session is half a row tall. A tutor reading 7:00 off
   the grid for a 7:30 booking is a missed session, which is what this fixes. */
.sheet2__block {
  position: absolute; left: 3px; right: 3px;
  border-radius: 4px; display: flex; align-items: center;
  padding: 0 11px; box-sizing: border-box; overflow: hidden;
}
/* Pale and untappable. #3E4A43 is the mock's ink for a block you cannot press:
   dark enough to read, flat enough not to invite a tap. */
.sheet2__block--taken { background: rgba(27,67,50,.09); }
.sheet2__block--taken span { font-size: 11.5px; font-weight: 600; color: #3E4A43; }
.sheet2__block--new { background: var(--green-deep); justify-content: space-between; }
.sheet2__block--new .sheet2__blockname { font-size: 11.5px; font-weight: 700; color: #FDFCF9; }
.sheet2__blocktag { font-size: 9.5px; font-weight: 700; letter-spacing: .06em; color: var(--gold-light); }
/* The session at its CURRENT hour during a move. Seeing both ends at once is
   what a confirmation sentence cannot do. */
.sheet2__block--now { border: 1px dashed var(--gold); background: none; justify-content: space-between; }
.sheet2__block--now .sheet2__blockname { font-size: 11.5px; font-weight: 600; color: var(--gold-ink); }
.sheet2__block--now .sheet2__blocktag { color: var(--gold-ink); }

/* ── THE THREE CONTROLS, ON ONE LINE ─────────────────────────────────────── */
.sheet2__three { padding: 18px 20px 0; display: flex; gap: 8px; }
/* position: relative so the drawn panel below hangs off the control rather than
   off the sheet. Without it the panel would be placed against whatever distant
   ancestor happened to be positioned, which is the bug the grid's anchor row
   already taught this file once. */
.sheet2__ctl { flex: 1; min-width: 0; position: relative; }
.sheet2__ctl--repeat { flex: 0 0 76px; width: 76px; }
.sheet2__ctllabel { display: block; font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
.sheet2__control {
  margin-top: 6px; min-height: 48px;
  display: flex; align-items: center; justify-content: space-between; gap: 4px;
  padding: 0 10px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px; box-sizing: border-box;
  position: relative;
}
.sheet2__value { font-size: 13.5px; color: var(--ink); }
.sheet2__ctlcaret { flex-shrink: 0; font-size: 9px; color: var(--muted); }

/* REPEAT IS A SWITCH, not a dropdown. It answers yes or no, and a two-item
   select for a yes-or-no question is a control pretending to be a choice. */
.sheet2__switchwrap { margin-top: 6px; min-height: 48px; display: flex; align-items: center; justify-content: center; }
.sheet2__switch {
  width: 52px; height: 31px; border-radius: 100px;
  background: rgba(27,67,50,.16); border: 1px solid rgba(27,67,50,.2);
  display: flex; align-items: center; justify-content: flex-start;
  padding: 0 3px; box-sizing: border-box; cursor: pointer; position: relative;
}
.sheet2__switch--on { background: var(--green-btn); border-color: var(--green-btn); justify-content: flex-end; }
.sheet2__knob { width: 25px; height: 25px; border-radius: 50%; background: var(--off-white); box-shadow: 0 1px 3px rgba(20,55,42,.2); }
.sheet2__switch--on .sheet2__knob { box-shadow: 0 1px 3px rgba(20,55,42,.3); }
.sheet2__switchbtn { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; border: 0; cursor: pointer; }

/* ⚠ LOCKED OFF WHILE THE HOLD IS TICKED. It carries no link at all, so this is
   presentation for a control that is genuinely absent rather than a disabled
   look over a live one: a hold is one row and createHold takes no recurrence,
   so a recurring hold is a state the server cannot write. The note says which,
   because a dead switch with no reason reads as a bug. */
.sheet2__switchwrap--locked { flex-direction: column; gap: 5px; margin-top: 2px; }
.sheet2__switch--locked { opacity: .45; cursor: default; }
.sheet2__switchnote { font-size: 9.5px; line-height: 1.2; text-align: center; color: var(--muted); }

/* ── THE RECURRENCE BLOCK, under the row and not on a second screen ───────── */
.sheet2__repeat { padding: 16px 20px 0; }
.sheet2__repeatcard { background: var(--off-white); border-radius: 6px; padding: 16px 17px; }
/* The heading restates the OUTCOME in words as the choices change, so the block
   always says what it will do rather than what its settings are. */
.sheet2__repeathead { font-family: var(--serif); font-size: 18px; color: var(--ink); }
.sheet2__repeatlabel { display: block; margin-top: 16px; font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
.sheet2__pair { margin-top: 6px; display: flex; gap: 2px; background: rgba(27,67,50,.04); border-radius: 5px; padding: 3px; }
.sheet2__pairbtn {
  flex: 1; min-height: 44px; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 4px; background: none;
  font-family: var(--sans); font-size: 12.5px; color: var(--ink); cursor: pointer;
}
.sheet2__pairbtn--on { background: var(--green-deep); font-weight: 700; color: #FDFCF9; }
/* One real date makes "no end date" concrete without turning the block into a
   calendar. */
/* ── THE DATE SHEET, STATE E ───────────────────────────────────────────────
   design_handoff_make_recurring. It REPLACES the sheet it came from rather than
   stacking on it, so it is a page with the same bottom-sheet frame as the
   cancel and session sheets, and it borrows their head and foot spacing.

   ⚠ NO CAPTION UNDER THE HEADING. The bundle draws one; it is cut by ruling and
   nothing replaces it. The dimmed days say it. */
.cal--until .until__head { padding: 14px 20px 0; }
.until__title {
  margin: 0; font-family: var(--serif); font-size: 20px; font-weight: 400;
  line-height: 1.25; color: var(--ink);
}
.until__cal {
  margin: 14px 20px 0;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.14); border-radius: 6px;
  padding: 12px 13px 14px;
}
.until__monthrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.until__month { font-family: var(--serif); font-size: 17px; color: var(--ink); }
/* 36px, the bundle's own, and the only controls on this sheet that are not the
   days themselves. */
.until__arrow {
  flex-shrink: 0; width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px; font-size: 16px; color: var(--green-btn); text-decoration: none;
}
.until__dows, .until__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.until__dows { margin-top: 8px; }
.until__dows span {
  height: 22px; display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; letter-spacing: .06em; color: var(--muted);
}
.until__grid { margin-top: 2px; }
/* ⚠ 40px, AND IT IS A RULED EXCEPTION TO THE 44px FLOOR, not an oversight. The
   bundle draws the cell at 40 and also says every tap target is 44 or larger;
   the two cannot both hold and the drawn size won.

   WHAT MAKES 40 DEFENSIBLE HERE AND NOWHERE ELSE: a calendar cell is not a lone
   control. It sits in a seven-wide row about 46px across, so the target is
   46 x 40 rather than 40 x 40, and a dead day is a <span> rather than a link --
   the cost of missing is nothing happening, not the wrong date being chosen.
   The numerals, the Custom box and Cancel are 44 and the buttons are 48. The
   month arrows are the bundle's 36 and are flagged in SPEC as the one thing
   here worth revisiting if it misses in use. SPEC carries the exception and its
   fence. */
.until__day {
  height: 40px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px; font-size: 13.5px; font-weight: 600; color: var(--ink);
  text-decoration: none;
}
.until__day--on { background: var(--green-deep); font-weight: 700; color: var(--off-white); }
/* Shown, and unpickable. Not a link, so there is nothing to mis-tap. */
.until__day--off { color: var(--disabled); font-weight: 400; }
.until__note { margin: 14px 20px 0; padding-left: 11px; border-left: 2px solid var(--gold); }
.until__note p { margin: 0; font-size: 11.5px; line-height: 1.6; color: var(--gold-ink); }
.until__foot { padding: 14px 20px 26px; }
/* ⚠ THE FILL DIED AT COMPUTED-VALUE TIME, IT WAS NEVER OUTRANKED. This said
   `background: var(--green)` and there is no --green token in the palette:
   the greens are --green-deep and --green-btn. A var() naming an undefined
   property with no fallback makes the WHOLE declaration invalid at computed-
   value time, so background fell to its initial `transparent` while
   `color: var(--off-white)` resolved fine and stayed white. White text, no
   fill, on the #FAF7F0 sheet. Nothing was competing with it: .until__go is
   the only rule in the file that touches background on this element.

   It is the palette's own button treatment now, the same three declarations
   .btn carries, so this primary matches every other primary in the flow. */
.until__go {
  min-height: 48px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  background: var(--green-btn); border: 1.5px solid var(--gold); border-radius: 4px;
  font-size: 12.5px; font-weight: 700; letter-spacing: .06em; color: var(--off-white);
  text-decoration: none;
}
.until__pick { margin: 0; text-align: center; font-size: 12.5px; color: var(--muted); }
.until__cancel {
  margin-top: 9px; min-height: 44px;
  display: flex; align-items: center; justify-content: center;
  font-size: 12.5px; font-weight: 600; color: var(--muted); text-decoration: none;
}

/* ── THE WEEKS CONTROL ─────────────────────────────────────────────────────
   design_handoff_make_recurring, and the same numbers in all three surfaces it
   renders in: the session sheet's card, + New Session on the phone and + New
   Session in the desktop modal. The bundle says if one changes, all three
   change, so there is one partial and one block of CSS.

   THE SOFT CONTAINER IS THE SHEET'S OWN, the same grid HOW LONG already uses,
   so the two controls under the toggle match rather than nearly matching. */
.wrec__labelrow {
  margin-top: 16px;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}
.wrec__label { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
/* The resolved end date, in the serif, because it is a date being read rather
   than a label being scanned. */
.wrec__value { font-family: var(--serif); font-size: 15px; color: var(--green-deep); }
.wrec__grid {
  margin-top: 6px;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 2px;
  background: rgba(27,67,50,.04); border-radius: 5px; padding: 3px;
}
.wrec__cell {
  min-height: 44px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px; font-size: 14px; color: var(--ink);
  text-decoration: none;
}
.wrec__cell--on { background: var(--green-deep); font-weight: 700; color: var(--off-white); }
.wrec__custom {
  margin-top: 7px;
  min-height: 44px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
  font-size: 12.5px; font-weight: 600; color: var(--green-btn); text-decoration: none;
}
/* The editor's line: every duration is counted from today, and a chip that did
   not say so would be ambiguous by five weeks. */
.wrec__foot { margin: 6px 0 0; font-size: 11px; line-height: 1.5; color: var(--muted); }

/* ⚠ 36px IN THE DESKTOP MODAL, 44 EVERYWHERE ELSE, which is the bundle's own
   split: the phone floor is a thumb, and the modal is a pointer. The session
   sheet keeps 44 at every width, so this is scoped to the flag the create
   sheet passes rather than to the width alone.

   ⚠ AND IT IS KEYED ON SHAPE, NOT WIDTH, like every other desktop block here.
   A phone in landscape is 844 x 390: on width alone it crossed 761 and would
   have had its tap targets shrunk to 36px mid-gesture, which is the QC 8 bug in
   miniature. test/phone-shell caught this the first time the suite ran over it. */
@media (min-width: 761px) and (min-height: 500px) {
  .wrec--tight .wrec__cell, .wrec--tight .wrec__custom { min-height: 36px; }
}

.sheet2__note { margin: 14px 0 0; padding-left: 12px; border-left: 2px solid var(--gold); }
.sheet2__note p { margin: 0; font-size: 12px; line-height: 1.6; color: var(--gold-ink); }

/* ── THE HOLD ROW ────────────────────────────────────────────────────────
   The mock's checkbox shape, at caption weight with a small box, because a hold
   is the quiet option on this sheet and not a second primary action. */
.sheet2__holdwrap { padding: 16px 20px 0; }
.sheet2__hold {
  display: flex; align-items: flex-start; gap: 11px; min-height: 44px;
  position: relative; cursor: pointer;
}
.sheet2__holdbox {
  flex-shrink: 0; width: 17px; height: 17px; margin-top: 2px;
  border: 1.5px solid rgba(27,67,50,.35); border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: #FDFCF9; box-sizing: border-box;
}
.sheet2__holdbox--on { background: var(--green-deep); border-color: var(--green-deep); }
/* The shared tick SVG, sized for the 17px box. It draws in currentColor, which
   the box already sets to the off-white the checked state needs. */
.sheet2__holdbox .tick { width: 11px; height: 11px; vertical-align: 0; }
.sheet2__holdtext { min-width: 0; }
.sheet2__holdtitle { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink); }
.sheet2__holdsub { display: block; margin-top: 2px; font-size: 11px; line-height: 1.45; color: var(--muted); }
.sheet2__holdbtn { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; border: 0; cursor: pointer; }

.sheet2__taken { margin: 10px 20px 0; font-size: 11.5px; color: var(--muted); }

/* ── NO STUDENT YET ──────────────────────────────────────────────────────
   The placeholder state, ruled: the sheet opens with nobody chosen.

   The avatar keeps its size and loses its fill, so the row does not change
   height when a student arrives and nothing below it moves. */
.sheet2__avatar--empty {
  background: none;
  border: 1px dashed rgba(27,67,50,.28);
}
.sheet2__name--placeholder { color: var(--faint); font-weight: 400; }

/* Where the button will be. Not a disabled button: this flow has no
   blocked-button state, and the line says what is missing rather than leaving a
   gap where a control was. */
.sheet2__needs {
  margin: 0; padding: 15px 0; text-align: center;
  font-size: 12.5px; color: var(--muted);
}

/* ══ THE DRAWN DROPDOWNS ═══════════════════════════════════════════════════
   TIME and HOW LONG, as the canonical draws them open. Ruled in QC 9, and it
   overrules the native <select> the sheet shipped with: an option carries one
   string and no styling, so the MIN ONLY caption could not be its own colour,
   the taken-hours row could not be a row at all, and Custom time could not show
   its value on the right.

   THE CANONICAL'S OWN NUMBERS: #FDFCF9 ground, a 6px radius, a 1px rule at
   rgba(27,67,50,.18), the 0 10px 28px shadow, 48px rows, 16px of side padding,
   a hairline between rows, the selected row filled #14372A with a #E3B53A tick,
   and the tint rows at rgba(27,67,50,.04).

   ⚠ THE DATE PICKER IS NOT HERE. The month calendar stays native; nothing in
   this block replaces it and no drawn replacement is ordered. */
.sheet2__droptrigger {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: 0; background: none; cursor: pointer;
  /* The 16px floor still applies even unseen: iOS zooms a page whose control
     font is smaller when it takes focus. */
  font-family: var(--sans); font-size: 16px;
}

/* THE PANEL. Absolute, so opening one does not move the controls under it or
   change the sheet's height -- the sheet never changes height, which is the
   whole reason the day grid is a fixed scroller. */
.sheet2__drop {
  position: absolute; z-index: 30; top: calc(100% + 4px);
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18);
  border-radius: 6px;
  box-shadow: 0 10px 28px rgba(20,55,42,.16);
  overflow: hidden auto;
  /* Never taller than the thumb can reach. The canonical draws seven rows and
     stops; more than that scrolls inside the panel rather than off the sheet. */
  max-height: 336px;
  overscroll-behavior: contain;
}
.sheet2__drop[hidden] { display: none; }

/* ⚠ WITHOUT SCRIPT THE PANEL IS SIMPLY OPEN, and that is the whole no-JS story.
   Every row is a real link with a real href, so a list that is merely visible
   is a working control: tapping a row navigates and the server renders the
   sheet with that time. Nothing needs a handler.

   THE ALTERNATIVE WAS A <noscript> <select>, AND IT WAS WORSE. It puts a SECOND
   control named `time` in the markup, which is the exact shape of the bug that
   cost QC 1 a round: two controls of one name arrive as an array and every
   reader that stringifies it falls back to a default. That it happens to be
   inert while scripting is on is not a property worth resting on.

   Where `scripting` is not supported the query does not match and a no-JS
   visitor gets what they get today, which is the sheet's default time and its
   <noscript> submit button. Degraded, not broken. */
@media (scripting: none) {
  .sheet2__drop[hidden] { display: block; position: static; max-height: none; box-shadow: none; }
  .sheet2__droptrigger, .sheet2__custominput { display: none; }
}

/* ⚠ THE PANEL IS WIDER THAN THE CONTROL IT HANGS FROM, and it has to be. The
   canonical draws the time list at 250px and the length list at 190px, while
   TIME's own closed box is about a third of a 390px screen. Constrained to the
   control it came from, "Custom time" wrapped onto two lines and the MIN ONLY
   caption had nowhere to sit.

   WHICH EDGE THEY HANG FROM IS FIXED RATHER THAN MEASURED. TIME is the left of
   the three controls, so its panel opens leftward and has room; HOW LONG is the
   middle one and a 190px panel opened from its left edge runs off the screen,
   so it opens rightward. Two rules and no measurement, which is one fewer thing
   to be wrong on a width nobody tested. Both are clamped to the viewport. */
[data-droppanel="time"] { left: 0; right: auto; min-width: 250px; }
[data-droppanel="minutes"] { right: 0; left: auto; min-width: 190px; }
.sheet2__drop { max-width: calc(100vw - 32px); }
/* Flipped by the script when there is no room below; see openDrop. */
.sheet2__drop--up { top: auto; bottom: calc(100% + 4px); }

.sheet2__droprow {
  min-height: 48px; box-sizing: border-box;
  display: flex; align-items: center; gap: 10px;
  padding: 0 16px;
  border-bottom: 1px solid rgba(27,67,50,.07);
  font-size: 14.5px; color: var(--ink); text-decoration: none;
  position: relative;
}
.sheet2__droprow:last-child { border-bottom: 0; }
.sheet2__droplabel { flex: 1; min-width: 0; }

/* THE SELECTED ROW IS FILLED, with the tick the canonical draws on it. */
.sheet2__droprow--on { background: var(--green-deep); }
.sheet2__droprow--on .sheet2__droplabel { font-weight: 700; color: var(--off-white); }
.sheet2__droptick { display: none; color: var(--gold-light); }
.sheet2__droprow--on .sheet2__droptick { display: inline-flex; }
.sheet2__droptick .tick { width: 13px; height: 13px; }

/* THE ROOM ON A SHORT HOUR, in its own colour at last. */
.sheet2__dropshort {
  flex-shrink: 0;
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  color: var(--gold-dark);
}
.sheet2__droprow--on .sheet2__dropshort { color: var(--gold-light); }

/* THE QUIET ROW that accounts for the hours the list leaves out. Not an option:
   no link, no tick, no target. */
.sheet2__droprow--note {
  min-height: 0; padding: 9px 16px;
  background: rgba(27,67,50,.04);
  font-size: 11px; line-height: 1.5; color: var(--muted);
}

/* CUSTOM TIME, FIRST AND ON THE TINT. Ruled in QC 9: first row, not last. */
.sheet2__droprow--custom { background: rgba(27,67,50,.04); }
.sheet2__droprow--custom .sheet2__droplabel { font-weight: 600; color: var(--green-btn); }
.sheet2__dropvalue { flex-shrink: 0; font-size: 13px; color: var(--ink); }
.sheet2__droppick { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; border: 0; cursor: pointer; }
/* ⚠ THE ENTRY LIVES IN THE ROW AND IS FOCUSED IN PLACE. It is laid over the
   row rather than replacing anything, so the panel does not close, nothing is
   swapped, and the wheel opens in the same gesture that picked the row. */
.sheet2__custominput {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: 0; background: none; font-size: 16px; cursor: pointer;
}
.sheet2__custominput[hidden] { display: none; }

/* The commit. Sits over the row's right end while the wheel is up, so the
   gesture that finishes the pick is on the row the pick is in. */
.sheet2__customset {
  position: relative; z-index: 2; flex-shrink: 0;
  border: 0; border-radius: 4px; padding: 7px 12px;
  background: var(--green-btn); color: var(--off-white);
  font-family: var(--sans); font-size: 11px; font-weight: 700; letter-spacing: .06em;
  cursor: pointer;
}
.sheet2__customset[hidden] { display: none; }

/* The <noscript> fallback is a real select and looks like the closed control it
   stands in for. Only ever rendered where script is off. */
.sheet2__nativefallback {
  margin-top: 6px; width: 100%; min-height: 48px;
  font-size: 16px; border-radius: 4px; border: 1px solid rgba(27,67,50,.22);
}

/* ══ MAKE RECURRING ════════════════════════════════════════════════════════
   design_handoff_make_recurring, three states, between attendance and the
   doors. A CARD, NOT A DOOR: the doors leave the sheet, this resolves in place.

   ⚠ THE TOGGLE DISCLOSES AND THE BUTTON COMMITS, which is why the toggle is a
   LINK: it carries state through the same GET the rest of this sheet uses and
   cannot write anything by construction. */
.mrec {
  margin: 14px 20px 0;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18);
  border-radius: 5px;
  padding: 13px 15px;
}
/* Open, the card takes the solid border: one live question on the sheet. */
.mrec--open { border: 1.5px solid var(--green-deep); }

.mrec__head { display: flex; align-items: center; gap: 12px; }
.mrec__headtext { flex: 1; min-width: 0; }
.mrec__title { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.mrec__sub { margin-top: 2px; font-size: 11px; line-height: 1.4; color: var(--muted); }

/* The create sheet's switch, at its own size. */
.mrec__switch {
  flex-shrink: 0; width: 52px; height: 31px; border-radius: 100px;
  background: rgba(27,67,50,.16); border: 1px solid rgba(27,67,50,.2);
  display: flex; align-items: center; justify-content: flex-start;
  padding: 0 3px; box-sizing: border-box; position: relative;
}
.mrec__switch--on { background: var(--green-btn); border-color: var(--green-btn); justify-content: flex-end; }
.mrec__knob { width: 25px; height: 25px; border-radius: 50%; background: var(--off-white); box-shadow: 0 1px 3px rgba(20,55,42,.2); }
.mrec__switchbtn { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; }

.mrec__body {
  margin-top: 13px; padding-top: 13px;
  border-top: 1px solid rgba(27,67,50,.1);
}
.mrec__label {
  display: block; margin-bottom: 6px;
  font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
}
.mrec__pair { display: flex; gap: 8px; margin-bottom: 12px; }
/* 44px is the floor, not a look. */
.mrec__seg {
  flex: 1; min-height: 44px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 10px; box-sizing: border-box;
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
  font-size: 12.5px; color: var(--ink); text-decoration: none; text-align: center;
}
.mrec__seg--on { background: var(--green-deep); border-color: var(--green-deep); color: var(--off-white); font-weight: 600; }

/* The clash list, in the words the time list uses. Gold, never red. */
.mrec__clashes { margin: 0 0 12px; }
.mrec__clash {
  margin: 0 0 6px; padding: 8px 0 8px 12px;
  border-left: 2px solid var(--gold);
  font-size: 12px; line-height: 1.55; color: var(--gold-ink);
}
.mrec__clash--skipped { opacity: .55; }
.mrec__skip { color: var(--green-btn); text-decoration: underline; text-underline-offset: 2px; }

/* The gold readback, the same rule the create sheet's recurrence note uses. */
.mrec__readback { margin: 0 0 12px; padding-left: 12px; border-left: 2px solid var(--gold); }
.mrec__readback p { margin: 0; font-size: 12px; line-height: 1.6; color: var(--gold-ink); }

.mrec__go {
  width: 100%; min-height: 48px;
  background: var(--green-btn); color: var(--off-white);
  border: 0; border-radius: 4px;
  font-family: var(--sans); font-size: 12.5px; font-weight: 700; letter-spacing: .05em;
  cursor: pointer;
}
.mrec__go:disabled { opacity: .45; cursor: default; }
.mrec__cancel {
  display: block; margin-top: 10px; text-align: center;
  font-size: 12.5px; color: var(--muted); text-decoration: none;
}

/* ── STATE C: THE ROW IS NOW THE EDITOR ───────────────────────────────────
   Gold ground, gold hairline, a chevron. It is a fact about the session that
   can be changed, so it opens D rather than toggling anything.

   ⚠ THIS REPLACED A CONFIRMATION THAT RENDERED ONCE. The previous bundle's C
   was "Now every Tuesday", drawn on the redirect after committing and never
   again, which left the recurrence unchangeable from the sheet describing it.
   Its tick and its two text rules went with it. */
.mrec--edit {
  display: flex; align-items: center; gap: 13px;
  background: var(--gold-tint);
  border: 1px solid rgba(168,132,42,.4);
  text-decoration: none;
}
.mrec__chev { flex-shrink: 0; font-size: 14px; color: var(--gold-ink); }

/* ── STATE D: THE EDITOR ──────────────────────────────────────────────────
   ⚠ THE SECOND OUTCOME IS GOLD-OUTLINED, NEVER RED. A tutor ending a
   recurrence is doing something ordinary, and red is reserved for attendance
   states everywhere in this product. */
.mrec__endform { margin-top: 7px; }
.mrec__end {
  width: 100%; min-height: 48px;
  background: var(--off-white); color: var(--gold-ink);
  border: 1px solid rgba(168,132,42,.5); border-radius: 4px;
  font-family: var(--sans); font-size: 12.5px; font-weight: 700; letter-spacing: .06em;
  cursor: pointer;
}

/* ⚠ DIMMED, NOT DISABLED, and still visible. While the recurrence block is open
   there is one live question; attendance and the doors are context. */
.att--dim, .sess__doors--dim { opacity: .35; }

/* ══ THE ATTENDANCE SLOT ═══════════════════════════════════════════════════
   design_handoff_attendance_row. One slot at the end of every session row,
   holding one fact at a time.

   ⚠ NO CIRCLE UNTIL THERE IS SOMETHING TO REPORT. An untaken row is words
   alone, so a filled circle marks only the rows that have an answer and gold
   marks only the rows that owe one. An earlier pass put a dashed ring on every
   untaken row and made a full week look like a backlog.

   ⚠ RED AND PURPLE ARE THE ONLY TWO COLOURS IN THE PORTAL OUTSIDE GREEN, GOLD
   AND CREAM, AND THEY STAY HERE. Cancelled is the only red on any calendar
   surface and no-show is the only purple; neither may leak into the rest of it.

   THE CIRCLE IS NOT A CONTROL. Nothing in this block is focusable or tappable:
   the ROW opens the sheet, and attendance is set there. */
.calrow__att {
  flex-shrink: 0; display: inline-flex; align-items: center; gap: 9px;
  font-weight: 700; letter-spacing: .06em;
  /* Week and month rows; the day card takes 12.5px below. */
  font-size: 11px;
}
.calrow--big .calrow__att { font-size: 12.5px; }

/* DUE. The only gold on the screen, and the last thing standing between a
   forgotten tap and a missing report: submitting attendance is what creates
   the survey. */
.calrow__att--due { color: var(--gold-ink); }

.calrow__attdisc {
  width: 20px; height: 20px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; line-height: 1;
}

/* FOUR GLYPHS, ONE PER STATE, so the meaning does not rest on colour.

   ⚠ THROUGH THE --att-* TOKENS, NEVER A SECOND HEX. Those are declared once,
   for the session sheet's marks, and a calendar rule with its own #5E4B94 in it
   would be a second declaration of the same idea and exactly the leak the
   no-red guard is written to catch. This slot is the same fact in a second
   place, so it reads the same token. */
.calrow__att--present { color: var(--att-present); }
.calrow__att--present .calrow__attdisc { background: var(--att-present); color: #fff; }

/* ⚠ LATE USES TWO GOLDS ON PURPOSE, AND IT IS NOT A MISTAKE TO FIX. The disc is
   --att-late (#C9A227) because a filled disc reads at a glance; the WORD is
   --gold-ink (#7C6210) because #C9A227 text on cream is 2.7:1. Same state, two
   treatments, each chosen for what it has to survive. */
.calrow__att--late { color: var(--gold-ink); }
.calrow__att--late .calrow__attdisc { background: var(--att-late); color: var(--green-deep); }

.calrow__att--noshow { color: var(--att-noshow); }
.calrow__att--noshow .calrow__attdisc { background: var(--att-noshow); color: #fff; }

.calrow__att--cancelled { color: var(--att-cancelled); }
.calrow__att--cancelled .calrow__attdisc { background: var(--att-cancelled); color: #fff; }

/* ══ THE PER-TUTOR SESSION ACCENT ══════════════════════════════════════════
   An admin reading the master schedule saw nine white cards with the tutor
   named in small grey text. Each tutor gets a pastel: a coloured left rule and
   a soft wash on the card ground.

   ⚠ ADMIN SURFACES ONLY. The two custom properties are set inline by
   partials/calendar-row.ejs and are absent for a tutor, so a tutor's own
   calendar renders exactly what it always did -- these rules cannot fire
   without them.

   THE COLOURS COME FROM A FORMULA, NOT FROM HERE. services/tutorColors walks
   the wheel by the golden angle, skipping the bands reserved for the red,
   gold, brand-green and purple state treatments. This file only says how the
   two values are worn.

   ⚠ AND IT LOSES TO EVERY STATE. The wash is 90% lightness, so --ink sits on
   it at better than 12:1 and the gold day-of pill, the HELD dashes, the group
   fill and the NEW tag all read exactly as they do on cream. On those rows the
   wash is not drawn at all -- see the partial -- and only the rule survives,
   which is the ruling: the tutor is still named and the state still owns the
   card. */
/* ⚠ THE WASH IS THE TIME BLOCK ONLY, NOT THE CARD. Ruled after the first
   version washed the whole row: at real density nine full-width pastels is a
   page of colour with a schedule somewhere in it.

   It runs from the card's left edge -- the rule sits on that edge -- through
   the time and stops between the time and the name. Drawn with a gradient on
   the CARD rather than a background on .calrow__time, so it can start at the
   card's own edge under the 15px padding and end at a point that is not any
   element's boundary. The rest of the card is the ordinary ground it always
   was, so the name, the tutor and the chevron read exactly as they did.

   THE STOP IS 15px OF PADDING + 44px OF TIME + HALF THE 10px GAP = 64px. One
   expression, derived rather than eyeballed, so a change to either number
   moves it. */
.calrow--washed {
  background:
    linear-gradient(to right, var(--accent-wash) 0 var(--accent-stop),
                    transparent var(--accent-stop)),
    var(--off-white);
}

/* ⚠ THE STOP IS THE MIDPOINT OF THE GAP BETWEEN THE TIME TEXT AND THE NAME,
   AND IT IS MEASURED RATHER THAN DERIVED. Ruled.

   The first version stopped at padding + time COLUMN + half the flex gap. The
   column is a fixed width so that names line up down the card, and the time
   text does not fill it: "9:00" leaves a lot of it empty and "11:30" leaves
   almost none. So that stop sat at the column's edge, which is a different
   distance from the end of the text on every row -- the tint appeared to end
   in a different place depending on the hour.

   THE TRUE MIDPOINT NEEDS THE RENDERED TEXT WIDTH, which CSS cannot ask for,
   so public/calendar-controls.js measures it and writes --accent-stop per row.
   Making the column shrink to its text would have let CSS do it, and would
   have cost the alignment the fixed column exists for.

   THE FALLBACK IS THE OLD ARITHMETIC, so a row renders sensibly before the
   measurement lands and on any engine that never runs it: 15px of padding, the
   44px column, half the 10px gap. The big day card is 15 + 50 + half of 11. */
.calrow--washed { --accent-stop: 64px; }
.calrow--washed.calrow--big { --accent-stop: 70.5px; }
.calrow--accent { border-left: 3px solid var(--accent-rule); }
/* The big day card already has its own radius; the rule follows it so the
   corner is not squared off by a border. */
.calrow--accent.calrow--big { border-radius: 6px; }

/* THE LEGEND, ON ADMIN CALENDAR VIEWS ONLY. Same row pattern the calendar's
   other legend used: one line, caption weight, no box. */
.callegend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  padding: 10px 18px 0;
}
.callegend__item { display: inline-flex; align-items: center; gap: 6px; }
.callegend__sw {
  width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0;
  background: var(--sw-wash); border-left: 3px solid var(--sw-rule); box-sizing: border-box;
}
.callegend__name { font-size: 11px; color: var(--muted); }

/* ── THE STATED-HOURS TINT ────────────────────────────────────────────────
   Admin only: an admin picking another tutor's slot, with that tutor's hours
   behind the grid. A reference layer and nothing more -- an hour outside it is
   still bookable and still warned about in the gold words the sheet already
   uses, and the conflict rule never reads it.

   ⚠ FAINT, AND GREEN RATHER THAN GOLD. Gold means unusual-but-fine everywhere
   in this product -- the recurrence readback, the outside-hours warning, the
   two cancel lanes -- and stated hours are the ordinary case, not an unusual
   one. A gold wash behind every available hour would spend the one colour this
   product reserves for "notice this" on the thing least worth noticing.

   4% of the brand green: enough to read as a band at a glance, far too little
   to compete with a session block at 100% or the NEW pill's solid #14372A. It
   sits UNDER everything in the slot, so every existing state -- taken, held,
   moved, new, and the tap target -- draws over it unchanged. */
.sheet2__avail {
  position: absolute; left: 0; right: 0;
  background: rgba(27,67,50,.055);
  border-left: 2px solid rgba(27,67,50,.16);
  pointer-events: none;
  z-index: 0;
}
/* Everything else in the slot is above it. The blocks already paint their own
   ground; this only has to lose to them. */
.sheet2__slot .sheet2__block { z-index: 1; }

/* ── THE STUDENT LIST, DRAWN ──────────────────────────────────────────────
   design_handoff_student_picker, "STUDENT, OPEN". Its own numbers: 330 wide,
   56px rows, a 30px serif avatar, the name at 14.5 over the subject at 11, and
   the selected row filled #14372A.

   ⚠ ONE ALPHABETICAL RUN, NO HEADERS. The gold band that used to sit over the
   students with nothing booked is gone; the fact moved onto the rows that own
   it, as a tag, and the count moved to the footer. See the sheet template.

   SEVEN ROWS AND THE FOOTER, which is exactly what the canonical draws: seven
   at 56 is 392, the footer is 48, and 440 is still inside a thumb's reach.
   Longer lists scroll inside the panel with the footer pinned to its floor. */
.sheet2__field--drop { position: relative; }
/* ⚠ NO `left`, AND THAT IS THE FIX RATHER THAN AN OMISSION.
   This rule carried `left: 0` from 67e926d, which drew the list. `left` on an
   absolutely positioned box resolves against its containing block's PADDING
   box, and the containing block here is .sheet2__field--drop, which carries
   `padding: 8px 20px 0` from the sheet rebuild well before that. So `left: 0`
   put the panel on the field's padding edge: MEASURED at 390 and at 1280, the
   panel opened at l=0 while the "Select a student" box it hangs from sits at
   l=20. Twenty pixels out, into the page gutter, at both widths.

   Corrected to the field's OWN inset rather than to a copy of the number:
   .sheet2__field publishes it as --sheet2-inset and this reads it, so the two
   cannot drift. Leaving `left` unset was tried first and is a pixel out: the
   static position an absolutely positioned box falls back to measured 21 here
   against the box's 20, so it was traded for a value that is exact by
   construction rather than nearly right by accident.

   ⚠ THE OTHER TWO PANELS KEEP THEIR OFFSETS AND ARE NOT THE SAME CASE.
   [data-droppanel="time"] is `left: 0` against a containing block with no
   padding on that side and measures flush already; minutes is deliberately
   `right: 0` so a 190px panel from the middle control does not run off the
   screen. Both verified unmoved after this change. */
[data-droppanel="student"] { left: var(--sheet2-inset); right: auto; min-width: 330px; max-height: 440px; }
.sheet2__srow {
  min-height: 56px; box-sizing: border-box;
  display: flex; align-items: center; gap: 11px; padding: 0 14px;
  border-bottom: 1px solid rgba(27,67,50,.07);
  text-decoration: none; position: relative;
}
.sheet2__srow:last-child { border-bottom: 0; }
.sheet2__sav {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  background: var(--green-deep); color: var(--gold-light);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 11px;
}
.sheet2__swho { flex: 1; min-width: 0; }
.sheet2__sname { display: block; font-size: 14.5px; font-weight: 600; color: var(--ink); }
.sheet2__ssub { display: block; margin-top: 1px; font-size: 11px; color: var(--muted); }

/* ⚠ THE TAG, AND IT IS THE ONLY GOLD IN THE LIST. #7C6210 is the same gold this
   sheet uses everywhere for a state worth noticing, and at 5.78:1 on cream it
   is text rather than decoration. Uppercase and right-aligned on the name's own
   line, so a row that has nothing to say ends empty. */
.sheet2__stag {
  flex-shrink: 0;
  font-size: 9.5px; font-weight: 700; letter-spacing: .08em;
  color: var(--gold-ink);
}

/* THE CHOSEN ROW, filled, with the avatar inverted onto the gold. */
.sheet2__srow--on { background: var(--green-deep); }
.sheet2__srow--on .sheet2__sname { font-weight: 700; color: var(--off-white); }
.sheet2__srow--on .sheet2__ssub { color: rgba(255,255,255,.68); }
.sheet2__srow--on .sheet2__sav { background: var(--gold-light); color: var(--green-deep); }
/* ⚠ A SELECTED FREE STUDENT KEEPS THE TAG, LIFTED TO #E3B53A -- it is not
   swapped for a tick. Both cannot sit at the end of one row, and the fill
   already says which row is chosen, so the tag is the one carrying information
   the fill does not. #7C6210 would not hold on the dark ground. */
.sheet2__srow--on .sheet2__stag { color: var(--gold-light); }

/* THE FOOTER, PINNED TO THE PANEL'S FLOOR so its count is readable at any
   scroll position rather than only after the last row. Opaque: the mock's
   rgba(27,67,50,.03) is laid over the panel's own paper as a gradient, because
   a translucent sticky bar would have the rows sliding through it. */
.sheet2__sfoot {
  position: sticky; bottom: 0; z-index: 1;
  min-height: 48px; box-sizing: border-box;
  display: flex; align-items: center; padding: 0 14px;
  background: linear-gradient(rgba(27,67,50,.03), rgba(27,67,50,.03)), var(--off-white);
  border-top: 1px solid rgba(27,67,50,.07);
  font-size: 12.5px; color: var(--muted);
}

/* ── THE SEARCH LINE, PINNED TO THE PANEL'S CEILING ───────────────────────
   design_handoff_student_search. The footer's mirror and deliberately so: the
   same 48px, the same 14px sides, and THE SAME OPAQUE GROUND -- the mock's
   rgba(27,67,50,.03) laid over the panel's own paper as a gradient, because a
   translucent sticky bar would have the rows sliding through it. Between the
   two the list scrolls, and the pair read as the panel's frame.

   ⚠ 440 IS DRAWN, NOT ARITHMETIC, AND THE CAP DOES NOT MOVE TO MAKE ROOM. 48
   for this field, six whole rows at 56, the 48px footer, and a seventh row cut
   by the floor. THE CUT ROW IS THE POINT: in a closed-height list it is the
   only thing that says there is more below it. Raising max-height to fit seven
   again would buy one row and spend the only signal the panel has.

   NO MEDIA QUERY AND NO WIDTH OF ITS OWN. [data-droppanel="student"] carries
   none either, so 330 wide and 440 tall are already this panel's desktop
   numbers and the field is the same object on both surfaces. */
.sheet2__ssearch {
  position: sticky; top: 0; z-index: 1;
  min-height: 48px; box-sizing: border-box;
  display: flex; align-items: center; gap: 10px; padding: 0 14px;
  background: linear-gradient(rgba(27,67,50,.03), rgba(27,67,50,.03)), var(--off-white);
  border-bottom: 1px solid rgba(27,67,50,.07);
}
.sheet2__ssearchico { flex-shrink: 0; color: var(--faint); }

/* ⚠ 16px, AND IT IS NOT A TYPOGRAPHIC CHOICE. Below 16 iOS zooms the whole
   sheet on focus, and a panel pinned to the trigger it hangs from then walks
   off the screen under the thumb that opened it. The same floor the stylesheet
   already puts on the invisible native pickers. */
.sheet2__ssearchin {
  flex: 1; min-width: 0; width: auto;
  margin: 0; padding: 0; border: 0; background: transparent;
  font-family: inherit; font-size: 16px; color: var(--ink);
}
.sheet2__ssearchin::placeholder { color: var(--faint); opacity: 1; }

/* FOCUS IS DRAWN ON THE BAR, NOT ON THE INPUT. The input has no box of its own
   -- no border, no fill, it is a bare line of text between the magnifier and
   the clear -- so a ring around it would float in the middle of the row. The
   bar's own hairline thickens to gold instead and the magnifier goes with it,
   which is also the only thing that changes: the 1px hairline becomes 1.5px on
   an element whose height is a min-height, so nothing below it moves. */
/* ⚠ AND box-shadow: none, WHICH IS NOT BELT AND BRACES. The global form rule
   at the top of this file gives `input:focus` a 3px gold glow, and (0,1,1)
   loses to this (0,2,0) only where this block actually names the property.
   Unnamed, that ring paints around a borderless transparent input sitting in
   the middle of the sticky bar -- a second focus state, drawn nowhere in the
   mock, on top of the gold hairline that IS the focus state. The same rule's
   `width: 100%` is why the input above names `width: auto`. */
.sheet2__ssearchin:focus { outline: none; box-shadow: none; }
.sheet2__ssearch:focus-within { border-bottom: 1.5px solid var(--gold); }
.sheet2__ssearch:focus-within .sheet2__ssearchico { color: var(--gold-ink); }

/* THE CLEAR, WHICH IS THERE ONLY WHEN THERE IS SOMETHING TO CLEAR. */
.sheet2__ssearchx {
  flex-shrink: 0; width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 50%; background: rgba(27,67,50,.1);
  display: flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: 13px; line-height: 1; color: var(--muted);
  cursor: pointer;
}
/* ⚠ SPELLED OUT BECAUSE THE RULE ABOVE SETS `display`. An author-origin display
   beats the user agent's [hidden] { display: none } outright, which is exactly
   how the deactivated roster rows sat in plain sight on production carrying an
   attribute that hid nothing. (0,2,0) against this block's own (0,1,0). */
.sheet2__ssearchx[hidden] { display: none; }

/* ⚠ THE FILTERED-OUT ROW HIDES BY CLASS, FOR THE SAME REASON. .sheet2__srow is
   `display: flex` from author origin, so setting `hidden` on it would hide
   nothing at all and the filter would silently do nothing while every count
   beside it said it had worked. (0,2,0) against .sheet2__srow's (0,1,0), and
   declared after it. */
.sheet2__srow--hide { display: none; }

/* THE HIGHLIGHT. It earns its place on the SUBJECT line specifically: typing
   `trig` and getting Molly Brigham is inexplicable without it. A wash rather
   than a colour, so the name underneath is still the name. */
.sheet2__smark { background: rgba(201,162,39,.34); border-radius: 2px; }

/* NOTHING MATCHED. Replaces the rows rather than sitting under them. The
   second line names the scope, because a student assigned to somebody else is
   the likely reason and without the line the search reads as broken. */
.sheet2__snone {
  padding: 16px 14px;
  border-bottom: 1px solid rgba(27,67,50,.07);
}
.sheet2__snone[hidden] { display: none; }
.sheet2__snonep { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.sheet2__snonep + .sheet2__snonep { margin-top: 7px; }
.sheet2__snoneq { color: var(--ink); }
/* A button because it acts rather than navigates, drawn as the link it reads
   as: there is no href that would clear a filter the server never heard of. */
.sheet2__snoneall {
  display: inline-block; margin-top: 10px; padding: 0;
  border: 0; border-bottom: 1px solid rgba(201,162,39,.6);
  background: transparent; cursor: pointer;
  font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--green-btn);
}

/* THE FOOTER'S TWO SENTENCES, ONE AT A TIME. Spans inside a flex row, so the
   attribute alone would hide them; spelled out anyway, because the next hand to
   give this block a `display` should not have to rediscover why. */
.sheet2__sfoot [hidden] { display: none; }

/* A roster with nobody on it is a real answer and says so. */
.sheet2__srow--note {
  min-height: 0; padding: 14px; font-size: 12.5px; color: var(--muted);
}

/* ⚠ AN EMPTY HOUR ROW IS A TAP TARGET. Ruled in QC 8, superseding the
   canonical's read-only day schedule; SPEC carries the supersession.

   Absolutely positioned over the whole slot, so the target is the row and
   clears 44px on its height alone -- the grid's rows are 44px. Invisible: the
   row already reads as empty and a second affordance drawn on every free hour
   would be eleven of them. Only rows with no session in them get one at all. */
.sheet2__slotpick {
  position: absolute; inset: 0;
  z-index: 1; cursor: pointer;
  -webkit-tap-highlight-color: rgba(27,67,50,.08);
}
.sheet2__slotpick:focus-visible {
  outline: 2px solid var(--gold); outline-offset: -2px;
}

/* ── CUSTOM TIME ──────────────────────────────────────────────────────────
   The row itself is the last <option> of the time control, so it needs no rule:
   it is the last row of the list because it is the last row of the list.

   PICKED, the control carries the faint tint the canonical marks it with, which
   is the same tint the lesson-type row and the taken note already use. It says
   "not one of the offered hours" without a caption. */
.sheet2__control--custom { background: rgba(27,67,50,.04); }

/* The way back, at caption weight, because leaving is not a decision the sheet
   should weight as heavily as making one. */
.sheet2__uselist {
  display: inline-block; margin-top: 5px;
  font-size: 10.5px; color: var(--muted); text-decoration: underline;
  text-underline-offset: 2px;
}
.sheet2__uselist:hover { color: var(--gold-ink); }

/* ⚠ THE ONE ERROR IN THE SHEET, AND IT IS GOLD. A collision on a free-typed
   minute is an ordinary fact about a busy afternoon, not a mistake, and nothing
   in this product uses red. Same gold rule the recurrence readback uses, so the
   two unusual-but-fine states read as one family. */
.sheet2__refusal {
  margin: 10px 20px 0; padding: 8px 0 8px 12px;
  border-left: 2px solid var(--gold);
  font-size: 12px; line-height: 1.55; color: var(--gold-ink);
}
/* Outside stated hours WARNS and never disables the button. */
.sheet2__warn {
  margin: 12px 20px 0; padding: 11px 13px; border-radius: 5px;
  background: var(--gold-tint); border: 1px solid rgba(201,162,39,.45);
  font-size: 12px; line-height: 1.6; color: var(--gold-ink);
}
.sheet2__apply { margin: 12px 20px 0; min-height: 44px; }

/* ── THE BUTTON, WHICH NAMES THE WHOLE BOOKING ───────────────────────────── */
.sheet2__foot { padding: 18px 20px 26px; }
.sheet2__book {
  width: 100%; min-height: 52px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  padding: 0 14px; border: 0; background: var(--green-btn); border-radius: 4px;
  font-family: var(--sans); font-size: 13px; font-weight: 700; letter-spacing: .05em;
  line-height: 1.3; color: #FDFCF9; cursor: pointer; box-sizing: border-box;
}
.sheet2__book:disabled { background: var(--disabled); cursor: default; }

/* The scope question a move asks instead of a cadence one. */
.sheet2__scope { margin: 16px 20px 0; padding: 0; border: 0; }
.sheet2__scoperow {
  display: flex; align-items: center; gap: 10px; min-height: 52px;
  padding: 0 14px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px; margin-top: 6px;
  font-size: 13.5px; color: var(--ink);
}
/* ⚠ THE RADIO IS A RADIO, NOT A FIELD, AND THE BASE RULE DOES NOT KNOW THAT.
   `input, textarea, select` near the top of this file gives every input
   `width: 100%` and a text field's padding, which is right for the twenty forms
   it was written for and wrong for a bare radio in a flex row: the control took
   the whole row as its basis and squeezed the label into a two or three line
   wrap running off the edge. Reported from a phone on the move sheet.

   Every other radio in the product already escapes it -- .cancel__radio is
   hidden behind its own dot, .qform__choice sets flex: 0 0 auto -- and this one
   was the last bare one left. */
.sheet2__scoperow input[type="radio"] {
  flex: 0 0 auto;
  width: 18px; height: 18px; margin: 0; padding: 0;
}
/* And the label takes what is left, rather than what the radio did not want. */
.sheet2__scoperow > span { flex: 1; min-width: 0; }

/* ── THE SESSION SHEET ────────────────────────────────────────────────────
   Transcribed from Calendar Flow.dc.html section 9. Same bottom-sheet frame as
   the create sheet: 390 wide, rounded at the top, shadow thrown upward.

   THE DOORS ARE RANKED BY FREQUENCY rather than listed as equals. Move is the
   filled one because a family shifting an hour happens weekly. Cancel is last
   and looks like the others: no red, no outline, no confirmation here, because
   the consequence lives on the next screen where a day-of and an advance cancel
   can be told apart.

   ABSENT BY PHASE: the ATTENDANCE block and its SUBMIT button. Attendance is a
   later build, so the sheet goes straight from the session to its doors rather
   than reserving space for something that cannot work yet. */
.cal--session, .cal--cancel {
  max-width: none; padding: 0 0 26px;
  background: var(--cream);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -14px 34px rgba(20,55,42,.16);
}

.sess__top {
  padding: 16px 20px 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}
.sess__eyebrow { font-size: 10px; font-weight: 600; letter-spacing: .12em; color: var(--gold-ink); }
/* A BORDERED BUTTON, not a bare link: it is the way out of a sheet and the mock
   gives it an edge so it reads as pressable at a glance. */
.sess__back {
  flex-shrink: 0; min-height: 40px; display: inline-flex; align-items: center; gap: 5px;
  padding: 0 13px; border: 1px solid rgba(27,67,50,.3); border-radius: 4px;
  background: var(--off-white); font-size: 12.5px; font-weight: 600;
  color: var(--green-btn); white-space: nowrap; box-sizing: border-box;
}

.sess__head { padding: 2px 20px 0; }
.sess__time { font-family: var(--serif); font-size: 26px; line-height: 1.2; color: var(--ink); }
.sess__whorow { margin-top: 11px; display: flex; align-items: center; gap: 11px; }
.sess__avatar {
  flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%;
  background: var(--green-deep); display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 11.5px; color: var(--gold-light);
}
.sess__name { font-size: 15px; font-weight: 600; color: var(--ink); }
/* THE NAME AS A DOOR TO THE STUDENT'S RECORD, for an admin. It has to stay the
   heading it already was: same size, same weight, same ink. A link's default
   blue and underline here would read as a different kind of thing and pull the
   eye off the time above it.
   ⚠ block PLUS fit-content, AND BOTH HALVES MATTER. It replaced a div, so it
   has to stay a block or the meta line under it shifts with the line box. But a
   plain block fills its parent, and the dotted rule below would then run the
   full width of the card rather than stopping at the end of the name. fit-content
   hugs the text, which is what makes the rule read as belonging to the name.
   The affordance is that dotted rule: it says "there is more here" without
   competing with the gold the rest of this sheet uses for attention. */
.sess__name--link {
  display: block; width: fit-content;
  text-decoration: none; color: var(--ink);
  border-bottom: 1px dotted rgba(27,67,50,.42);
}
.sess__name--link:hover, .sess__name--link:focus-visible {
  border-bottom-color: var(--green-deep); color: var(--green-deep);
}
.sess__meta { margin-top: 1px; font-size: 11.5px; color: var(--muted); }

/* ── WHERE THIS SESSION CAME FROM ──────────────────────────────────────────
   design_handoff_booking_grid part two. Above attendance and below the student,
   which is where the frame draws it: the provenance explains the row and
   attendance is the thing done to it.

   ⚠ NO PILL INSIDE IT. A gold pill on this gold tint measured 4.22:1. The block
   is already gold and its eyebrow carries the same words, so the eyebrow is
   plain, and it takes the same treatment .sess__eyebrow already has.

   20px to match .sess__head and .sess__top, which is the sheet's gutter. */
.sessorigin {
  margin: 12px 20px 0; padding: 10px 12px; border-radius: 6px;
  border: 1px solid rgba(168, 132, 42, .35); background: rgba(201, 162, 39, .12);
  display: flex; flex-direction: column; gap: 4px;
}
.sessorigin__eyebrow {
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink);
}
.sessorigin__line { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--gold-ink); }
/* The same dashed square the grid draws a block with, so the sheet and the
   calendar agree on what a block is. Frame 1a, and the tokens are the grid's. */
.sess__blockchip {
  flex-shrink: 0; width: 36px; height: 36px; border-radius: 4px; box-sizing: border-box;
  border: 1.5px dashed rgba(168,132,42,.7); background: rgba(201,162,39,.12);
}
/* The block's note, under the meta. It used to be the headline; the headline
   now says what the row is, so this says what it is for. */
.sess__holdnote { margin-top: 3px; font-size: 11.5px; color: var(--gold-ink); }

/* ── BLOCKED TIME, GIVEN TO A STUDENT ──────────────────────────────────────
   design_handoff_block_to_session frame 1a. The picker opens IN PLACE of the
   doors, so this block sits exactly where .sess__doors would have.
   NO RED ANYWHERE: gold carries the one warning, as everywhere else here. */
.sess__give { padding: 18px 20px 0; display: flex; flex-direction: column; gap: 9px; }
.sess__giverow { display: flex; align-items: baseline; gap: 12px; }
.sess__giveeyebrow {
  flex: 1; min-width: 0;
  font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink);
}
/* 44px, like every other target on this sheet. */
.sess__giveclose {
  flex-shrink: 0; min-height: 44px; display: inline-flex; align-items: center;
  font-size: 12.5px; font-weight: 600; color: var(--muted); text-decoration: none;
}

/* ⚠ 16px ON THE INPUT, which is not a style choice: iOS zooms the whole sheet
   on focus at anything smaller, and this sheet is fixed-position furniture. */
.sess__giveinput {
  width: 100%; min-height: 48px; padding: 0 13px; box-sizing: border-box;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
  font-size: 16px; font-family: inherit; color: var(--ink);
}

/* The list caps and scrolls INSIDE the sheet, so a long roster cannot push the
   button off the bottom. overscroll-behavior stops the page scrolling with it. */
.sess__givelist {
  max-height: 216px; overflow-y: auto; overscroll-behavior: contain;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.18); border-radius: 4px;
}
.sess__giveopt {
  min-height: 52px; display: flex; align-items: center; gap: 11px; padding: 0 14px;
  border-bottom: 1px solid rgba(27,67,50,.07); text-decoration: none;
}
.sess__giveopt:last-child { border-bottom: 0; }
.sess__givemono {
  flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: #F1EAD6;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 10.5px; color: var(--gold-ink);
}
.sess__givewho { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.sess__givename { font-size: 14px; font-weight: 600; color: var(--ink); }
.sess__givemeta { margin-top: 1px; font-size: 11.5px; color: var(--muted); }
/* A LABEL, NOT A CONTROL. The row is still pickable wherever it is tapped. */
.sess__givebusy {
  flex-shrink: 0; font-size: 9px; font-weight: 700; letter-spacing: .08em; color: var(--gold-ink);
}
.sess__givenone { padding: 14px; }
.sess__givenonetitle { font-size: 13px; font-weight: 600; color: var(--ink); }
.sess__givenonesub { margin-top: 3px; font-size: 11.5px; line-height: 1.5; color: var(--muted); }

.sess__picked {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  background: var(--off-white); border: 1.5px solid var(--green-deep); border-radius: 4px;
}
.sess__pickedmono {
  flex-shrink: 0; width: 34px; height: 34px; border-radius: 50%; background: var(--green-deep);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 12px; color: var(--gold-light);
}
.sess__pickedwho { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.sess__pickedname { font-size: 15px; font-weight: 700; color: var(--ink); }
.sess__pickedmeta { margin-top: 2px; font-size: 12px; color: var(--muted); }
.sess__pickedx {
  flex-shrink: 0; width: 44px; height: 44px; display: flex; align-items: center;
  justify-content: center; border-radius: 50%; font-size: 15px;
  color: var(--muted); text-decoration: none;
}
.sess__giveclock { margin-top: 1px; }
.sess__clockpill {
  display: inline-flex; align-items: center; padding: 5px 12px; border-radius: 100px;
  background: rgba(201,162,39,.18); border: 1px solid rgba(168,132,42,.4);
  font-size: 12.5px; font-weight: 700; color: var(--gold-ink);
}

.sess__lenhead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.sess__leneyebrow { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
.sess__lenword { font-family: var(--serif); font-size: 15px; color: var(--green-deep); }
/* ⚠ auto-fit, NOT a fixed four. The frame draws three options because it draws
   a sixty minute block; a ninety minute one has five, and a fixed column count
   would either strand a gap or crush them. Minimum 56px keeps each at a real
   target width before it wraps. */
.sess__lengrid {
  margin-top: 6px; display: grid; grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
  gap: 2px; background: rgba(27,67,50,.05); border-radius: 5px; padding: 3px;
}
.sess__lenopt {
  min-height: 44px; display: flex; align-items: center; justify-content: center;
  border-radius: 4px; font-size: 14px; color: var(--ink); text-decoration: none;
}
.sess__lenopt--on { background: var(--green-deep); font-weight: 700; color: var(--off-white); }
.sess__lenrem { margin: 8px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--gold-ink); }

.sess__givego {
  width: 100%; min-height: 52px; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 0 16px; box-sizing: border-box; border: 0;
  background: var(--green-btn); border-radius: 4px;
  font-family: inherit; font-size: 13px; font-weight: 700; letter-spacing: .05em;
  line-height: 1.3; color: var(--off-white); cursor: pointer;
}
.sess__givewarn {
  margin: 9px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--gold-ink); text-align: center;
}

/* ── THE DOORS ───────────────────────────────────────────────────────────── */
.sess__doors { padding: 18px 20px 0; display: flex; flex-direction: column; gap: 7px; }
.sess__door {
  min-height: 56px; display: flex; align-items: center; gap: 12px;
  padding: 0 15px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 5px; box-sizing: border-box;
}
.sess__doorbody { flex: 1; min-width: 0; }
.sess__doorlabel { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.sess__doorsub { margin-top: 1px; font-size: 11px; line-height: 1.4; color: var(--muted); }
.sess__doorchev { flex-shrink: 0; font-size: 14px; color: var(--muted); }

/* Move is the filled one: a family shifting an hour happens weekly. */
.sess__door--filled { background: var(--green-deep); border-color: var(--green-deep); }
.sess__door--filled .sess__doorlabel { color: #FDFCF9; }
.sess__door--filled .sess__doorsub { color: rgba(255,255,255,.68); }
.sess__door--filled .sess__doorchev { color: rgba(255,255,255,.6); }

/* The admin-only door carries a gold edge and a pill. ABSENT for a tutor
   entirely; the pill exists because the mock draws both roles on one sheet and
   the live page renders only the row the viewer may press. */
.sess__door--admin { border-color: rgba(168,132,42,.45); }
.sess__doortitle { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.sess__adminpill {
  font-size: 9px; font-weight: 700; letter-spacing: .08em;
  padding: 2px 7px; border-radius: 100px;
  background: rgba(201,162,39,.22); color: var(--gold-ink);
}

/* Stated once, under the doors, rather than three times inside them. */
.sess__series { padding: 18px 20px 0; }
.sess__seriesbox { padding: 11px 13px; background: rgba(27,67,50,.03); border-radius: 4px; }
.sess__seriesbox p { margin: 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* The one thing here that leaves the calendar, so it is a link at the foot in
   regular weight rather than a fourth door. */
.sess__foot { padding: 16px 20px 26px; }
.sess__history {
  min-height: 44px; display: inline-flex; align-items: center;
  font-size: 13px; color: var(--green-btn);
}
.sess__history span { border-bottom: 1px solid rgba(27,67,50,.4); padding-bottom: 1px; }

/* The state line a tutor sees on a cancelled session, and the admin block that
   carries the waive. Gold, never red. */
.sess__state { margin: 14px 20px 0; font-size: 12.5px; color: var(--gold-ink); }
.sess__audit { margin: 18px 20px 0; padding: 14px; border: 1px solid var(--line); border-radius: 5px; background: rgba(27,67,50,.03); }
.sess__auditline { margin: 0; font-size: 12.5px; line-height: 1.65; color: var(--body); }
.sess__waive {
  margin-top: 12px; width: 100%; min-height: 48px; border: 1px solid var(--gold);
  border-radius: 4px; background: var(--gold-tint); color: var(--gold-ink); cursor: pointer;
  font-family: var(--sans); font-size: 12.5px; font-weight: 700; letter-spacing: .06em;
}
.sess__waivenote { margin: 9px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* ── FINDING A SUB ────────────────────────────────────────────────────────
   Transcribed from Calendar Flow.dc.html section 7. Admin only. */
.sub__when { display: flex; align-items: baseline; gap: 11px; flex-wrap: wrap; margin-top: 8px; }
.sub__time { font-family: var(--serif); font-size: 24px; color: var(--ink); }
.sub__len { font-size: 13px; color: var(--muted); }

/* The gold block accounts for a short list and states the scope before
   anything can be picked. Gold, because this is unusual rather than wrong. */
.sub__notewrap { padding: 18px 20px 0; }
.sub__note { padding: 14px 16px; background: var(--gold-tint); border-left: 2px solid var(--gold); }
.sub__notelabel { font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink); }
.sub__note p { margin: 7px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--gold-ink); }
.sub__movehint { margin: 7px 20px 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); }
.sub__movehint a { color: var(--green-btn); font-weight: 600; }

/* ── DATE, TIME AND HOW LONG, ABOVE THE LIST ─────────────────────────────────
   The canonical's three widths, 150 / 130 / 120, on one line. They WRAP rather
   than shrink below 390px, because a date reading "Tue, 9/1" with the year
   clipped off is worse than a second line.

   The select is transparent and laid over the box, which is how every other
   picker in this build works: the visible value is styled text the mock
   specifies, and the real control sits on top of it so the whole box is the
   tap target. */
.sub__when2 {
  padding: 16px 20px 0;
  display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap;
}
.sub__ctl { display: block; }
.sub__ctl:nth-of-type(1) { width: 150px; }
.sub__ctl:nth-of-type(2) { width: 130px; }
.sub__ctl:nth-of-type(3) { width: 120px; }
.sub__ctllabel {
  display: block; font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
}
.sub__control {
  position: relative;
  margin-top: 6px; min-height: 44px;
  display: flex; align-items: center; justify-content: space-between; gap: 4px;
  padding: 0 12px; box-sizing: border-box;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
}
.sub__value {
  min-width: 0; font-size: 13px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sub__caret { flex-shrink: 0; font-size: 9px; color: var(--muted); }
.sub__select {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: 0; background: none; cursor: pointer;
  /* 16px, so iOS does not zoom the viewport when it takes focus. Invisible, but
     the size still governs that behaviour. */
  font-family: var(--sans); font-size: 16px;
}
.sub__apply {
  min-height: 44px; padding: 0 14px; border: 1px solid rgba(27,67,50,.25);
  border-radius: 4px; background: var(--off-white);
  font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: var(--green-btn);
  cursor: pointer;
}
/* Only rendered when the proposal differs from what is booked, so confirming
   cannot silently do two things when the button names one. */
.sub__moved {
  margin: 8px 20px 0; padding: 10px 12px;
  background: var(--gold-tint); border-left: 2px solid var(--gold); border-radius: 0 4px 4px 0;
  font-size: 11.5px; line-height: 1.6; color: var(--gold-ink);
}

.sub__listwrap { padding: 16px 20px 0; }
.sub__listlabel { font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted); }
.sub__empty { margin: 10px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--muted); }
.sub__list { margin-top: 8px; display: flex; flex-direction: column; gap: 7px; }

.sub__row {
  min-height: 56px; display: flex; align-items: center; gap: 11px;
  padding: 0 14px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 4px; box-sizing: border-box;
  cursor: pointer;
}
/* The chosen one fills dark, which is the mock's only selected state here. */
.sub__row--on, .sub__row:has(.sub__radio:checked) {
  background: var(--green-deep); border-color: var(--green-deep);
}
/* The radio is the control; the row is its label. Hidden rather than styled,
   because the row already carries the whole selected state. */
.sub__radio { position: absolute; opacity: 0; width: 0; height: 0; }
.sub__avatar {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  background: var(--gold-light); display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 11px; color: var(--green-deep);
}
.sub__who { flex: 1; min-width: 0; }
.sub__name { display: block; font-size: 14.5px; font-weight: 700; color: var(--ink); }
.sub__hours { display: block; margin-top: 2px; font-size: 11px; line-height: 1.4; color: var(--muted); }
.sub__row--on .sub__name, .sub__row:has(.sub__radio:checked) .sub__name { color: #FDFCF9; }
.sub__row--on .sub__hours, .sub__row:has(.sub__radio:checked) .sub__hours { color: rgba(255,255,255,.72); }
.sub__hours b { color: var(--gold-ink); }
.sub__row--on .sub__hours b, .sub__row:has(.sub__radio:checked) .sub__hours b { color: var(--gold-light); }
.sub__tick { flex-shrink: 0; font-size: 13px; color: transparent; }
.sub__row--on .sub__tick, .sub__row:has(.sub__radio:checked) .sub__tick { color: var(--gold-light); }

.sub__consequence { margin: 9px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); text-align: center; }

/* ── CANCELLING ───────────────────────────────────────────────────────────
   Transcribed from Calendar Flow.dc.html sections 10 and 11.

   TWO LANES, ONE SCREEN, and which one you are in is stated before anything
   else. The lane is decided by the SESSION'S OWN calendar day on the business
   clock, in services/scheduling, never here.

   ⚠ THE SERIES QUESTION COMES BEFORE THE CONSEQUENCE, because the answer
   changes what the consequence is. Two 52px rows worded as outcomes rather than
   as settings.

   GOLD, NEVER RED. A cancellation is unusual, not an error. The day-of lane
   gets a 2px gold edge because its consequence is the one that costs money;
   the advance lane's is a quiet tinted box because nothing is at stake. */
.cal--cancel {
  max-width: none; padding: 0 0 26px;
  /* WHITE, not cream: the mock lifts this sheet a shade brighter than the create
     sheet, which is the only visual difference between a screen that books and
     one that undoes. */
  background: var(--off-white);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -14px 34px rgba(20,55,42,.18);
}
.cancel__head { padding: 16px 18px 0; }
.cancel__title { margin: 0; font-family: var(--serif); font-size: 21px; font-weight: 400; line-height: 1.25; color: var(--ink); }
/* THE LOAD-BEARING PHRASE. "Seven days away" is how a tutor knows which lane
   they are in before reading anything else. */
.cancel__lede { margin: 9px 0 0; font-size: 13.5px; line-height: 1.65; color: var(--muted); }

.cancel__scopewrap { padding: 16px 18px 0; }
.cancel__legend { display: block; padding: 0; font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted); }
.cancel__scope { margin: 0; padding: 0; border: 0; }
.cancel__rows { margin-top: 8px; display: flex; flex-direction: column; gap: 7px; }
.cancel__scoperow {
  min-height: 52px; display: flex; align-items: center; gap: 11px;
  padding: 0 14px; border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  background: var(--off-white); box-sizing: border-box; cursor: pointer;
  font-size: 14.5px; color: var(--ink);
}
/* The radio is drawn rather than native, because the mock's selected state is a
   2px border on the whole row and a filled disc, not a platform control. */
.cancel__radio { position: absolute; opacity: 0; width: 0; height: 0; }
.cancel__dot {
  flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%;
  border: 1.5px solid rgba(27,67,50,.3); box-sizing: border-box;
}
.cancel__scoperow:has(.cancel__radio:checked) { border: 2px solid var(--green-deep); font-weight: 600; }
.cancel__scoperow:has(.cancel__radio:checked) .cancel__dot { border: 5px solid var(--green-deep); }

.cancel__conseqwrap { padding: 16px 18px 0; }
/* The advance lane: quiet, because nothing is at stake. */
.cancel__consequence {
  margin: 0; padding: 14px 16px;
  background: rgba(27,67,50,.03); border: 1px solid rgba(27,67,50,.14); border-radius: 4px;
  font-size: 12.5px; line-height: 1.65; color: var(--muted);
}
.cancel__consequence b { color: var(--ink); font-weight: 600; }
/* The day-of lane: a 2px gold edge, because this is the consequence that can
   cost a family money. */
.cancel__consequence--dayof {
  background: var(--gold-tint); border: 2px solid var(--gold); color: var(--gold-ink);
}
.cancel__consequence--dayof b { color: var(--gold-ink); font-weight: 700; }
/* The second half of the day-of consequence sits OUTSIDE the gold box: the hour
   freeing up is a fact about the calendar, not about the charge. */
.cancel__aside { margin: 11px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--muted); }

.cancel__foot { padding: 18px 18px 26px; display: flex; flex-direction: column; gap: 8px; }
.cancel__go {
  min-height: 52px; display: flex; align-items: center; justify-content: center;
  width: 100%; border: 0; background: var(--green-btn); border-radius: 4px;
  font-family: var(--sans); font-size: 13.5px; font-weight: 700; letter-spacing: .06em;
  color: #FDFCF9; cursor: pointer;
}
.cancel__keep {
  min-height: 44px; display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; color: var(--muted);
}

/* ── CONNECT CALENDAR, the page ──────────────────────────────────────────── */
.cal--connect { max-width: 560px; }

/* ⚠ THE SIDE GUTTERS, PUT BACK, AND THEY WERE NEVER SHIPPED RATHER THAN LOST.
   Reported from a phone: the card touching both screen edges and the intro
   paragraph reading as clipped.

   WHERE THEY WENT. This page is `<div class="page cal cal--connect">`. The
   phone `.page` rule sets `padding: 22px 16px 36px`, but it lives in an early
   media block, and `.cal { padding: 0 0 24px }` further down the file is a base
   rule at EQUAL specificity, so it wins on source order and zeroes the sides.
   Its own comment says why it does that: "Overrides .page's 26px side padding:
   the mock pads each section itself", which is exactly right for the calendar
   grid, where .cal__head, .cal__foot and the day rows each carry their own 18px.
   The connect page took the .cal class without having self-padding sections, so
   it inherited edge-to-edge and had no gutter of its own to put back.

   MEASURED at 375, 390, 393 and 430 with the insets forced to an iPhone's 59
   and 34: .page, .conn__rows, .conn__lede and .conn__facts all ran l=0 to the
   full viewport width with padding-left and padding-right computing to 0px.

   ⚠ AND THERE IS NO HORIZONTAL OVERFLOW, which is worth writing down because it
   is the half of the report that does NOT reproduce. documentElement.scrollWidth
   equals clientWidth at every one of those widths and nothing renders past the
   viewport edge. What reads as text clipped mid-word is text that begins at
   x=0: with no gutter the first character sits on the physical edge, where a
   rounded corner and the bezel take the rest.

   18px to match the mock, and it is the same gutter .cal__head already uses, so
   the heading above and the card below line up on one edge.

   ⚠ LONGHANDS, NOT A SHORTHAND. The bottom padding on this page is the tab bar
   clearance set in the last block of this file; a shorthand here would reset it
   and put the last row back under the bar. Phone only: above the breakpoint the
   560px cap and the auto margins already hold it off the edges. */
@media (max-width: 760px), (max-height: 499px) {
  .cal--connect { padding-left: 18px; padding-right: 18px; }
}
.conn__body { margin-top: 18px; }
.conn__lede { margin: 0; font-size: 13.5px; line-height: 1.65; color: var(--muted); }
/* ⚠ .conn__field, .conn__label, .conn__hint AND .conn__webcal ARE GONE WITH THE
   INPUT THEY DRESSED. The link is no longer standing text on this page: it lives
   behind the sixth row, which copies it. Deleted rather than left orphaned so
   that putting the input back is a decision somebody makes on purpose, with the
   overflow it caused in front of them, rather than a class that happens to still
   have styling.

   .conn__url STAYS, and is now built by public/calendar-connect.js alone. It is
   the fallback for a refused clipboard: with the link off the page, a copy that
   cannot happen would otherwise leave a tutor with no route to the value at all,
   so the script puts a selectable field back exactly when it is needed. */
.conn__url {
  margin-top: 7px; width: 100%; min-height: 48px; padding: 0 12px; box-sizing: border-box;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
  font-family: var(--sans); color: var(--ink);
  /* The 16px floor, and this one is focusable by design: a tutor taps it to
     select the whole link. ⚠ DO NOT SHRINK THIS TO MAKE A LONG URL FIT. Anything
     under 16px makes iOS zoom on focus and the zoom persists after the keyboard
     closes, which is a worse fault than the one being fixed. Fitting the URL is
     what the copy row is for. */
  font-size: 16px;
}
.conn__fallback { margin: 10px 0 0; display: flex; flex-direction: column; gap: 4px; }
.conn__fallbacksay { font-size: 11.5px; font-weight: 600; color: var(--gold-ink); }

/* ── THE SIX DESTINATIONS ──────────────────────────────────────────────────
   One card, hairline between rows, so six taps read as one list rather than six
   cards. overflow:hidden is what lets the first and last rows take the card's
   radius without either of them carrying a corner of its own. */
.conn__rows {
  margin-top: 14px; background: var(--off-white);
  border: 1px solid var(--line); border-radius: 6px; overflow: hidden;
  display: flex; flex-direction: column;
}
/* ⚠ 56px MINIMUM AND IT IS A TAP TARGET, not a look. Five of these six rows hand
   off to another app, which is not an action to be half-hit on a phone.
   min-height rather than height: the Google row's sub line is heavier and the
   work-or-school name can wrap at 390, and a fixed height would clip both. */
.conn__row {
  min-height: 56px; padding: 12px 13px;
  border-bottom: 1px solid rgba(27,67,50,.09);
  display: flex; align-items: center; gap: 12px;
  /* It is a <button> in one case and an <a> in five, so the button's own
     appearance has to be unset for the two to render as one list. */
  width: 100%; box-sizing: border-box; text-align: left;
  background: none; border-left: 0; border-right: 0; border-top: 0;
  font-family: var(--sans); color: inherit; cursor: pointer;
}
.conn__row:last-child { border-bottom: 0; }
.conn__row:hover { background: rgba(27,67,50,.03); }
.conn__glyph {
  width: 30px; height: 30px; flex: none; border-radius: 7px;
  background: rgba(27,67,50,.06); color: var(--green-btn);
  display: flex; align-items: center; justify-content: center;
}
.conn__rowtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.conn__name { font-size: 13px; font-weight: 600; line-height: 1.3; color: var(--ink); }
.conn__sub { font-size: 10.5px; line-height: 1.3; color: var(--muted); }
.conn__chev { flex: none; font-size: 17px; line-height: 1; color: #8A948C; }

/* ⚠ BOTH GOOGLE SUB LINES ARE IN THE MARKUP AND THE WIDTH PICKS ONE. The phone
   line is not a warning about our software: the Google Calendar APP cannot add a
   calendar by link at all, only Google Calendar in a browser can, and no design
   gets around it. Gold rather than red, because it is a fact about where the
   tutor has to be standing and not an error they have made. Nothing in this
   product uses red. */
.conn__sub--desk { display: none; font-weight: 700; letter-spacing: .04em; color: var(--gold-ink); }
/* ⚠ AFTER the rule above, not merged into it. A media query adds NO specificity,
   so this wins on source order alone and would silently lose if somebody moved
   it up. Same breakpoint as the shell, so "phone" means one thing on this page.

   The row is ONE row either way. On a laptop it is an ordinary link that deep
   links straight through and the panel never opens; on a phone the sub line says
   where it has to be done and the script opens the panel instead. Hiding the row
   on a phone is not an option: a tutor looking for Google and not finding it
   concludes Rhythm does not support Google. */
@media (max-width: 760px), (max-height: 499px) {
  .conn__sub--wide { display: none; }
  .conn__sub--desk { display: block; }
}

/* ── THE COPY ROW, ANSWERING IN PLACE ──────────────────────────────────────
   Both states are rendered and one is hidden, so the swap cannot reflow: the
   row is the same 56px before and after and nothing below it moves. */
.conn__glyphdone, .conn__name--done, .conn__sub--done { display: none; }
.conn__row--copy.is-copied { background: var(--gold-tint); }
.conn__row--copy.is-copied .conn__glyph { background: rgba(201,162,39,.16); color: var(--gold-ink); }
.conn__row--copy.is-copied .conn__name { color: var(--gold-ink); }
.conn__row--copy.is-copied .conn__glyphidle,
.conn__row--copy.is-copied .conn__name--idle,
.conn__row--copy.is-copied .conn__sub--idle { display: none; }
.conn__row--copy.is-copied .conn__glyphdone,
.conn__row--copy.is-copied .conn__name--done { display: block; }
.conn__row--copy.is-copied .conn__sub--done { display: block; }
.conn__row--copy.is-copied .conn__chev { visibility: hidden; }

/* ── THE PANEL BEHIND THE GOOGLE ROW ───────────────────────────────────────
   ⚠ display:none REPEATED AGAINST THE hidden ATTRIBUTE, and this is the one
   line here that comes from an incident rather than a mock. SPEC.md records an
   author-origin display on a row beating the user agent's [hidden]{display:none}
   so that rows nobody could see sat in plain sight on production. This block is
   display:flex when open, so the attribute ALONE would not hide it. */
.conn__panel[hidden] { display: none; }
.conn__panel {
  margin-top: 12px; padding: 14px 15px 15px;
  background: var(--gold-tint); border: 1px solid rgba(201,162,39,.45); border-radius: 6px;
  display: flex; flex-direction: column; gap: 9px;
}
.conn__paneleyebrow { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; color: var(--gold-ink); }
.conn__panelsay { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--ink); }
.conn__panelsay b { font-weight: 700; }
.conn__panelacts { display: flex; flex-wrap: wrap; gap: 8px; }
.conn__panelform { margin: 0; }
.conn__panelbtn {
  min-height: 44px; padding: 0 13px;
  border: 1px solid var(--gold); border-radius: 4px; background: var(--off-white);
  font-family: var(--sans); font-size: 12.5px; font-weight: 700; letter-spacing: .03em;
  color: var(--gold-ink); cursor: pointer;
}
.conn__panelsent { margin: 0; font-size: 12px; line-height: 1.55; font-weight: 600; color: var(--gold-ink); }
.conn__panelthen { margin: 0; font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* ── RESET, NOW A CLOSED ROW ───────────────────────────────────────────────
   Its BEHAVIOUR is unchanged; only its resting size is. The warning sentence
   still says the same thing and the POST still does the same thing. */
.conn__reset {
  margin-top: 14px; background: var(--off-white);
  border: 1px solid var(--line); border-radius: 6px; overflow: hidden;
}
.conn__resetsum {
  min-height: 52px; padding: 12px 13px; box-sizing: border-box;
  display: flex; align-items: center; gap: 12px; cursor: pointer;
  list-style: none;
}
/* Safari draws its own triangle through ::-webkit-details-marker, which
   list-style alone does not remove. */
.conn__resetsum::-webkit-details-marker { display: none; }
.conn__resetname { flex: 1; font-size: 13px; font-weight: 600; color: var(--ink); }
.conn__reset[open] .conn__resetsum .conn__chev { transform: rotate(90deg); }
.conn__resetbody {
  padding: 0 13px 14px; display: flex; flex-direction: column; gap: 10px;
}
.conn__facts { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; }
.conn__facts p { margin: 0; font-size: 12.5px; line-height: 1.65; color: var(--muted); }
.conn__facts b { color: var(--ink); font-weight: 600; }
.conn__revoke { margin: 0; display: flex; flex-direction: column; gap: 7px; align-items: flex-start; }
.conn__revokebtn {
  min-height: 44px; padding: 0 14px; border: 1px solid var(--gold); border-radius: 4px;
  background: var(--gold-tint); font-family: var(--sans); font-size: 12.5px;
  font-weight: 700; letter-spacing: .04em; color: var(--gold-ink); cursor: pointer;
}
.conn__revokenote { font-size: 11.5px; line-height: 1.6; color: var(--muted); }

/* ── RECENT CHANGES ───────────────────────────────────────────────────────
   Transcribed from Calendar Desktop.dc.html's feed panel.

   ONE CARD, DAY HEADERS INSIDE IT, rows separated by hairlines: the sent log's
   family, not a table. Actor, verb, student on one line; the consequence on the
   next; the time and the link beneath.

   ⚠ ROWS WRAP RATHER THAN SCROLL. flex-wrap with flex-basis:100% on the two
   sentence lines means each takes a full row and the time and link share the
   last one, so a 360px phone reflows instead of clipping. That is the fix the
   sent log already needed. */
.cal--changes { max-width: 860px; }
.feed__top {
  padding: 8px 0 0;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap;
}
.feed__eyebrow { font-size: 10.5px; font-weight: 600; letter-spacing: .12em; color: var(--gold-ink); }
.feed__title {
  margin: 5px 0 0; font-family: var(--serif); font-size: 30px; font-weight: 400;
  line-height: 1.1; color: var(--ink); white-space: nowrap;
}
.feed__controls { flex-shrink: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.feed__back {
  flex-shrink: 0; min-height: 44px; display: inline-flex; align-items: center; gap: 5px;
  padding: 0 12px; border: 1px solid rgba(27,67,50,.3); border-radius: 4px;
  background: var(--off-white); font-size: 12px; font-weight: 600;
  color: var(--green-btn); white-space: nowrap;
}
.feed__filter { position: relative; display: inline-flex; }
/* ⚠ THE NATIVE APPEARANCE IS WHY IT WAS THE WRONG FONT. The family was already
   var(--sans), the same token Back to calendar beside it inherits -- but a
   <select> left at its native appearance is drawn BY THE OS, in the system UI
   font, and no font-family declaration reaches it. Turning the appearance off
   is what hands the text back to the stylesheet.

   The caret goes with the native control, so the label draws its own below. */
.feed__select {
  -webkit-appearance: none; appearance: none;
  min-height: 44px; padding: 0 30px 0 12px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  font-family: var(--sans); font-weight: 600; color: var(--ink); white-space: nowrap;
  /* ⚠ THE 16px FLOOR IS A PHONE RULE AND IT STAYS ONE: iOS zooms the page on
     focusing any control under it. On a pointer there is no zoom to provoke, so
     the desktop block below matches this to the button beside it. */
  font-size: 16px;
}
.feed__filter::after {
  content: '\25BE';
  position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  font-size: 9px; color: var(--muted); pointer-events: none;
}

.feed__card {
  margin-top: 16px; background: var(--off-white);
  border: 1px solid rgba(27,67,50,.12); border-radius: 6px; overflow: hidden;
}
.feed__day {
  margin: 0; padding: 10px 18px; background: rgba(27,67,50,.05);
  border-bottom: 1px solid rgba(27,67,50,.1);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
}
.feed__row {
  padding: 13px 18px; background: var(--off-white);
  border-bottom: 1px solid rgba(27,67,50,.07);
  display: flex; flex-wrap: wrap; gap: 5px 14px; align-items: baseline;
}
.feed__row:last-child { border-bottom: 0; }
/* SENTENCES, NOT COLUMNS. Actor, verb, student reads at a glance; a five-column
   table of the same facts does not. */
.feed__line { flex-basis: 100%; margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--body); }
.feed__actor, .feed__student { color: var(--ink); font-weight: 700; }
.feed__verb { color: var(--body); font-weight: 400; }
/* ⚠ EVERY ROW CARRIES ITS CONSEQUENCE. Without it the same-day and the advance
   cancellation read identically, and that is the single most important
   distinction in this flow. */
.feed__detail { flex-basis: 100%; margin: 0; font-size: 12px; line-height: 1.6; color: var(--muted); }
.feed__at { font-size: 11.5px; color: var(--muted); white-space: nowrap; }
/* ⚠ THE UNDERLINE BELONGS TO THE TEXT, NOT TO THE TAP TARGET. It was a
   border-bottom on a box 44px tall with the text centred in it, so the line sat
   about fourteen pixels under the words and read as a rule across the row
   rather than as a link. The 44px stays -- it is the tap target and is not
   negotiable -- and the line moves onto the glyphs, where a link's underline
   goes. */
/* ⚠ `a.feed__open`, NOT `.feed__open`. `.page a { text-decoration: none }` is a
   class AND a type, so it outranks a bare class and quietly strips the
   underline -- which is almost certainly why this reached for a border in the
   first place. Matching its specificity and coming later in the file is what
   makes the rule stick. */
a.feed__open {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 11.5px; color: var(--muted); white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: rgba(27,67,50,.35);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* ⚠ THE SIZE DOES NOT MATCH THE BUTTON, AND THAT IS A CONFLICT RATHER THAN AN
   OVERSIGHT. The polish ruling asks for the same family, size and weight as
   Back to calendar, which is 12px. test/survey-submitting refuses ANY rule
   reaching a form control under 16px, because iOS zooms the page on focusing
   one, and it is deliberately blind to media queries: a blanket floor with no
   exceptions is the only kind that survives.
   
   A desktop-only 12px was written here and reverted the same hour: it broke
   that guard, and weakening a floor that exists for a real phone defect is not
   a call to make while fixing a font. THE FAMILY AND THE WEIGHT NOW MATCH; the
   size is Trey's to rule on, and the readback names both ways out. */
.feed__empty { margin: 16px 0 0; font-size: 12.5px; color: var(--muted); }

@media (max-width: 760px), (max-height: 499px) {
  .sheet2__three { gap: 7px; }
  .sheet2__ctl { flex: 1 1 100%; }
  .sheet2__book { font-size: 12.5px; }
}

/* ══ ATTENDANCE ═════════════════════════════════════════════════════════════
   Four options, then one Submit. Both widths: attendance is not a desktop
   feature, it is the thing a tutor does on a phone straight after a lesson.

   IT SITS WITH THE CALENDAR'S OTHER SECTIONS, which is where a reader looks
   for it. It spent one commit declared ABOVE the calendar instead, because
   "no red anywhere on the calendar" sliced the file from the calendar's header
   to the END OF IT and would have failed on this block wherever in that region
   it sat. That guard now reads rules BY SELECTOR and exempts .att by name, so
   the displacement is no longer needed and has been undone.

   The confinement itself is asserted in test/calendar-attendance.js, and more
   tightly than a region scan could: only a .att__mark-- rule may read these
   tokens, and no calendar rule may borrow --danger.

   ⚠ THE ONLY TWO NEW COLOURS IN THE PRODUCT, AND THEY ARE CONFINED HERE.
   design_handoff_calendar_desktop is explicit that purple and red belong to
   attendance and must not leak, so they are declared as attendance tokens on
   this block rather than added to :root beside the palette. Nothing outside
   .att-- can reach them, and test/calendar-page.test.js's "no red" check reads
   the rest of the calendar without meeting them.

     present        green tick   #1B7F4B
     present, late  gold tick    #C9A227   (the portal's own gold)
     no-show        purple ?     #5E4B94   NEW
     cancelled      red cross    #B42318   NEW  */
/* ⚠ DECLARED ON BOTH SCOPES THAT READ THEM, IN ONE BLOCK, AND THIS IS A BUG FIX
   RATHER THAN A TIDY-UP.

   THE BUG, reported from a phone: every attendance mark on the calendar was a
   glyph with no disc behind it -- the word rendered, the mark did not. A custom
   property only inherits DOWNWARD from the element it is declared on, and these
   four were declared on `.att`, which is the session sheet's attendance block.
   A calendar row is not inside it, so `background: var(--att-present)` resolved
   to nothing, the disc fell back to transparent, and `color: #fff` left a WHITE
   glyph on a cream row. The word looked fine because its own colour failed the
   same way and inherited the row's ink.

   It was every state on every calendar surface, not the four the phone happened
   to show, and it had been that way since the attendance row shipped.

   ONE DECLARATION, TWO SELECTORS. Not a second copy of the hexes on a calendar
   rule: that is the leak the confinement below is about, and two copies of
   #5E4B94 is how the purple starts travelling. The confinement itself is
   unchanged -- these are still unreachable from anything that is not an
   attendance mark or a calendar attendance slot.

   ⚠ AND THE SECOND SELECTOR IS `.att-scope`, NOT `.calrow__att`, WHICH IS NOT
   COSMETIC. test/calendar-page's no-red guard reads rules BY SELECTOR PREFIX
   and refuses #B42318 on anything beginning `.calrow`; its own comment says
   `.att` is left out because attendance is the sanctioned exception with a
   tighter guard of its own. Declaring the tokens on `.calrow__att` made that
   guard fail -- correctly, on what it can see, since it cannot tell the
   attendance slot from any other calendar rule.

   Naming the scope in attendance's own vocabulary says which of the two it is,
   in the selector, where the guard reads. The class is added by
   partials/calendar-row beside `calrow__att`. */
.att, .att-scope {
  --att-present: #1B7F4B;
  --att-late: #C9A227;
  --att-noshow: #5E4B94;
  --att-cancelled: #B42318;
}
.att { padding: 18px 20px 0; }
.att__label { display: block; font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
.att__done {
  margin: 8px 0 0; display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 600; color: var(--ink);
}
/* THE MARK IS THE GLYPH AND ITS COLOUR, and it is the only thing that carries
   the colour: no filled pill, no tinted row. A tick that is the wrong green is
   a mark; a whole row that is is a state. */
.att__mark {
  flex-shrink: 0; width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700; line-height: 1;
}
.att__mark--present { color: var(--att-present); }
.att__mark--late { color: var(--att-late); }
.att__mark--noshow { color: var(--att-noshow); }
.att__mark--cancelled { color: var(--att-cancelled); }

.att__options { margin-top: 7px; display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.att__option {
  min-height: 44px; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 0 6px; box-sizing: border-box;
  background: var(--off-white); border: 1px solid rgba(27,67,50,.18); border-radius: 4px;
  font-size: 12px; color: var(--ink);
}
/* THE PICK IS GREEN, whichever of the four it is. The mark's own colour is
   reserved for the RECORDED state; a selection is not yet a fact. */
.att__option--on { background: var(--green-deep); border-color: var(--green-deep); font-weight: 700; color: #FDFCF9; }

.att__commit { margin-top: 9px; }
.att__consequence { margin: 0 0 8px; font-size: 11.5px; line-height: 1.6; color: var(--muted); }
.att__submit {
  width: 100%; min-height: 44px; border: 0; border-radius: 4px;
  background: var(--green-btn);
  font-family: var(--sans); font-size: 12px; font-weight: 700; letter-spacing: .06em;
  color: #FDFCF9; cursor: pointer;
}

/* ══ THE FOUR MODALS: ONE SHELL, DESKTOP ONLY ═══════════════════════════════
   design_handoff_calendar_desktop. New session, Session, Find a substitute and
   Cancel are one shell on a desk: a scrim, a cream panel, an 8px radius, and a
   close on the scrim. On a phone every one of them is the bottom sheet it has
   been through QC 5 and NOTHING in this block reaches it.

   ⚠ THE SCRIM IS position: fixed. It is declared inside a min-width query, so
   it cannot exist at any phone width and cannot be the fixed element CLAUDE.md
   records unpinning the tab bar. Called out in the readback all the same,
   because the rule there is that a fixed element is declared and not reasoned
   away.

   THE PANEL SCROLLS WITH THE PAGE rather than inside a fixed box. The scrim is
   what is pinned; the panel is an ordinary centred block above it. A modal
   taller than the viewport therefore scrolls the way the page does, which is
   the behaviour that needs no keyboard handling and no scroll locking.

   WIDTHS ARE THE CANONICAL'S, one per surface, because they are not arbitrary:
   the create modal is two columns and needs 700, the cancel modal is one column
   of prose and 560 is what keeps its line length readable. */
.calmodal__scrim { display: none; }
/* Inert on a phone: the create sheet's three column wrappers exist only so a
   desktop can place them, and display:contents leaves the phone's DOM flat. */
.sheet2__ctltop, .sheet2__ctlrest, .sheet2__dayside { display: contents; }

@media (min-width: 761px) and (min-height: 500px) {
  .calmodal__scrim {
    display: block;
    position: fixed; inset: 0; z-index: 1;
    background: rgba(20,55,42,.42);
  }
  .cal--sheet, .cal--session, .cal--cancel {
    position: relative; z-index: 2;
    margin: 34px auto;
    border-radius: 8px;
    box-shadow: 0 18px 50px rgba(20,55,42,.3);
    /* The grab handle is the phone's affordance for a sheet that is dragged.
       Nothing here is dragged, so it goes rather than sitting on as decoration. */
  }
  .cal--sheet .sheet2__grab,
  .cal--session .sheet2__grab,
  .cal--cancel .sheet2__grab { display: none; }

  .cal--sheet { max-width: 700px; padding-bottom: 0; }

  /* ── THE CREATE MODAL IS TWO COLUMNS ──────────────────────────────────────
     Controls in a 262px left column, the read-only day beside them. Same five
     decisions, same order within each column, nothing dropped: the canonical
     says desktop changes the ARRANGEMENT of the phone sheet and nothing else.

     THREE WRAPPERS, EACH display:contents ON A PHONE, so the DOM order below
     761px is exactly the phone's and the live region
     public/calendar-controls.js swaps is still one subtree.

     TWO ROWS, THE SECOND 1fr. The day side spans both, and a spanning grid item
     distributes its height across every track it spans; with content-sized rows
     that opened a gap above every control in the left column. Making the second
     row 1fr sends all of that slack to the foot of the column instead. */
  .cal--sheet .sheet2__controls {
    display: grid;
    grid-template-columns: 262px 1fr;
    grid-template-rows: min-content 1fr;
    gap: 0 22px;
    padding: 18px 22px 20px;
  }
  .cal--sheet .sheet2__ctltop  { display: block; grid-column: 1; grid-row: 1; }
  .cal--sheet .sheet2__ctlrest { display: block; grid-column: 1; grid-row: 2; align-self: start; }
  .cal--sheet .sheet2__dayside { display: block; grid-column: 2; grid-row: 1 / span 2; align-self: start; }

  /* The form owns the side padding now, so its children stop repeating it. */
  .cal--sheet .sheet2__field,
  .cal--sheet .sheet2__sched,
  .cal--sheet .sheet2__three,
  .cal--sheet .sheet2__secondrow,
  .cal--sheet .sheet2__taken,
  .cal--sheet .sheet2__repeat,
  .cal--sheet .sheet2__holdwrap { padding-left: 0; padding-right: 0; }
  /* ⚠ .sheet2__warn IS DELIBERATELY NOT IN THE LIST ABOVE, and it used to be.
     Every element there takes its 20px inset FROM padding, so on a desktop the
     column supplies the inset and the padding goes. This box is the one whose
     horizontal padding is its own INTERIOR: its inset comes from
     `margin: 12px 20px 0`, and `padding: 11px 13px` is what holds the sentence
     off its own border. Swept in, it computed `11px 0px 11px 0px` here while
     390, where the rule does not reach, kept the correct `11px 13px`. The text
     sat hard against the left border; the right only LOOKED roomy because the
     sentence ends before the edge.

     ⚠ AND THE MARGIN IS WHAT THE DESKTOP TAKES INSTEAD. The box carries its
     own 20px AND sits inside the desktop column's inset, so it started 20px
     right of every control above it: measured at 1280, picker 312 and box 332.
     Zeroing the sides lands it on 312 with the rest of the sheet.

     A reading of 332 against 332 was taken at one point and used to argue the
     margin should stay. That reading came from the collapsed layout described
     below, where every column was 62px wide and nothing was where it belonged.
     It was wrong about the page, not about the rule.

     ⚠ AND IT IS REMOVED FROM THE SELECTOR LIST, NOT BY DELETING THE BLOCK.
     Taking `.cal--sheet .sheet2__warn { ... }` out wholesale left
     `.cal--sheet .sheet2__holdwrap,` dangling with no declarations, so the
     parser ran the whole list into the NEXT rule and every element in it --
     the fields, the schedule, the three-control row -- was given
     `flex: 0 0 62px; width: 62px`. The desktop sheet collapsed to a 62px
     column. It shipped, because no test measured that column and both guard
     scripts are blind to it: audit-cascade looks for rules that cannot win,
     and this rule won. test/calendar-sheet-desktop-columns now measures it. */
  .cal--sheet .sheet2__warn { margin-left: 0; margin-right: 0; }

  /* Three controls in a 262px column, at the canonical's own widths: TIME and
     HOW LONG share what is left after REPEAT's fixed 62. */
  .cal--sheet .sheet2__ctl--repeat { flex: 0 0 62px; width: 62px; }
  .cal--sheet .sheet2__control { min-height: 44px; padding: 0 8px; }
  .cal--sheet .sheet2__value { font-size: 13px; white-space: nowrap; }

  /* THE FOOT IS OUTSIDE THE GRID, in the POST form below it, and stays a bar at
     the foot of the panel: the portal's convention for a modal's actions and
     the canonical's for all four of these. */
  .cal--sheet .sheet2__foot {
    margin: 0; padding: 16px 22px 20px;
    border-top: 1px solid rgba(27,67,50,.1);
  }

  /* FIXED HEIGHT, THE DAY SCROLLS INSIDE IT. The canonical is explicit that the
     sheet never changes height, so the controls and the button stay put while
     the day is scrolled. 264px is six hours at rest of the eleven it holds. */
  .cal--sheet .sheet2__grid { height: 264px; overflow-y: auto; overscroll-behavior: contain; }
  .cal--session { max-width: 640px; }
  /* Substitute shares the session's markup and carries its own width: it is a
     list of tutors and their free hours, and the canonical draws it narrower. */
  .cal--sub { max-width: 620px; }
  .cal--cancel { max-width: 560px; }
}

/* THE CALENDAR SITS ABOVE THE OVERLAY SECTION DELIBERATELY. That section is
   delimited by "from its heading to the end of the file", and
   test/survey-submitting.test.js asserts every selector inside it is
   overlay-scoped. Appending here rather than at the foot is what keeps that
   guard meaning what it says. */
/* ══ THE CALENDAR SUB-PAGES CLEAR THE TAB BAR ══════════════════════════════
   ⚠ DECLARED LAST, AND IT HAS TO BE. A media query adds no specificity, so the
   mobile `.cal { padding-bottom: ... }` above is outranked by every later
   `.cal--sheet`, `.cal--session`, `.cal--cancel`, `.cal--connect` and
   `.cal--changes` rule that sets a padding shorthand. Each of those was
   therefore zeroing the clearance the fixed tab bar needs, and the last of the
   sheet could not be scrolled to: the BOOK button sat under the bar with
   nothing left to scroll.

   That is the SAME defect, from the same cause, as the Week bug fixed in
   e15d91d. It is fixed here for the whole family rather than one page at a
   time, and being the last rule in the file is what makes it hold.

   THE NUMBER IS THE BAR'S OWN, 8 + 52 + 14, plus the band iOS draws the home
   indicator in, so the two move together.

   NOT APPLIED TO .cal--shell: the calendar itself is a flex shell whose
   scroller already ends where the pinned bar begins, and adding padding there
   would put a gap under the last row. */
@media (max-width: 760px), (max-height: 499px) {
  .cal--sheet,
  .cal--session,
  .cal--cancel,
  .cal--connect,
  .cal--changes {
    padding-bottom: calc(60px + max(14px, var(--sab)));
  }
}

/* ══ THE PHONE SHELL: LOCKED NAV, SCROLLING MIDDLE, LOCKED TAB BAR ═════════
   QC 6, ruled: "locked nav bar, then a dynamic scrollable container, then the
   locked bottom tab bar." The header ran off the top of the screen and the
   green above it went with it.

   ⚠ THE NAV IS NOT position: fixed, AND THAT IS THE POINT.

   A third fixed element on this shell is the one thing this stylesheet has
   already been burned by twice, and it would also need a height constant: the
   mobile .nav is `height: auto`, so any page padding that cleared it would be a
   number copied from a rule that is free to change. Instead the BODY stops
   scrolling and a child scrolls. The nav is an ordinary static flex item that
   cannot scroll away because there is nothing under it to scroll, and it keeps
   whatever height it turns out to have. No number, no script, no fourth fixed
   element.

   THIS IS THE PATTERN THE CALENDAR ALREADY RUNS, generalised. body:has(.cal--shell)
   has done exactly this since the calendar landed and has been through four
   device passes with no detach reported, which is also the strongest thing
   known about the tab bar: a fixed element in a document that never scrolls
   cannot come unpinned during a scroll, because the document has none.

   SCOPED BY :has(> .tabbar), so it is on precisely the pages that HAVE the
   mobile shell. The survey, login and every student-facing screen carry no tab
   bar and go on scrolling as documents. Where :has() is unsupported the block
   does not apply at all and the page scrolls as it does today: degraded, not
   broken.

   ⚠ THE COST, STATED: an inner scroller is not restored on a back navigation
   the way a document scroll is. Returning to a long list lands at the top.
   That is the trade the ruling buys, and it is worth naming rather than
   discovering. */
@media (max-width: 760px), (max-height: 499px) {
  /* ⚠ THE DOCUMENT MUST NOT SCROLL, AND THIS IS THE LINE THREE FIXES MISSED.
   *
   * THE GREEN BLOCK, FOURTH ATTEMPT, AND THE FIRST ONE THAT IS NOT ABOUT
   * HEIGHT. The three before this all corrected the BODY's height: publish
   * --vh in pixels, then recompute it on the right event, then take svh so it
   * cannot move. All three worked. The body is exactly 100svh and clips
   * correctly, and the band still came back from a real phone.
   *
   * Because the band was never caused by a wrong height. MEASURED on the
   * review sheet at 390: body 844 tall and overflow:hidden, correct, while
   * `html` was overflow:visible with scrollHeight 1577 against clientHeight
   * 844 -- A DOCUMENT THAT CAN SCROLL 733px. Focusing the REASON field made
   * the browser scroll the DOCUMENT to reveal it, window.scrollY went to 22,
   * the body's top went to -22, and its bottom edge lifted to 822. The strip
   * below it is bare canvas, and the canvas takes the body's green.
   *
   * ⚠ AND IT NEVER CAME BACK, which is the whole symptom. Blurring restored
   * nothing: scrollY stayed at 22. `overflow: hidden` on the BODY does not
   * stop the document scrolling -- the scrollingElement is `html` -- so the
   * shell was scrolled out from under the viewport with no way to return.
   * On a phone the keyboard is not 22px, it is a few hundred, which is why
   * Trey saw everything below the field go green.
   *
   * ⚠ AND CONSTRAINING `html` DOES NOT FIX IT. That was tried here first and
   * MEASURED: html at 100svh with overflow:hidden, and then with
   * overflow:clip, and then clip on both html and body. All three left
   * scrollY at 22 and the 22px band still open. `overflow: hidden` clips
   * painting but stays PROGRAMMATICALLY scrollable, and revealing a focused
   * field is a programmatic scroll, so the one thing that had to be stopped
   * is the one thing hidden does not stop.
   *
   * WHAT IS PROVEN, by hiding each body child in turn and re-reading
   * html.scrollHeight: it is .page that makes the document scrollable.
   * Hiding it takes html.scrollHeight from 1577 to 844 on its own; no other
   * child moves it more than its own height. So the answer is not to clip
   * the scroll, it is to leave the document with nothing to scroll.
   *
   * position: fixed TAKES THE SHELL OUT OF FLOW, so it contributes no
   * scrollable overflow to the viewport at all. Measured on the same repro:
   * html.scrollHeight falls 1577 -> 844, scrollY stays 0 through focus AND
   * blur, no band opens below the body, .page still scrolls, and .tabbar
   * stays exactly where it was (top 769, bottom 844, still fixed).
   *
   * ⚠ top/left/right AND NOT inset: 0. Pairing bottom:0 with height:100svh
   * over-constrains the box and the engine drops one of them; leaving bottom
   * unset keeps 100svh the single source of the shell's height, which is the
   * whole of the third fix and is not being given up.
   *
   * ⚠ IT DOES NOT MAKE body THE CONTAINING BLOCK FOR .tabbar, which is worth
   * stating because it is the first thing to fear here and it is the exact
   * area SPEC.md records two incidents in. A plain position:fixed ancestor
   * does not establish a containing block for fixed descendants; only
   * transform, filter, perspective, backdrop-filter, contain and will-change
   * of those do. MEASURED rather than reasoned, because reasoning about
   * containing blocks is what wasted three device passes on the last one:
   * with the body forced to 400px the bar does not move, staying at bottom
   * 844. It is still anchored to the viewport, exactly as before.
   *
   * Geometry is therefore UNCHANGED by this rule. Only flow participation
   * changes: the body ends at 100svh from the top either way, the bar sits at
   * the viewport bottom either way, and .page still scrolls. That is the whole
   * of why this is the smallest change that could work.
   *
   * ⚠ AND IT IS STILL NOT CLOSED BY A GREEN SUITE. Headless has no async
   * scrolling thread and no keyboard; it is the shell, and the shell is the
   * one place in this codebase where measurement has twice said fine and the
   * phone has said otherwise. DEVICE PASS REQUIRED. */
  body:has(> .tabbar) {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;

    /* ⚠ 100svh, AND THE UNIT IS THE WHOLE FIX. This is the shell the green
       block comes from: the body carries the green ground, .page carries the
       cream, and overflow:hidden clips .page to the body. So any moment the
       body is SHORTER than the screen, the strip below it paints the body's
       green and stays there.
     *
     * TWO EVENT-PATCH ATTEMPTS FAILED HERE. First a JS-published --vh
     * recomputed on focusout, then again on focusin and the visual viewport's
     * scroll. Both were the same shape of answer: keep a number in step with a
     * moving target by listening for the right moment. There is always another
     * moment.
     *
     * svh IS THE SMALL VIEWPORT HEIGHT AND IT DOES NOT MOVE. Not for the URL
     * bar, not for the keyboard. dvh is the DYNAMIC one and is what shrank for
     * the keyboard and was reported not to grow back; svh is defined as the
     * viewport with the browser UI at its largest, so there is no state to go
     * stale and nothing to recompute. The keyboard overlays this shell instead
     * of resizing it, which is what makes a dead band structurally impossible
     * rather than merely unobserved.
     *
     * A DEFINITE HEIGHT IS REQUIRED, not min-height: .page below is
     * flex:1 with overflow-y:auto, so it is the scroller and needs a bounded
     * parent. min-height would let it grow with its content and overflow:hidden
     * would then clip the bottom of every long page out of reach.
     *
     * The dvh line is the fallback for an engine without svh, which shipped
     * alongside it; it is ordered first so svh wins wherever both are known. */
    height: 100dvh;
    height: 100svh;
    display: flex;
    flex-direction: column;

    /* ⚠ hidden ON BOTH AXES, AND THE X IS THE RULING. No mobile surface scrolls
       horizontally, ever; the scroll track is vertical only. This is the fence
       rather than the fix -- whatever is wide is still wide -- so it sits
       alongside the widths below rather than instead of them. It is here so a
       future wide element cannot reintroduce sideways drift the way this one
       did. */
    overflow: hidden;

    /* ⚠ GREEN, NOT CREAM, AND IT IS THE BODY THAT CARRIES IT. QC 7 ruling: the
       band above the nav renders the SAME green as the nav, continuous from the
       top of the screen through it, on every portal page.

       WHY THE BODY AND NOT .navtop ALONE. .navtop already paints the top inset
       green and it is not enough: with apple-mobile-web-app-status-bar-style
       set to `default`, iOS does not extend the view under the status bar, so
       env(safe-area-inset-top) is 0, .navtop has no height, and the band the
       clock sits in takes the app's own background -- which was cream. Painting
       the BODY green makes that band green whether the inset resolves or not,
       which is the only version that does not depend on an answer this side
       cannot see. theme-color and the manifest's theme_color are already
       #14372A and agree with it.

       The cream moves to .page, which is `flex: 1` and therefore fills every
       pixel between the two bars. Nothing else changes colour. */
    background: var(--green-deep);
  }

  /* ⚠ NO RULE FOR .nav OR .navtop HERE, DELIBERATELY, and the obvious one is
     flex-shrink: 0 so neither bar can be squeezed.

     It is not needed. .page below is `flex: 1`, which is flex-basis: 0%, so it
     contributes nothing to the total and there is never an overflow to
     distribute; combined with min-height: 0 it absorbs all the free space and
     shrinks to nothing before either bar gives up a pixel.

     And it would be actively harmful. Adding it made the LAST rule for .nav in
     this file one outside the phone override block, and test/training-mobile-
     layout failed exactly as designed: it asserts the phone block is the last
     word on .nav, because a media query adds no specificity and this stylesheet
     has twice shipped an override that read as a fix while a later rule won.
     body:has(> .tabbar) > .nav is higher specificity and would in fact have won
     -- but a guard that has to reason about which is the point at which it
     stops being a guard. body:has(.cal--shell) declined the same rule for the
     same reason and says so at its own site.

*/

  /* THE ONLY SCROLLER, and min-height: 0 is load-bearing. Without it a flex
     child refuses to shrink below its content, so the container grows instead
     of scrolling and the nav goes back off screen.

     :not(.cal--shell) because the calendar is the one page that already has an
     inner scroller of its own, .cal__scroll, and two nested scrollers is a page
     that scrolls twice. Written as :not() rather than left to source order
     because the two selectors are the same specificity and this must not turn
     on which block came last.

     THE BOTTOM PADDING IS UNCHANGED and still clears the tab bar: the scroller
     runs to the bottom of the viewport, the bar is fixed over it, so the last
     row still needs the bar's height reserved. That contract and its three
     dependants are untouched. */
  body:has(> .tabbar) > .page:not(.cal--shell) {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* ⚠ AND NEVER SIDEWAYS. overflow-y alone computes overflow-x to `auto` as
       well, so the scroller could pan horizontally the moment anything inside
       it was wide. Stated on both axes, so the track is vertical by
       declaration rather than by luck. */
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    /* A scroll that reaches either end does not chain to the body. */
    overscroll-behavior: contain;
  }

  /* THE CREAM THE BODY GAVE UP. .page is flex: 1, so it fills everything
     between the locked nav and the locked tab bar and the green never shows
     through. .cal--sheet and .cal--cancel paint their own ground and are
     unaffected. */
  body:has(> .tabbar) > .page { background: var(--cream); }

  /* NOTHING IS WIDER THAN THE SCREEN. The fence above stops travel; this stops
     the clipping the fence would otherwise hide, and it overrides any cap a
     desktop rule leaks below its breakpoint -- which is exactly how a 390px
     artboard width ended up governing a 430px phone.

     ⚠ .page ONLY. The obvious thing is to list .nav and .navtop with it, and
     test/training-mobile-layout refused that and was right to: it would make
     the last rule for .nav in this file one outside the phone override block,
     and a guard that has to reason about whether the override matters is not a
     guard. Neither needs it anyway -- both are flex items in a column
     container, so they are stretched to the body's width and cannot exceed it.
     Same reason the shell declines a flex-shrink rule for them above. */
  body:has(> .tabbar) > .page { width: 100%; max-width: 100%; }

  /* ⚠ width: 100% ABOVE IS NOT BELT AND BRACES. .page carries `margin: 0 auto`
     from its base rule, and an auto inline margin on a flex item OVERRIDES the
     stretch it would otherwise get: the item sizes to its content and the
     margins centre it. In portrait that is invisible, because the sheet's
     content is at least as wide as the screen. Rotate to landscape and it is
     the QC 7 gutter again in a new place -- the create sheet measured 447px
     inside an 844px screen, with 198px of ground either side. A definite width
     makes the auto margins resolve to zero, which is what stretch would have
     done. */

  /* THE WORK VIEWER'S SCROLL LOCK MOVES WITH THE SCROLL. `body.wview-open
     { overflow: hidden }` locked the document, and the document is no longer
     what scrolls here, so the page went on scrolling behind the viewer. */
  body:has(> .tabbar).wview-open > .page { overflow: hidden; }
}

/* ── THE PRE-SESSION TAB ─────────────────────────────────────────────────────
   design_handoff_presession 1a. Settings left, preview right, stacking on a
   narrow screen.

   ⚠ 44px MINIMUM ON EVERY TAP TARGET and a 16px FLOOR ON EVERY FOCUSABLE
   FIELD. The chips, the palette tokens, the toggles, the audience switch and
   both buttons all carry the height; the three copy fields carry the size, so
   iOS does not zoom the page when one is focused. Neither is negotiable and
   both are guarded by tests.
   ⚠ NO RED ANYWHERE on this surface, the failed-send line included. */

.pst { max-width: 1000px; }

.pst__head { padding: 0 0 4px; }
.pst__title {
  margin: 0;
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 25px; font-weight: 400; color: #1D2B23;
}
.pst__sub { margin: 6px 0 0; font-size: 13px; line-height: 1.6; color: #5C6B62; }

.pst__flash {
  margin: 14px 0 0; padding: 10px 13px;
  background: rgba(27, 67, 50, .07);
  border-left: 3px solid #1B4332; border-radius: 3px;
  font-size: 13px; color: #1B4332;
}
/* Gold, not red: a test send with nothing booked is an explanation. */
.pst__flash--no {
  background: rgba(201, 162, 39, .14);
  border-left-color: #C9A227; color: #7C6210;
}

.pst__body {
  margin-top: 16px;
  display: flex; flex-wrap: wrap; align-items: flex-start;
  background: #FAF7F0;
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 6px;
  overflow: hidden;
}
.pst__left,
.pst__right { flex: 1; min-width: min(340px, 100%); box-sizing: border-box; padding: 22px 26px; }
.pst__left { display: flex; flex-direction: column; gap: 22px; }
.pst__right { background: rgba(27, 67, 50, .03); border-left: 1px solid rgba(27, 67, 50, .09); }

.pst__sec { display: block; }
.pst__label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: #5C6B62;
}
.pst__rule { height: 1px; background: rgba(27, 67, 50, .09); }
.pst__note { margin: 6px 0 0; font-size: 11.5px; line-height: 1.5; color: #5C6B62; }

/* ── The hour chips ──────────────────────────────────────────────────────── */
.pst__spelled {
  margin-top: 8px;
  font-family: 'Libre Caslon Text', Georgia, serif;
  font-size: 15px; color: #14372A;
}
.pst__chips {
  margin-top: 7px;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 2px;
  background: rgba(27, 67, 50, .04); border-radius: 5px; padding: 3px;
  max-width: 340px;
}
.pst__chip {
  min-height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px; font-size: 16px; color: #1D2B23; cursor: pointer;
}
.pst__chip--on { background: #14372A; font-weight: 700; color: #FDFCF9; }
/* The radio itself is the state, not decoration: it stays focusable and it
   stays in the tab order, so it is clipped rather than display:none. */
.pst__chipin {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; margin: 0; pointer-events: none;
}
.pst__chip:focus-within { outline: 2px solid #1B4332; outline-offset: 1px; }

/* ── The toggles ─────────────────────────────────────────────────────────── */
.pst__toggles { margin-top: 10px; display: flex; flex-direction: column; gap: 9px; }
.pst__tog {
  min-height: 44px;
  display: flex; align-items: center; gap: 13px; cursor: pointer;
}
.pst__togin { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.pst__switch {
  flex-shrink: 0; width: 44px; height: 26px; border-radius: 100px;
  background: rgba(27, 67, 50, .16); border: 1px solid rgba(27, 67, 50, .2);
  display: flex; align-items: center; justify-content: flex-start;
  padding: 0 3px; box-sizing: border-box;
}
.pst__switch > span {
  width: 20px; height: 20px; border-radius: 50%; background: #FDFCF9;
}
.pst__togin:checked + .pst__switch {
  background: #1B4332; border-color: #1B4332; justify-content: flex-end;
}
.pst__tog:focus-within .pst__switch { outline: 2px solid #1B4332; outline-offset: 2px; }
.pst__togwho { flex: 1; min-width: 0; }
.pst__togname { display: block; font-size: 13.5px; font-weight: 600; color: #1D2B23; }
.pst__tognote { display: block; margin-top: 1px; font-size: 11.5px; color: #5C6B62; }

/* ── The copy fields, their pilled reading, and the palette ──────────────── */
.pst__field {
  display: block; box-sizing: border-box; width: 100%; margin-top: 7px;
  padding: 11px 13px;
  background: #FDFCF9; border: 1px solid rgba(27, 67, 50, .22); border-radius: 4px;
  font-family: inherit; font-size: 16px; line-height: 1.6; color: #1D2B23;
}
.pst__field--area { resize: vertical; }
.pst__pills {
  margin-top: 6px; font-size: 13.5px; line-height: 1.9; color: #5C6B62;
}
.pst__pill,
.pst__token {
  display: inline-block; padding: 1px 7px; margin: 0 1px;
  border-radius: 3px;
  background: rgba(201, 162, 39, .22); border: 1px solid rgba(168, 132, 42, .35);
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12px; font-weight: 600; color: #7C6210;
}
.pst__palette { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
.pst__token {
  min-height: 44px; padding: 0 10px; margin: 0;
  display: inline-flex; align-items: center;
  border-radius: 4px; background: rgba(201, 162, 39, .16);
  border-color: rgba(168, 132, 42, .3);
  font-size: 11.5px; cursor: pointer;
}

/* ── The gold blocks ─────────────────────────────────────────────────────── */
.pst__gold {
  margin-top: 11px; padding: 13px 15px;
  background: rgba(201, 162, 39, .14);
  border: 1px solid rgba(168, 132, 42, .4); border-radius: 5px;
}
.pst__goldlabel {
  font-size: 11px; font-weight: 700; letter-spacing: .08em; color: #7C6210;
}
.pst__gold p { margin: 7px 0 0; font-size: 12.5px; line-height: 1.65; color: #7C6210; }
.pst__see { margin-top: 8px; font-size: 12.5px; color: #7C6210; }
.pst__see summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; }
.pst__see ul { margin: 0 0 4px; padding-left: 18px; }
.pst__see li { margin-top: 3px; }
.pst__see a { color: #7C6210; }

/* ── The two buttons ─────────────────────────────────────────────────────── */
.pst__actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.pst__save {
  min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 20px; background: #1B4332; border: 0; border-radius: 4px;
  font-family: inherit; font-size: 13px; font-weight: 700; letter-spacing: .06em;
  color: #FDFCF9; cursor: pointer;
}
.pst__test {
  min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 16px; background: transparent;
  border: 1px solid rgba(27, 67, 50, .25); border-radius: 4px;
  font-family: inherit; font-size: 12.5px; font-weight: 600; color: #1B4332;
  cursor: pointer;
}
.pst__testform {
  margin-top: 14px; display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
}
.pst__testnote { font-size: 11.5px; line-height: 1.5; color: #5C6B62; }

/* ── The preview ─────────────────────────────────────────────────────────── */
.pst__prevtop {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.pst__aud {
  display: flex; gap: 2px; background: rgba(27, 67, 50, .06);
  border-radius: 5px; padding: 3px;
}
.pst__audbtn {
  min-height: 44px; display: flex; align-items: center; padding: 0 13px;
  border-radius: 4px; font-size: 11.5px; color: #1D2B23; text-decoration: none;
}
.pst__audbtn--on { background: #14372A; font-weight: 700; color: #FDFCF9; }
.pst__none { margin: 12px 0 0; font-size: 13px; line-height: 1.6; color: #5C6B62; }
.pst__prev {
  margin-top: 12px;
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 4px; overflow: hidden;
}
.pst__prevsubj {
  padding: 9px 13px; background: #FDFCF9;
  border-bottom: 1px solid rgba(27, 67, 50, .09);
  font-size: 12px; color: #5C6B62;
}
/* The email is a full table layout at its own width. It is shown at scale
   inside its own scroller rather than squeezed, so what an admin reads is the
   real thing rather than a reflow that only happens here. */
.pst__prevmail { overflow-x: auto; background: #FAF7F0; }
.pst__prevmail img { max-width: 100%; height: auto; }

/* ⚠ THE BASE STATE IS OFF, AND THAT IS WHAT KEEPS DESKTOP UNTOUCHED.
   The arrows and the 1b label are printed into the markup on both widths, the
   same way .wgrid and .cal__week are, because the server cannot know the width.
   Above the phone query they are absent, and `display: contents` on the row
   dissolves the wrapper entirely: the day strip becomes a direct child of
   .cal__daytop again, so the desktop's geometry is byte for byte what it was
   before this wrapper existed rather than what a new flex row would make it. */
.cal__warrow, .cal__weeklabel { display: none; }
.cal__weekrow { display: contents; }

/* ── THE PHONE'S WEEK ARROWS ─────────────────────────────────────────────────
   design_handoff_week_arrows, 1a and 1b.

   ⚠ THE WHOLE BLOCK IS INSIDE THE PHONE QUERY, and it is the exact complement
   of the desktop one used everywhere else in this file. SCOPE IS THE RULING:
   the desktop calendar is untouched in every respect, and a base rule here
   would reach it. That includes the row rules at the bottom, which look like
   general row styling and are not: `.calrow` still renders on the desktop
   month's opened day, where the old tutor rule stands.

   ⚠ THE OLD .cal__arrow DAY-PAGING PAIR IS NOT ON THIS SCREEN and never was:
   it is display:none at every width below the desktop query. That is what makes
   these the replacement rather than a second pair. Ruled, and SPEC records it:
   day movement inside a week is the strip, week movement is the arrows. */
@media (max-width: 760px), (max-height: 499px) {

  /* Shared by 1a and 1b. Paper fill and a hairline: OUTLINED, NEVER FILLED,
     because a filled arrow beside seven day cards reads as an eighth day. */
  .cal__warrow {
    flex-shrink: 0; box-sizing: border-box;
    width: 28px; min-height: 56px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 5px;
    background: var(--off-white); border: 1px solid rgba(27,67,50,.16);
    font-size: 17px; line-height: 1; color: var(--green-btn);
    text-decoration: none;
  }
  /* 1b's pair: wide for the thumb, short because this is the header's third
     row. Same fill and hairline as 1a. */
  .cal__warrow--wide { width: 44px; min-height: 30px; height: 30px; border-radius: 4px; font-size: 16px; }

  /* ── 1a: the row that holds the arrows and the strip ──────────────────
     ⚠ THE STRIP KEEPS ITS OWN PADDING RULE AND LOSES IT HERE, because the row
     now owns the side padding; leaving both would inset the strip twice and
     the arrows would not be flush with the cards they move. */
  .cal__weekrow--strip {
    padding: 14px 18px 0;
    display: flex; align-items: stretch; gap: 4px;
  }
  .cal__weekrow--strip .cal__strip {
    flex: 1; min-width: 0;
    padding: 0; gap: 4px;
  }
  /* Seven cards in what is left after two 28px arrows and eight 4px gaps: about
     39px each at 390. minmax-style, not bare flex:1, so a long numeral cannot
     push one card wider than its neighbours. */
  .cal__weekrow--strip .cal__stripday { flex: 1 1 0; min-width: 0; }

  /* ── 1b: the arrows on a row of their own ─────────────────────────────
     Week and month. It took the session count's line rather than adding one. */
  .cal__weekrow--own {
    padding: 11px 18px 0;
    display: flex; align-items: center; gap: 8px;
  }
  .cal__weeklabel {
    display: block;
    flex: 1; min-width: 0; text-align: center;
    font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* ⚠ THE WEEK TOTAL COMES OFF THE PHONE. Every day card states its own count,
     so the total was one more number in a header that had several. `--count` is
     what makes this reachable: Recent changes one row below carries the same
     .cal__summary class and must stay. */
  .cal__summary--count { display: none; }

  /* ⚠ AND THE TUTOR COLOUR KEY COMES OFF. The left rule on every row already
     carries the tutor and the row names them in words, so the key restated the
     list twice over. Both of the bundle's frames drop it, which is why this is
     not scoped to the week alone. Desktop keeps it. */
  .callegend { display: none; }

  /* ── NAMES TRUNCATE, NEVER WRAP ───────────────────────────────────────
     ⚠ min-width:0 ALONE IS NOT ENOUGH, and the bundle says so from a real
     failure: it lets the box shrink but the text still wraps, which made one
     row 15px taller than its neighbours. nowrap AND hidden AND ellipsis AND
     min-width:0, on the name and on the tutor field both. */
  .calrow__name { min-width: 0; }
  .calrow__right {
    min-width: 0; max-width: 82px;
    /* ⚠ SHRINK 1, NOT 0. The tutor is what yields on a narrow row, before the
       student's name does: the name is what the row is about. */
    flex-shrink: 1;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* ⚠ RULING 1: THE TUTOR COMES OFF ONCE THE PROMPT IS UP, ON MOBILE.
     A row cannot carry a name, a tutor and a status without one of them
     shrinking, and the tutor is the one you can afford: the coloured rule still
     says whose it is, and a past session is read for whether it happened rather
     than for who taught it.

     ⚠ THIS SUPERSEDES calendar-row.ejs's RULE, WHICH DESKTOP KEEPS: there the
     tutor stays and the subject yields, because the tutor is the axis a master
     schedule is scanned along. Two rules, one partial, and the width chooses.
     SPEC.md records the supersession.

     Both shapes: `calrow__right` is the week row's tutor field and
     `calrow__meta` is the day card's line, which carries the tutor as well. */
  .calrow--prompted .calrow__right,
  .calrow--prompted.calrow--big .calrow__meta { display: none; }
}

/* ── WEEKLY EARNINGS ─────────────────────────────────────────────────────────
   design_handoff_admin_tools. Seven columns on the desktop, cards on the phone.

   ⚠ NO RED ANYWHERE, and there is nothing here that would want it: a tutor who
   filed nothing is a quiet row, not an error. The one gold is the paid-versus-
   taught gap, which is information rather than a warning.

   ⚠ NO DASH CHARACTERS. An empty cell carries no placeholder at all, which is
   why the quiet row's cells are literally empty elements. */
.pay { padding: 0; overflow: hidden; }
.pay__row {
  display: grid;
  grid-template-columns: minmax(0, 2fr) repeat(6, minmax(0, 1fr));
  align-items: center; gap: 12px;
  padding: 12px 18px;
  border-top: 1px solid rgba(27, 67, 50, .08);
  font-size: 13px; color: var(--ink);
}
.pay__row--head {
  padding-top: 10px; padding-bottom: 10px;
  border-top: 0;
  font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
}
.pay__row--total {
  border-top: 2px solid rgba(27, 67, 50, .16);
  font-weight: 700; background: rgba(27, 67, 50, .03);
}
.pay__who { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
.pay__name {
  min-width: 0; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* THE TREND, at 9px beside the name. Direction reads from colour as well as
   shape, so it does not depend on telling two small glyphs apart. */
.pay__trend { flex-shrink: 0; font-size: 9px; font-weight: 700; white-space: nowrap; }
/* ⚠ THESE TWO ARE REACHED THROUGH A TEMPLATE EXPRESSION, `pay__trend--<%= dir %>`,
   so a literal search of the templates finds neither and a dead-rule sweep will
   offer to delete both. They are live. */
.pay__trend--up { color: #1B7F4B; }
.pay__trend--down { color: var(--gold-ink); }
.pay__trend--level { color: var(--muted); font-weight: 600; }
.pay__arrow { margin-right: 2px; }
/* The paid-versus-taught gap, under the paid figure. Gold, because it is a
   fact worth noticing rather than a fault. */
.pay__taught {
  display: block; margin-top: 2px;
  font-size: 10.5px; color: var(--gold-ink);
}
.pay__owed { font-weight: 700; }
.pay__quiet { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
.pay__none { grid-column: 1 / -1; font-size: 12.5px; color: var(--faint); }
.pay__band {
  padding: 10px 18px;
  border-top: 1px solid rgba(27, 67, 50, .08);
  background: rgba(27, 67, 50, .03);
  font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
}
/* ⚠ THE STAFF BAND CARRIES NO TEACHING COLUMNS AT ALL. Its own layout rather
   than the seven-column grid with cells left out, so there is no chance of an
   empty teaching cell reading as a zero. */
.pay__staff {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 12px 18px;
  border-top: 1px solid rgba(27, 67, 50, .08);
  font-size: 13px;
}
.pay__role { font-size: 11.5px; color: var(--muted); }
.pay__nothourly { margin-left: auto; font-size: 11.5px; color: var(--muted); }

@media (max-width: 760px), (max-height: 499px) {
  /* CARDS, per the bundle. The header row goes; each cell captions itself from
     the data-label the desktop grid ignores, which is the same mechanism the
     trust rows already use on this page. */
  .pay__row--head { display: none; }
  .pay__row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px 12px; padding: 14px 16px;
  }
  .pay__who { grid-column: 1 / -1; }
  .pay__row > span[data-label]::before {
    content: attr(data-label) " ";
    font-size: 9px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
  }
  /* An empty cell prints its caption and nothing else on a quiet row, which
     would be six labels over nothing. The whole cell goes instead. */
  .pay__row--quiet > span[data-label]:empty { display: none; }
  /* OWED LARGE AND RIGHT-ALIGNED, as drawn. */
  .pay__row > span[data-label="OWED"] {
    grid-column: 2; justify-self: end; text-align: right;
    font-size: 17px; font-weight: 700;
  }
  .pay__row > span[data-label="OWED"]::before { display: block; text-align: right; }
  .pay__staff { padding: 14px 16px; }
}

/* ── THE SENT LOG: A CAPPED SCROLLER WITH PER-ENTRY STATE ────────────────────
   design_handoff_admin_tools.

   ⚠ overscroll-behavior: contain, so reaching the end of this list does not
   hand the gesture to the page underneath it. On a phone that is the difference
   between reading the log and losing your place on the page. */
.ann__log {
  max-height: 196px; overflow-y: auto; overscroll-behavior: contain;
}
/* The state chip. GOLD while live, GREY once expired, and no red anywhere:
   an expired announcement ran its course, which is not a fault. */
.ann__state {
  display: inline-block; margin-right: 7px; padding: 1px 6px;
  border-radius: 3px;
  background: rgba(27, 67, 50, .07);
  font-size: 9px; font-weight: 700; letter-spacing: .07em; color: var(--muted);
  vertical-align: 1px;
}
.ann__state--live { background: var(--gold-tint); color: var(--gold-ink); }

@media (max-width: 760px), (max-height: 499px) {
  .ann__log { max-height: 172px; }
}

/* ── ADMIN TOOLS: THE STATUS CHIPS ───────────────────────────────────────────
   design_handoff_admin_tools. Transcribed from the mock's own chip() helper:
   three tones, each with its own fill, hairline, ink and 6px dot.

   ⚠ NO RED. An empty tray is not a success and a full one is not an error.

   ⚠ TWO EMPTY TONES, AND THE DIFFERENCE IS REAL. `ok` is a clean bill of
   health ("no stranded sessions"); `quiet` is an empty tray ("nothing to
   book"). Green for the first, grey for the second, exactly as drawn. */
.achips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 0 0 var(--rt-s5);
}
.achip {
  min-height: 34px; box-sizing: border-box;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 0 12px; border-radius: 100px;
  font-size: 12px; text-decoration: none;
}
.achip__dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

.achip--ok    { background: rgba(27, 127, 75, .1);  border: 1px solid rgba(27, 127, 75, .28); color: #155F39; }
.achip--quiet { background: rgba(27, 67, 50, .05);  border: 1px solid rgba(27, 67, 50, .16);  color: #5C6B62; }
.achip--gold  { background: rgba(201, 162, 39, .16); border: 1px solid rgba(168, 132, 42, .4); color: #7C6210; }
.achip--ok    .achip__dot { background: #1B7F4B; }
.achip--quiet .achip__dot { background: #A9B1AA; }
.achip--gold  .achip__dot { background: #C9A227; }

/* The count, as a filled pip so the number reads as a quantity rather than as
   part of the sentence. */
.achip__n {
  min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 100px; background: var(--gold-ink);
  font-size: 10px; font-weight: 700; color: #FDFCF9;
}
.achip--gold:hover { background: rgba(201, 162, 39, .3); }

/* AUTO-SEND KEEPS ITS BUTTON, ON THE CHIP. The button's own tone follows the
   state: a quiet green while it is on, and gold while it is off and asking. */
.achip--act { padding-right: 4px; }
.achip__form { margin: 0; display: inline-flex; }
.achip__btn {
  min-height: 26px; padding: 0 10px;
  border: 0; border-radius: 100px;
  font-family: var(--sans);
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
  cursor: pointer;
}
.achip__btn--on  { background: rgba(27, 127, 75, .2); color: #155F39; }
.achip__btn--off { background: var(--gold); color: var(--green-deep); }
.anchor { display: block; height: 0; }
.trust__seeall { margin-left: auto; font-size: 12px; color: var(--green-btn); white-space: nowrap; }

/* ── ADMIN TOOLS: THE TOOL TILES AT 60px ─────────────────────────────────────
   Down from 200. FOUR ACROSS, and the whole card is already the link, so the
   only thing that comes off is the separate `Open` control: a second target
   inside a clickable card is two targets for one destination. */
.atiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.atile {
  align-items: center; gap: 12px;
  min-height: 60px; padding: 0 14px;
}
.atile__icon { width: 30px; height: 30px; font-size: 15px; }
/* ⚠ nowrap AT ANY WIDTH. At 1680 each tile is about 398px against a ~105px
   title so it fits comfortably; the rule is here so it cannot wrap at a width
   nobody tested. THE BETA PILL YIELDS FIRST, which is what min-width:0 on the
   title and flex-shrink on the pill buy. */
/* ⚠ NO TRUNCATION ON A TILE, ON EITHER SURFACE. This is the only rule in the
   file that clipped one, and it clipped ALL FOUR: nowrap plus overflow hidden
   plus an ellipsis, which is what rendered "Practice review …" on a phone.
   A tile that cannot show its own name is broken, and a taller tile is not a
   problem. If a title does not fit on one line it WRAPS and the tile grows.
   Written on the base rule rather than overridden per surface, so the next long
   title cannot bring it back on the one that was missed. */
.atile__title { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; }
/* Nothing may impose a height that clips them either. */
.atile, .atile__body, .atile__title { max-height: none; }
/* ⚠ AND WHEN THE TITLE WRAPS, THE BADGE STARTS THE LINE. Above, the badge is
   inline with a left margin, which is right while it sits beside the words. On a
   phone the title wraps and it falls to a line of its own, where that margin was
   still applying: measured at 390, the title's first glyph sat at x77 and the
   badge at x85, which reads as a badge floating loose under the heading rather
   than belonging to it.

   A block of its own width, with the margin off, puts its left edge on the
   title's text edge: flush under the P of "Practice".

   ⚠ IN ITS OWN MEDIA QUERY, AND THAT IS THE POINT OF THE EXTRA LINES. Written
   as a base rule it read correctly and was wrong: it would have broken the
   badge onto its own line on the DESKTOP card too, where the title does not
   wrap and the badge belongs beside the words. The rules around it here are
   deliberately base rules and say so, which is exactly how this one got written
   as one. */
@media (max-width: 760px), (max-height: 499px) {
  .atile__beta { display: block; width: fit-content; margin-left: 0; margin-top: 5px; }
}
.atile__beta { flex-shrink: 1; min-width: 0; overflow: hidden; }
/* The description and the Open link come off at this height. The title names
   the destination and the tile is the link. */
.atile__desc, .atile__open { display: none; }

@media (max-width: 1100px) {
  .atiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .atiles { grid-template-columns: minmax(0, 1fr); }
  /* 60px is already above the 44px floor, so the tap target needs nothing. */
}

/* ── ADMIN ROSTERS: TWO COLUMNS, ONE MENU PER ROW ────────────────────────────
   design_handoff_admin_tools. Tutors is 6 and complete; Students is 33+ and
   paginated. They are not peers, so they stack full width rather than sitting
   side by side: at equal width the long list is forced to the short one's
   height, which is what capped Students at six rows.

   ⚠ THE CARD CARRYING THE POPOVER MUST BE overflow: visible. `overflow: hidden`
   for the rounded corners clipped the student list at the card's bottom edge,
   which is the whole point of the popover unreachable. The rounding moves onto
   the first and last rows instead. */
.rlist { overflow: visible; }
/* ⚠ AND THE CARD, WHICH IS WHAT THE COMMENT ABOVE ACTUALLY NAMES. The rule that
   was written for it targets `.rlist` instead, so the LIST stopped clipping and
   the CARD around it went on doing exactly what the note says must not happen.
   Measured on a phone: the roster card computes overflow:hidden on both axes and
   the open popover's bottom edge sat 3px inside it, cut off at the card's
   boundary. The rounding is already handled on the first and last rows, so
   there is nothing left for the card's own clip to do. */
.page--admin section.card--pad { overflow: visible; }
.roster__toggle { border-radius: 6px 6px 0 0; }

/* THREE COLUMNS FOR TUTORS, TWO FOR STUDENTS. Set on the list, so the rows
   themselves stay the same partial at every width. */
#tutors-list .roster__row { }
.rlist--cols3, .rlist--cols2 { display: grid; gap: 0; }
.rlist--cols3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.rlist--cols2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The filter and the empty line span the grid rather than becoming cells. */
.rlist--cols3 > .rlist__filter, .rlist--cols2 > .rlist__filter,
.rlist--cols3 > .rlist__none,   .rlist--cols2 > .rlist__none,
.rlist--cols3 > .empty,         .rlist--cols2 > .empty,
.rlist--cols3 > .rlist__pager,  .rlist--cols2 > .rlist__pager { grid-column: 1 / -1; }

/* ⚠ EVERY TUTOR CELL IS EXACTLY TWO LINES AND 64px TALL. A grid row is as tall
   as its tallest cell, so a single flagged tutor used to stretch its whole row.
   Both flags are pills on the name line and the meta is one line, so the cell
   cannot grow; 64px with the content centred is what makes the column read as
   a column rather than as a ragged stack. */
.rlist--cols3 .roster__row, .rlist--cols2 .roster__row {
  min-height: 64px; align-items: center;
}
.roster__name { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; min-width: 0; }
/* The NAME yields, never the flags: a truncated name is still identifiable and
   a truncated flag is not a flag. */
/* ⚠ THE NAME YIELDS AND THE FLAGS DO NOT. Targeted through its own span: as a
   bare text node the name is an anonymous flex item and no selector reaches it,
   which is how this rule used to land on the first PILL instead. */
/* ⚠ NO ELLIPSIS ON A PERSON'S NAME. Ruled: full names render, always. This
   carried overflow:hidden with an ellipsis, which is how "Caroline Pousson"
   became "Caroline Pous…" in a narrow column. It keeps min-width:0 so it still
   yields space to the flags beside it, but it yields by WRAPPING now, and the
   row grows rather than hiding half of somebody's name. */
.roster__nametext { min-width: 0; overflow-wrap: anywhere; }
.norate, .badge, .spop { flex-shrink: 0; }
/* ⚠ AND NO LEFT MARGIN ON THIS LINE, for the reason set out above .spop__pill.
   A margin separates two items sharing a line AND indents one that starts a
   line; the row's own gap does only the first, and these can wrap.

   ⚠ SCOPED HERE RATHER THAN REMOVED FROM .norate ITSELF, which is the one place
   this fix is an override instead of a deletion. .norate is not only a roster
   flag: admin.ejs and partials/charge-rate.ejs both render it inside a
   paragraph of running text, where it is the first thing on the line and its
   6px is holding it off the paragraph edge rather than off a neighbour. Taking
   the margin off the class would move copy on two surfaces nobody reported, so
   it is neutralised where it is a flex item and left alone where it is not. */
.roster__name .norate { margin-left: 0; }
.roster__meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── THE ⋯ MENU ──────────────────────────────────────────────────────────────
   One control per row instead of a wall of blue. A native <details>, so it
   opens without script and the panel is a real disclosure. */
.rmenu { position: relative; }
.rmenu__btn {
  list-style: none; cursor: pointer;
  min-width: 44px; min-height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 6px;
  font-size: 18px; line-height: 1; color: var(--muted);
}
.rmenu__btn::-webkit-details-marker { display: none; }
.rmenu__btn:hover, .rmenu[open] .rmenu__btn { background: rgba(27, 67, 50, .07); color: var(--ink); }
.rmenu__panel {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 40;
  min-width: 170px;
  display: flex; flex-direction: column; align-items: stretch;
  padding: 5px;
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .16); border-radius: 6px;
  box-shadow: 0 10px 26px rgba(20, 55, 42, .14);
}
/* The controls inside keep their own classes and their own hooks; only their
   shape inside the panel is set here. */
.rmenu__panel > * {
  min-height: 44px; display: flex; align-items: center;
  padding: 0 10px; border-radius: 4px;
  text-align: left; font-size: 13px; background: none; border: 0;
}
.rmenu__panel > *:hover { background: rgba(27, 67, 50, .07); }

/* ── THE SETTINGS STRIP ──────────────────────────────────────────────────────
   design_handoff_admin_tools, transcribed from the mock: a header row, then
   four cells in a flex row divided by hairlines, each with a name, a note
   saying what the setting is for, and its value or its switch on the right. */
.sstrip {
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 6px;
  overflow: hidden;
  margin: 0 0 var(--rt-s5);
}
.sstrip__head {
  padding: 14px 20px;
  border-bottom: 1px solid rgba(27, 67, 50, .09);
  font-size: 10.5px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink);
}
.sstrip__row { display: flex; flex-wrap: wrap; }
.sstrip__cell {
  flex: 1; min-width: 0; box-sizing: border-box;
  padding: 15px 20px;
  border-right: 1px solid rgba(27, 67, 50, .09);
  display: flex; align-items: center; gap: 14px;
  margin: 0; text-decoration: none;
}
.sstrip__cell:last-child { border-right: 0; }
.sstrip__cell:hover { background: rgba(27, 67, 50, .03); }
.sstrip__text { flex: 1; min-width: 0; }
/* ⚠ NO BUTTON CHROME. The notifications label is a button because it is the
   only door into that dialog, and a bare <button> renders with a border, a
   grey ground and a radius: on the row that reads as a pill wrapped around the
   label, which is not what the mock draws. The control stays and the chrome
   goes. */
.sstrip__opener {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.sstrip__name {
  display: block; font-size: 13px; font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sstrip__note {
  display: block; margin-top: 2px; font-size: 11px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The rate is the one value drawn in the serif, because it is a number rather
   than a phrase. */
.sstrip__money {
  flex-shrink: 0; font-family: var(--serif); font-size: 19px; color: var(--green-deep);
}
.sstrip__value { flex-shrink: 0; font-size: 13px; color: var(--ink); }
.sstrip__chev { flex-shrink: 0; font-size: 13px; color: var(--muted); }

/* A REAL SWITCH, shaped from a submit button so it works with no script. */
.sstrip__sw {
  flex-shrink: 0; box-sizing: border-box;
  width: 42px; height: 24px; padding: 0 3px;
  display: inline-flex; align-items: center; justify-content: flex-start;
  border: 1px solid rgba(27, 67, 50, .2); border-radius: 100px;
  background: rgba(27, 67, 50, .16);
  cursor: pointer;
}
.sstrip__sw--on { background: var(--green-btn); border-color: var(--green-btn); justify-content: flex-end; }
.sstrip__knob { width: 18px; height: 18px; border-radius: 50%; background: #FDFCF9; }

@media (max-width: 1100px) {
  .rlist--cols3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sstrip__cell { flex: 1 1 50%; min-width: 240px; }
}
@media (max-width: 760px), (max-height: 499px) {
  .rlist--cols3, .rlist--cols2 { grid-template-columns: minmax(0, 1fr); }
  .sstrip__cell {
    flex: 1 1 100%; min-width: 0;
    border-right: 0; border-top: 1px solid rgba(27, 67, 50, .09);
    min-height: 56px;
  }
  .sstrip__row > .sstrip__cell:first-child { border-top: 0; }
  /* The popover is a sheet-width card on a phone rather than a dropdown that
     would run off the edge of a 390px screen. */
  .spop__card { left: 0; right: 0; width: auto; }
}

/* ── THE TWO CREATION MODALS ─────────────────────────────────────────────────
   design_handoff_admin_tools.

   ⚠ WITHOUT SCRIPT THE FORM IS THE INLINE BLOCK IT ALWAYS WAS. The trigger and
   the modal chrome only exist once public/admin-modal.js has said the browser
   can do this, which it does by stamping the root. Everything below is scoped
   to that stamp, so an un-upgraded page renders exactly what it rendered before
   the dialog element was introduced. */
.amodal__bar { display: none; }
.amodal { display: block; border: 0; padding: 0; background: none; max-width: none; }
.amodal__head, .amodal__close { display: none; }

[data-amodal-ready] .amodal__bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
[data-amodal-ready] .amodal__head { display: flex; }
[data-amodal-ready] .amodal__close { display: block; }

[data-amodal-ready] .amodal {
  /* ⚠ CENTRED AND SIZED TO ITS CONTENT, NEVER PINNED TOP AND BOTTOM. Pinning
     with inset:0 made a 378px form 1551px tall and put the button a thousand
     pixels below the last field. translate centring lets the box be as tall as
     it needs and no taller; the cap is what stops a long form running off. */
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  /* ⚠ margin: 0 IS THE CENTRING FIX, AND ITS ABSENCE IS WHY DIALOGS SAT OFF
     CENTRE. A <dialog> carries `margin: auto` from the user agent, which centres
     it in the viewport on its own. Combined with top/left 50% and a translate,
     the box was centred TWICE and the two disagreed: the auto margins resolve
     against the available space, then the transform moved it again by half its
     own size. Zeroing the margin leaves exactly one centring mechanism.
     Fixed here, at the shared container, so every dialog on the page inherits
     it rather than each being nudged into place. */
  margin: 0;
  width: 1080px; max-width: calc(100vw - 48px);
  max-height: calc(100% - 68px);
  padding: 0; border: 0; border-radius: 8px;
  background: var(--off-white);
  box-shadow: 0 24px 60px rgba(20, 55, 42, .28);
  overflow: hidden;
}
[data-amodal-ready] .amodal:not([open]) { display: none; }
[data-amodal-ready] .amodal::backdrop { background: rgba(20, 55, 42, .5); }
/* ⚠ CAPPED BY THE DIALOG, NOT BY ITS OWN IDEA OF THE VIEWPORT. This restated
   the cap as calc(100vh - 68px) while the dialog above used
   calc(100% - 68px), so the two could disagree, and on a phone they did: vh is
   the LARGE viewport with the address bar hidden, so the panel was allowed to be
   taller than the box clipping it.
 *
 * ⚠ AND max-height: 100% IS NOT THE FIX EITHER, which a browser check caught
 * before this shipped: the dialog's HEIGHT is auto, only its max-height is set,
 * and a percentage max-height against an auto-height parent does not resolve.
 * The panel came out unbounded, the body never had to shrink, and the scroller
 * still did not scroll.
 *
 * So the DIALOG is the flex container and the panel is its one flexible child.
 * That bounds the panel by the dialog's own max-height without a percentage
 * resolving against anything, at both widths, and there is one place to change
 * the cap. min-height: 0 for the same reason it is on the body below: without it
 * a flex item refuses to shrink under its content. */
[data-amodal-ready] .amodal { display: flex; flex-direction: column; }
[data-amodal-ready] .amodal:not([open]) { display: none; }
[data-amodal-ready] .amodal__panel {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0;
}
[data-amodal-ready] .amodal__head {
  flex-shrink: 0;
  align-items: center; justify-content: space-between; gap: 12px;
  padding: 18px 24px;
  border-bottom: 1px solid rgba(27, 67, 50, .12);
}
.amodal__title { margin: 0; font-family: var(--serif); font-size: 20px; color: var(--ink); }
.amodal__close {
  min-width: 44px; min-height: 44px;
  border: 0; background: none; font-size: 22px; line-height: 1;
  color: var(--muted); cursor: pointer;
}
/* ⚠ min-height: 0 IS WHAT MAKES overflow-y: auto MEAN ANYTHING HERE, and
   without it this scroller did not scroll at all.
 *
 * A flex item's min-height defaults to `auto`, which in a column flex container
 * refuses to shrink the item below its CONTENT height. So this box stayed as
 * tall as the whole form however short the panel was: it never became a
 * scrollport, the panel overflowed, and .amodal's overflow: hidden cut the
 * overflow off. On a phone that put the ADD STUDENT button below the sheet's
 * lower edge with no way to reach it, because the one element that was supposed
 * to scroll had no overflow of its own to scroll.
 *
 * flex: 1 1 auto is stated rather than left to the default so the body is
 * explicitly the part that gives, and the head above it already refuses to
 * (flex-shrink: 0). */
[data-amodal-ready] .amodal__body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* ⚠ THE CHAIN STOPS HERE. Reaching either end of this scroller used to pass
     the rest of the wheel up to the viewport, so the page slid underneath a
     modal that stayed put. Measured at 1440x820 with the page at 600: driving
     the body to its bottom and wheeling again moved the PAGE to 1100 while the
     body stayed at 305. `contain` keeps the overscroll inside the box without
     preventing the scroll itself, so a wheel, a trackpad, a touch drag and the
     arrow keys all still scroll the modal exactly as before. */
  overscroll-behavior: contain;
  padding: 20px 24px 24px;
}
/* ⚠ AND THE DIALOG ITSELF, because the body is not the whole panel. The head is
   outside the scroller, and five of the seven dialogs have a body SHORT enough
   not to scroll at all -- measured, their scrollHeight equals their clientHeight
   -- so a wheel over them met no scroller of ours and went straight to the
   viewport. `.amodal` is already `overflow: hidden`, which makes it a scroll
   container, so it can carry this and absorb those. */
[data-amodal-ready] .amodal { overscroll-behavior: contain; }
/* ⚠ AND THE PAGE IS LOCKED, because overscroll-behavior CANNOT reach the case
   that matters most: a wheel over the BACKDROP is not over any element of ours
   at all, and it scrolled the page 600 -> 1200 -> 1800 in the measurement.
   Only taking the scroll away from the document answers that one.
 *
 * ⚠ AND IT IS A NO-OP ON A PHONE, BY MEASUREMENT RATHER THAN BY SCOPING. The
 * shell is 100svh and `.page` scrolls inside it, so at 390 the document's
 * scrollHeight EQUALS its clientHeight -- 844 and 844 -- and there is no
 * document scroll to take away. That is also why the fault is desktop-only:
 * the chain reaches a root that cannot scroll and stops. No media query is
 * used, because a query would be a claim about where the fault is and this is
 * the fact itself.
 *
 * public/admin-modal.js adds and removes the class, and restores the scroll
 * position on close. */
html.amodal-lock { overflow: hidden; }
/* ⚠ TWO REAL COLUMNS, REPLACING A MULTICOL. design_handoff_edit_student Part 4.
   This was `column-count: 2` on the form, and the comment here used to say "the
   form's own children flow into them; nothing in the form was regrouped" as
   though that were a design. It was the bug: the break fell wherever the content
   happened to end, so COMPANY STANDARD rendered in the right column while the
   rate field it describes sat in the left, +Parent was stranded three fields
   from PARENT NAME, and two subheadings appeared below the fields they labelled.

   A GRID, SO A GROUP IS WHOLE OR ABSENT. The columns are real elements now
   (.aform__col), placed rather than flowed, and no amount of content in one can
   push anything into the other.

   ⚠ THE ERROR SLOT AND THE SUBMIT SPAN BOTH, by grid-column. public/admin-
   actions.js inserts .formerr as the form's FIRST child on a refusal, so it is a
   grid item and is placed here rather than by the column-span it used to carry;
   without this it would sit in one half of the body. */
[data-amodal-ready] .amodal__body--2col > form {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0 34px;
  align-items: start;
}
[data-amodal-ready] .amodal__body--2col > form > .submit {
  grid-column: 1 / -1; margin-top: 18px;
}
[data-amodal-ready] .amodal__body--2col > form > .formerr { grid-column: 1 / -1; }
/* ⚠ min-width: 0 OR THE COLUMN REFUSES TO SHRINK. A grid track is auto-sized to
   its content's minimum by default, and this column holds inputs and a typeahead
   panel; without this a long value widens the track past its half and the two
   stop being equal. */
[data-amodal-ready] .amodal__body--2col > form > .aform__col { min-width: 0; }
/* ⚠ THE break-inside AND break-after RULES WENT WITH THE MULTICOL. They existed
   to stop a label being split from its field across a column break, and a grid
   has no column break to be split across. Keeping them would be a rule about a
   layout that is no longer here. */

/* THE REFUSAL, INSIDE THE FORM IT IS ABOUT. A modal is in the top layer, so a
   toast explaining a refusal paints behind the sheet the admin is reading; this
   is the message they can actually see. Column-span so it is not caught in one
   half of the two-column body. */
.formerr {
  /* ⚠ column-span: all WENT WITH THE MULTICOL. The slot is placed by the grid
     rule above, where the layout that needs it lives, rather than carrying a
     property for a layout this component no longer sits in. It is also used
     outside that grid, where a spanning declaration meant nothing anyway. */
  margin: 0 0 14px; padding: 10px 12px;
  border-left: 3px solid var(--danger, #9B2C2C);
  background: rgba(155, 44, 44, .07);
  border-radius: 3px;
  font-size: 13px; line-height: 1.45; color: var(--ink);
}
.formerr[hidden] { display: none; }

.amodal__open {
  min-height: 40px; padding: 0 16px;
  border: 0; border-radius: 4px;
  background: var(--green-btn); font-family: var(--sans);
  font-size: 12px; font-weight: 700; letter-spacing: .05em; color: #FDFCF9;
  cursor: pointer;
}

/* ⚠ THE RATE AND SCHOOL-YEAR MODALS STAY 620px. Two fields do not want 1080. */
[data-amodal-ready] .amodal--narrow { width: 620px; }
/* A card inside a modal has already been given its padding by the modal body,
   and its border would be a second box drawn inside the first. */
.card--flush { border: 0; box-shadow: none; padding: 0; margin: 0; background: none; }
/* The strip cells are buttons now, so they take a button's reset. */
button.sstrip__cell {
  font: inherit; text-align: left; background: none; border: 0;
  border-right: 1px solid rgba(27, 67, 50, .09); cursor: pointer;
}
button.sstrip__cell:last-child { border-right: 0; }

/* THE ROSTERS STACK. See the note in admin.ejs: they are not peers. */
.cols--stack { display: block; }
.cols--stack > .card + .card { margin-top: var(--rt-s5); }

@media (max-width: 760px), (max-height: 499px) {
  /* ⚠ SHEETS, NOT MODALS, on a phone. Full width, anchored to the bottom, and
     scrolling inside: a centred 1080px box has nothing to offer at 390px. */
  /* ⚠ dvh, NOT vh AND NOT %. Both of those are the LARGE viewport on mobile
     Safari, the one measured with the address bar collapsed, so a sheet
     anchored to bottom:0 had its lower edge underneath the browser chrome and
     the submit button with it. The overlay in the survey already carries this
     rule and the same reason; 100dvh is the live height, so 88dvh is 88% of
     what is actually on screen. */
  [data-amodal-ready] .amodal {
    top: auto; bottom: 0; left: 0;
    transform: none;
    width: 100%; max-width: 100%;
    max-height: 88dvh;
    border-radius: 10px 10px 0 0;
  }
  /* THE PANEL IS CAPPED BY THE DIALOG (max-height: 100%, set above) and does
     not restate a viewport of its own, which is what let the two disagree. */
  /* ⚠ THE HOME INDICATOR. The sheet sits ON the bottom edge, so its last
     control needs the safe area under it or the thumb lands on the system bar.
     --sab is published by public/safe-area.js; env() is refused in this file. */
  [data-amodal-ready] .amodal__body { padding-bottom: calc(24px + var(--sab)); }
  /* ONE COLUMN. Two columns at 390px is two columns of about 170px.
     The grid collapses to a single track, so the two .aform__col elements stack
     in source order: the signup run first, what the family gets after it. */
  [data-amodal-ready] .amodal__body--2col > form { grid-template-columns: 1fr; }
  /* ⚠ EVERY INPUT IS 16px, or iOS zooms the page the moment one is focused. */
  [data-amodal-ready] .amodal__body .in,
  [data-amodal-ready] .amodal__body .sel,
  [data-amodal-ready] .amodal__body textarea { font-size: 16px; }
  .amodal__open { min-height: 44px; }
}

/* ── 2a, THE 500 ─────────────────────────────────────────────────────────────
   design_handoff_error_pages. FULL BLEED, NEVER A CARD: a white card on cream
   alone in a wide window is a dialogue box with nothing behind it, and the old
   page read as one that had failed to finish loading rather than as a
   deliberate stop.

   ⚠ NO RED, NO ICON, NO WARNING TRIANGLE. The gold rule is the mark, and it is
   the same 3px the report email uses above a signature. */
body.errpage { background: var(--green-deep); min-height: 100vh; margin: 0; }
.errpage__main {
  display: flex; align-items: center;
  padding: 72px 26px 96px;
}
.errpage__col { width: 100%; max-width: 560px; margin: 0 auto; }
.errpage__rule {
  display: block; width: 44px; height: 3px;
  background: var(--gold); margin-bottom: 26px;
}
.errpage__h1 {
  margin: 0; font-family: var(--serif); font-weight: 400;
  font-size: 40px; line-height: 1.15; color: #FDFCF9;
}
.errpage__lead {
  margin: 18px 0 0; font-size: 16px; line-height: 1.65; color: #C8D3CD;
}
/* The saved-work clause is the brightest thing in the paragraph, because it is
   the one question a tutor who lost a page mid-survey is asking. */
.errpage__lead b { color: #FDFCF9; font-weight: 600; }
.errpage__second {
  margin: 14px 0 0; font-size: 16px; line-height: 1.65;
  color: rgba(255, 255, 255, .62);
}
/* ONE BUTTON, GOLD, because green on green is invisible. Same treatment as the
   pre-session email's Join button. */
.errpage__go {
  margin-top: 30px;
  min-height: 48px; box-sizing: border-box;
  display: inline-flex; align-items: center; padding: 0 26px;
  background: var(--gold-light); border-radius: 4px;
  font-size: 13px; font-weight: 700; letter-spacing: .06em;
  color: var(--green-deep); text-decoration: none;
}
.errpage__foot {
  margin-top: 40px; padding-top: 22px;
  border-top: 1px solid rgba(255, 255, 255, .16);
}
.errpage__eyebrow {
  font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-light);
}
.errpage__note {
  margin: 9px 0 0; font-size: 13.5px; line-height: 1.65;
  color: rgba(255, 255, 255, .62);
}
.errpage__tel { color: #FDFCF9; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.4); }

/* ── 1b, THE 404 ─────────────────────────────────────────────────────────────
   ⚠ CREAM, NOT DARK. A 404 is not a 500 and must not read like one: nothing
   broke, so there is nothing to retry and nothing to report. */
.nf { max-width: 560px; }
.nf__h1 {
  margin: 0; font-family: var(--serif); font-weight: 400;
  font-size: 30px; line-height: 1.2; color: var(--ink);
}
.nf__lead { margin: 14px 0 0; font-size: 15px; line-height: 1.7; color: var(--body); }
.nf__card {
  margin-top: 26px;
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 6px;
  overflow: hidden;
}
.nf__label {
  padding: 12px 18px;
  border-bottom: 1px solid rgba(27, 67, 50, .1);
  font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
}
.nf__row {
  min-height: 56px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 18px;
  border-top: 1px solid rgba(27, 67, 50, .07);
  font-size: 14.5px; color: var(--ink); text-decoration: none;
}
.nf__row:first-of-type { border-top: 0; }
.nf__row:hover { background: rgba(27, 67, 50, .04); }
.nf__chev { color: var(--muted); }

@media (max-width: 760px), (max-height: 499px) {
  .errpage__main { padding: 40px 20px 64px; }
  .errpage__h1 { font-size: 30px; }
  .errpage__go { min-height: 48px; width: 100%; justify-content: center; }
}

@media (max-width: 760px), (max-height: 499px) {
  .ann__cols { grid-template-columns: minmax(0, 1fr); }
  .ann__cols > .ann__compose {
    border-right: 0;
    border-bottom: 1px solid rgba(27, 67, 50, .09);
  }
}

/* ── ROLODEX 9a: THE NEW COMPONENTS ──────────────────────────────────────────
   design_handoff_rolodex. All portal colours, no new tokens. */

/* ⚠ THE THIRD AVATAR VARIANT. Solid green is a tutor, gold tinted is a tutor in
   training, solid gold is an admin: it inverts the tutor disc, and three states
   this distinct need no legend. */
.rolodex__avatar--admin {
  background: var(--rt-gold);
  color: var(--rt-green-deep);
  border: 0;
}

/* ⚠ AN EMPTY CELL IS EMPTY, and holds a 34px box so the row stays level with a
   tutor's. NO DASH, no "n/a", no placeholder text: dash characters are banned
   from every rendered surface and the crawler test fails the whole page on one.
   Same treatment as the earnings table's empty week. */
.rolodex__blank { display: block; min-height: 34px; }

/* An admin's earnings cell carries one word rather than a figure. Quiet, and
   not the italic "No rate set" variant: that one means an unfinished record,
   and this is a complete one about somebody who does not teach. */
.rolodex__nothourly {
  display: block; min-height: 34px;
  font-family: var(--rt-font-ui); font-size: 12px; color: var(--rt-secondary);
}

/* The gold asterisk on the AVAILABILITY head, which says once what no row
   repeats. */
.rolodex__zoneflag { margin-left: 3px; color: var(--rt-gold-bright); }

/* ⚠ THE FOOTNOTE SITS UNDER THE TABLE AND OUTSIDE THE SCROLLER, so it can never
   be scrolled sideways away from the column it explains. Bold, because it
   changes how every figure above it is read. */
.rolodex__zonefoot {
  margin: 12px 0 0;
  font-family: var(--rt-font-ui); font-size: 12px; font-weight: 600;
  color: var(--rt-secondary);
}

/* ── THE NO-ZOOM BLOCK AS ONE PANEL ──────────────────────────────────────────
   ⚠ TWO LINES DELIBERATELY. The fact and the action are short and always fit
   together; the sentence is 179px and never does. Letting all three wrap puts
   "Ask" on line two for a long first name and line one for a short one, so the
   block would change shape with the name in it. */
.rolodex__zoompanel {
  display: block; margin-top: 8px;
  padding: 7px 10px;
  background: rgba(201, 162, 39, .14);
  border-left: 2px solid var(--rt-gold);
  border-radius: 0 3px 3px 0;
}
.rolodex__zoomtop {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.rolodex__zoompanel .rolodex__hoursnote { display: block; margin-top: 2px; }

/* ══ ADMIN TOOLS AT 390: WHAT THE REARRANGEMENT ACTUALLY HAS TO UNDO ══════════
 *
 * ⚠ WHY THIS BLOCK EXISTS, AND IT IS THE WHOLE LESSON OF THIS FAILURE.
 *
 * The rearrangement added markup and NEW DESKTOP RULES. It never reconciled
 * them with the `@media (max-width:760px)` block written for the OLD page, which
 * restyles these exact sections. On a phone the old rules kept winning and the
 * page rendered as the shape it had before: rows stacked into centred columns,
 * tiles full width, the settings cards at full height under the new strip.
 *
 * ⚠ AND THE CASCADE IS PER PROPERTY, WHICH IS HOW IT PASSED EVERY TEXT CHECK.
 * `.roster__name` at 2058 sets `flex-direction: column`. My later rule set
 * `display`, `align-items`, `gap` and `flex-wrap` and NOT `flex-direction`, so
 * source order never came into it: the property I needed to beat was one I had
 * not written. A stylesheet grep for the class finds my rule and says it is
 * there. Only a rendered pixel says which declaration won.
 *
 * Everything below is scoped to the ADMIN ROSTERS (.rlist) rather than to
 * .roster__row, which the stranded and email-failure panels also use with their
 * own action buttons. */
@media (max-width: 760px) {

  /* ── THE ROWS GO BACK TO BEING ROWS ────────────────────────────────────
     The old stacking existed because three loose links -- Edit, Reset
     password, Deactivate -- could not fit beside the name and ran 69px off
     the right edge. That is gone: the actions are one 44px overflow menu, so
     the reason the stack existed has been removed and the stack with it. */
  .rlist .roster__row {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    min-height: 64px;
    text-align: left;
  }
  /* ⚠ flex-direction IS THE DECLARATION THAT MATTERS. Setting display and
     align-items alone left the name a column and the pills under it. */
  .rlist .roster__name {
    flex-direction: row;
    align-items: center;
    flex-wrap: nowrap;
    gap: 6px;
    min-width: 0;
  }
  .rlist .roster__main { flex: 1; min-width: 0; }
  .rlist .roster__meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* The actions are one button now, so they stop being a full-width wrapping
     row with a negative margin pulling the line above it. */
  .rlist .roster__acts {
    display: flex; align-items: center;
    width: auto; margin-top: 0; gap: 0;
    flex-shrink: 0;
  }

  /* ── THE TILES ARE TWO ACROSS, NOT ONE ─────────────────────────────────
     Four full-width 60px cards is 288px of chrome before the first thing an
     admin came for. Two across is 144px and still clears the tap target. */
  .atiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .atile { padding: 0 10px; gap: 8px; }
  .atile__icon { width: 26px; height: 26px; font-size: 13px; }
  .atile__title { font-size: 13px; }

  /* ── THE COMPOSER IS A CARD, NOT A COLUMN OF ADVICE ────────────────────
     Every explanatory line under the fields is true and none of it is worth
     946px on a phone. The counters, the push note and the window note fold
     into one quiet line; the send button and the fields stay exactly as they
     are, because those are the controls. */
  /* ⚠ THE ADVICE FOLDS, THE CONTROLS DO NOT. Every explanatory line here is
     true and none of it is worth the height on a phone: the send window note,
     the "what a push shows" note and the roster's deactivation essay are six
     lines each. The fields, the button and the log are untouched, because
     those are what the section is for. */
  .ann__timingnote { display: none; }
  .ann__foot { display: none; }
  .roster__note { display: none; }
  .ann__count { font-size: 10.5px; }

  /* ⚠ THE PILL DROPS ITS WORD AND KEEPS ITS NUMBER. "5 STUDENTS" is 55px the
     name needs at 390, and the popover it opens states the word in its own
     header, so nothing is lost. The name reads and the flag still flags. */
  /* ⚠ THE WORD STAYS. This hid it at 390 to buy width for the name, and the
     result was a bare "9" that says nothing: a number with no noun is not a
     label. Ruled: the phone carries "N STUDENTS" like the desktop, and the row
     makes room for it rather than the chip dropping the word. The room comes
     from the name line wrapping, which it now does at every width. */
  .rlist .spop__word { display: inline; }
  .rlist .spop__pill { padding: 3px 7px; }
  /* A floor under the name, so it is the LAST thing to give rather than the
     first: the flags have already shrunk by the time this bites. */
  .rlist .roster__nametext { flex: 1 1 auto; min-width: 84px; }
  /* ONE HEADING PER SECTION. The serif title and the gold band said the same
     word one line apart; the band carries the count and the button, so it is
     the one that stays. */
  /* ⚠ SCOPED TO MATCH THE RULE IT HAS TO BEAT. The base is
     `[data-amodal-ready] .amodal__bar { justify-content: space-between }`,
     specificity (0,2,0); a bare `.amodal__bar` here is (0,1,0) and loses, which
     left the button at flex-start with one child. It read as a button floating
     above the section instead of sitting on its band. */
  [data-amodal-ready] .amodal__bar {
    justify-content: flex-end;
    padding-bottom: 6px;
  }
  .amodal__bar .card__title { display: none; }

  /* The section blurb goes with the other advice: the band under it already
     names the section and carries its count. */
  .ann__hint { display: none; }

  /* The section headings stop being 22px serif titles above every card: the
     rail already names the section you are in. */
  .page--admin .amodal__bar .card__title { font-size: 17px; }
}

/* ── TRUST, AS STREAK CARDS ──────────────────────────────────────────────────
   design_handoff_admin_tools. Only the pairs with a streak going, four across,
   longest first. The table it replaces was 63 rows over seven pages with about
   59 of them reading 0/10.

   ⚠ PAGE-OWNED NAMES. `tcard` rather than another `trust__` variant, so nothing
   in the shared admin-family mobile block can reach these: that block styles
   `.trust__row` for five other templates and is why the old rows stacked. */
.tcards {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;
  padding: 4px 18px 18px;
}
.tcard {
  display: flex; flex-direction: column; gap: 7px;
  padding: 13px 14px;
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14); border-radius: 6px;
}
/* Eligible is the one state worth marking, and gold is how this page marks
   everything that wants a decision. */
.tcard--eligible { border-color: rgba(168, 132, 42, .45); background: rgba(201, 162, 39, .07); }
.tcard__who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tcard__tutor {
  font-size: 13px; font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tcard__student {
  font-size: 11.5px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The number is the thing the eye lands on, which is the whole reason the wall
   of zeros was worth removing. */
.tcard__count { font-family: var(--serif); font-size: 22px; line-height: 1; color: var(--green-deep); }
.tcard__of { font-family: var(--sans); font-size: 11px; color: var(--muted); }
.tcard__bar {
  display: block; height: 4px; border-radius: 100px;
  background: rgba(27, 67, 50, .1); overflow: hidden;
}
.tcard__bar > span { display: block; height: 100%; background: var(--gold); }
.tcard__bar--full > span { background: var(--green-btn); }
.tcard__act { display: flex; align-items: center; min-height: 30px; }
.tcard__togo { font-size: 11px; color: var(--muted); }
.trust__empty { margin: 0; padding: 4px 18px 18px; font-size: 12.5px; color: var(--faint); }

@media (max-width: 1100px) { .tcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px), (max-height: 499px) {
  /* Two across on a phone, not one: these are small cards and one per row is a
     column of mostly whitespace. */
  .tcards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; padding: 4px 16px 16px; }
  .tcard__count { font-size: 19px; }
}

/* ══ ADMIN TOOLS: DENSITY AND SCALE, TRANSFERRED FROM THE MOCK ═══════════════
 *
 * ⚠ EVERY NUMBER HERE IS COPIED FROM THE MOCK'S OWN SOURCE, and that sourcing
 * is deliberate. Eleven structural tests passed while the page rendered at
 * roughly double the mock's scale, because structure is what they asserted: no
 * test had ever read a font-size, a line-height, a padding or a gap.
 *
 * ⚠ AND THE FIRST ATTEMPT AT THIS BLOCK READ THE WRONG THING, recorded because
 * it produced confident numbers that were all slightly wrong. The mocks lay
 * their content out in a fixed narrow artboard: opened at 1680 the desktop
 * mock renders its roster card 216px wide, so every name wraps and every row
 * is a wrapped measurement. Reading computed heights off that render gave "the
 * roster row is 53px" when the mock's own declaration is `min-height:64px`,
 * and I spent a pass tightening a row toward a number the mock never had.
 * Font sizes survived that mistake because they do not depend on width; every
 * box measurement did not.
 *
 * So: FONT SIZES AND BOX VALUES BOTH COME FROM THE DECLARATIONS, at
 *   Admin Tools Desktop.dc.html:160-205  and  Admin Tools Mobile.dc.html:188-222,
 * with the sc-for style variables resolved from the data block at the foot of
 * each file (`cellStyle`, `pillStyle`, `dotsStyle`).
 *
 * ⚠ THE HORIZONTAL HALF OF EVERY PADDING BELOW IS PARKED, NOT APPLIED. See the
 * band rule for the conflict and who owns the ruling.
 *
 * ⚠ SCOPED UNDER .page--admin so the shared roster rules keep serving the five
 * other templates that use them unchanged. */

/* THE SECTION BAND. The mock's header is this and nothing above it: there is no
   serif section heading anywhere inside the page frame, and the 29px Libre
   Caslon "Tutors" that used to sit over "TUTORS · 7" was the same word twice
   with the invented one larger. Mock: padding 14px 20px, gap 14px. */
.page--admin .roster__toggle {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  /* ⚠ VERTICAL ONLY, AND THE HORIZONTAL HALF IS PARKED, NOT FORGOTTEN.
     The mock sets `padding:14px 20px` here and `14px 18px` on the row, so its
     dividers bleed the full card width while the text sits ~20px in.
     `test/admin-roster-edges.test.js` forbids a horizontal padding on
     .rgroup, .roster__toggle, .rlist, .rlist__filter and .roster__row outright,
     and it failed on exactly that. That test is NOT stale: it guards a real
     incident where a foreign `.rlist{padding:20px 20px 24px}` from 1800 lines
     away insetted the filter and every row while leaving the band on the card
     edge. Its literal (zero) and the mock's value are in direct conflict, so
     the ruling is Trey's and the readback carries it.

     THE COST OF PARKING IS ABOUT 5px, measured not assumed: an existing
     wrapper already insets band and rows together to x=53 in a card at x=28,
     so the rendered inset is 25px against the mock's 20px band and 18px row.
     Worth noting for the ruling: the mock's own band and row differ by 2px,
     so the mock does not itself hold the shared edge this guard protects. */
  padding-top: 14px; padding-bottom: 14px;
  border-bottom: 1px solid rgba(27, 67, 50, .09);
  border-radius: 5px 5px 0 0;
}
.page--admin .roster__head {
  font-size: 10.5px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink);
}
/* ⚠ NO UNDERLINE, per the mock and the ruling. The shared rule gives this link
   a bottom border to read as a link in its old position; on the roster band it
   sits beside a filter and a button, where an underline reads as a mistake. */
.page--admin .roster__showarch {
  margin-left: auto; font-size: 11.5px; color: var(--muted);
  text-decoration: none; border-bottom: 0;
}
/* The add button sits on the band, at the mock's own size. */
.page--admin .amodal__open {
  margin-left: auto; flex-shrink: 0;
  min-height: 36px; padding: 0 14px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .05em;
  white-space: nowrap;
}
.page--admin .roster__showarch + .amodal__open { margin-left: 0; }
/* THE FILTER, ON THE BAND. Desktop mock: flex:1, min 180 / max 300, 36px tall,
   0 11px, 12.5px.

   ⚠ THE 12.5px IS A RULED EXCEPTION TO THE PORTAL'S 16px FLOOR, not an
   oversight. test/survey-submitting.test.js refuses a font-size under 16px on
   any focusable text control, and it is media blind on purpose: a rule with no
   media query still applies on an iPad, where Safari zooms the page on focus
   exactly as iOS does. The mobile override below sets 16px, so a phone was
   never at risk; a 1024px iPad matches neither breakpoint and takes this. Trey
   ruled the mock wins here, so the guard carries a one selector allowlist and
   this comment is the other half of that record. */
.page--admin .rlist__filter {
  flex: 1; min-width: 180px; max-width: 300px;
  min-height: 36px; padding: 0 11px;
  font-size: 12.5px;
  /* ⚠ WRITTEN LONGHAND, NOT AS `margin: 0`, and that is not style. The gap test
     in admin-roster-edges reads the filter's own margin out of the stylesheet
     TEXT with /\.rlist__filter \{[^}]*margin:\s*([\d.]+px)/, and this
     selector ends in the same bytes, so a shorthand here would be picked up as
     "the filter's own top margin" and read 0 where that ruling wants 10.
     Longhand carries the same 4px off the band without impersonating it.
     The 4px was for when the filter sat BELOW the band; on the band it is just
     5px of extra band height against the mock's 64. */
  margin-top: 0; margin-bottom: 0;
}

/* THE ROWS. Mock: min-height 64px, padding 14px 18px, gap 10px, hairline top. */
.page--admin .rlist .roster__row {
  /* Vertical only. See the band above for why the mock's 18px is parked. */
  padding-top: 14px; padding-bottom: 14px;
  gap: 10px;
  min-height: 64px;
}
.page--admin .roster__nametext {
  font-size: 13.5px; font-weight: 700; line-height: 1.15;
}
.page--admin .roster__name { gap: 7px; }
.page--admin .roster__meta {
  margin-top: 2px;
  font-size: 11.5px; line-height: 1.2; color: var(--muted);
  /* ⚠ ONE LINE, AND IT WRAPS RATHER THAN TRUNCATING. Both mocks draw this as a
     single line, and the student row used to force a second with a <br>. It was
     also ellipsized, which cut people's names in half; the ruling is that a
     person's name always renders in full. With the email address off the row
     the subline fits, and on the rare row that still does not, it wraps and the
     row grows instead of hiding a name. */
  overflow-wrap: anywhere;
}

/* THE PILLS. Mock pillStyle: 3px 9px, 9px/700, tracking .06em, gap 5px.
   ⚠ line-height: 1 IS AN ADDITION TO THE MOCK'S LIST, and it is load-bearing.
   A 9px pill inheriting the body's 1.6 renders 21px tall, so the name line was
   sized by the PILL rather than by the 13.5px name beside it, and every pixel
   taken off the two text lines was taken off a line that was not the tallest
   thing in the row. The mock does not declare it because the mock's own body
   leading is not 1.6. */
.page--admin .spop__pill, .page--admin .norate, .page--admin .badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 100px;
  font-size: 9px; font-weight: 700; letter-spacing: .06em; line-height: 1;
}

/* The action column must not become the tallest thing in the row either: it is
   the menu box and nothing else. The 2px of top padding it carried made it 32
   against a 30px box, and a flex row is as tall as its tallest child. */
.page--admin .roster__acts { align-items: center; min-height: 0; padding: 0; }

/* ⚠ THE ⋯ BUTTON, and a genuine conflict between two standing rules resolved
   rather than fudged.

   The mock draws it 26x26 (dotsStyle). The 44px tap-target ruling says a
   control is 44px. A 44px box inside a 64px row is most of the row, and it was
   what held the row at 71 while the two text lines were tightened in vain.

   Both rules are kept. The BOX is 26x26, which is what sizes the row and what a
   reader sees. The TARGET is 44x44, extended past the box by an absolutely
   positioned overlay that takes part in hit-testing and not in layout. Nothing
   is smaller to a thumb; the row is smaller to the eye. */
.page--admin .rmenu { display: flex; align-items: center; }
.page--admin .rmenu__btn {
  position: relative; box-sizing: border-box;
  min-width: 26px; min-height: 26px; width: 26px; height: 26px;
  border-radius: 4px;
  font-size: 15px; line-height: 1;
}
.page--admin .rmenu__btn::after {
  content: ''; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}

/* The tiles and the settings strips, same scale. */
.page--admin .atile__title { font-size: 13.5px; font-weight: 700; }
.page--admin .sstrip__name { font-size: 13px; font-weight: 600; }
.page--admin .sstrip__note { font-size: 11px; line-height: 1.45; color: var(--muted); }

@media (max-width: 760px), (max-height: 499px) {
  /* Admin Tools Mobile.dc.html:188-222. The phone diverged harder because it
     was carrying the desktop's oversized type into a narrower column. */
  .page--admin .roster__toggle { padding-top: 13px; padding-bottom: 13px; gap: 10px; }
  .page--admin .roster__head { font-size: 10px; }
  .page--admin .amodal__open { min-height: 36px; padding: 0 12px; font-size: 11px; }

  /* ⚠ 16px, AND IT IS NOT A TYPO FOR 12.5. The mobile mock declares the phone
     filter at 16px where the desktop one is 12.5px, which is the iOS zoom
     floor: any smaller and focusing the field zooms the whole page. The two
     values are meant to differ. flex-basis 100% is what drops it onto its own
     line under the eyebrow and the add button, which is the arrangement the
     mobile mock draws. */
  /* ⚠ NO margin-top HERE, and the omission is deliberate. A 2px top margin
     tripped "the gap under the header band computes to exactly 10px", which
     records an earlier ruling of Trey's that the roster-spacing bundle wins and
     the band-to-filter gap is 10px, built as list padding with the filter's own
     margin held at zero so nothing can collapse. That ruling and the new one
     (the filter moves onto the band) now describe the same 15px of space by
     two different numbers, since the mobile mock draws its filter wrapper at
     `padding:11px 15px 0`. 10 or 11 is Trey's to settle, so this adds nothing
     to either side and the existing rule keeps producing what it produced. */
  .page--admin .rlist__filter {
    /* ⚠ order 1 PUTS IT BELOW BOTH, and without it the band came out three
       lines tall: eyebrow, filter, then the add button pushed onto a line of
       its own. The mobile mock draws eyebrow and ADD TUTOR on one line with the
       filter under them, which is document order plus this one property. */
    order: 1;
    flex: 1 0 100%; max-width: none;
    min-height: 40px; padding: 0 11px; font-size: 16px;
  }

  /* ⚠ THE PILLS MUST WRAP AT 390 OR THEY LAND ON THE MENU. Measured: the third
     pill started at x=307 and ran to 340, while the action column occupies
     309 to 339, so "1 STUDENT" was drawn on top of the dots. The row is
     flex-wrap:nowrap by design, so the name line has to give instead. The
     mock's phone row carries two pills and ours carries three, which is why the
     mock never had to solve this. */
  .page--admin .roster__name { flex-wrap: wrap; row-gap: 4px; }



  /* ⚠ NO GROW. A phone rule gives the name flex:1 1 auto, so a 69px name was
     stretched to 156px and the first pill was shoved to the right edge with a
     hole between the two. The mock declares flex-shrink:1 and min-width:0 and
     no grow at all, which is what puts the pills straight after the name and
     lets a long name ellipsize instead of pushing them off. */
  .page--admin .roster__nametext { flex: 0 1 auto; min-width: 0; }

  .page--admin .rlist .roster__row {
    padding-top: 12px; padding-bottom: 12px; gap: 10px; min-height: 0;
  }
  .page--admin .roster__name { gap: 6px; }
  .page--admin .roster__meta { margin-top: 3px; font-size: 11px; }
  .page--admin .spop__pill, .page--admin .norate, .page--admin .badge {
    gap: 4px; padding: 3px 8px; font-size: 8.5px;
  }
  /* The phone mock draws the menu larger than the desktop one, at 30x30. */
  .page--admin .rmenu__btn {
    min-width: 30px; min-height: 30px; width: 30px; height: 30px; font-size: 14px;
  }

  /* The settings values, per Admin Tools Mobile.dc.html:270 and :281. */
  .page--admin .sstrip__money { font-size: 18px; }
  .page--admin .sstrip__value { font-size: 12.5px; }

  /* ── ABOVE THE ROSTER, which is where the phone was still at the old scale.
     The desktop was close before this pass because .achip and .achip__btn were
     already carrying the desktop mock's numbers exactly (34px/0 12px/12px and
     26px/0 10px/10.5px). The phone had no overrides at all, so it rendered the
     DESKTOP's values in a 390px column, and the mock declares a different set
     for every one of them. */

  /* Mock: 21px, against the portal-wide 28px. This is the single biggest
     item on the phone page and it was 7px over. */
  .page--admin .h1 { font-size: 21px; margin-bottom: 0; }

  /* Mock: 36px tall, 0 11px, 11.5px. */
  .page--admin .achip { min-height: 36px; padding: 0 11px; font-size: 11.5px; }
  /* The auto-send chip is the taller one, and it holds a button. */
  .page--admin .achip--act { min-height: 40px; padding: 0 5px 0 12px; font-size: 12px; gap: 8px; }
  .page--admin .achip__btn { min-height: 30px; padding: 0 12px; }
  .page--admin .achips { gap: 6px; }

  /* The section rail. Mock: 36px tall, 0 14px, 12.5px. */
  .page--admin .srail__pill { min-height: 36px; padding: 0 14px; font-size: 12.5px; }

  /* Mock: 56px tall, 12px 13px, gap 9px, icon 28x28 at 13px, title 13px/1.25. */
  .page--admin .atile { min-height: 56px; padding: 12px 13px; gap: 9px; }
  .page--admin .atile__icon { width: 28px; height: 28px; font-size: 13px; }
  .page--admin .atile__title { font-size: 13px; line-height: 1.25; }
}

/* ══ TRUST ON A PHONE: ITS OWN COMPONENT, NOT THE CARD GRID REFLOWED ════════
 *
 * Admin Tools Mobile.dc.html:301-323. Per Trey's structural ruling, desktop and
 * mobile are two designs: the desktop mock draws four cards across, the mobile
 * mock draws a stacked list inside one card with a row the desktop card has no
 * equivalent of. Squeezing .tcards into two columns is the thing that ruling
 * forbids, and it is what shipped before this.
 *
 * ⚠ THE SWAP IS BY CLASS IN A MEDIA QUERY, NEVER THE `hidden` ATTRIBUTE. Both
 * structures carry an author origin `display`, and an author `display` beats the
 * user agent's `[hidden]{display:none}`, so an attribute swap would paint both.
 * That exact failure has already shipped from this file once, on .roster__row,
 * and sat on production until a human noticed. The specificities are written to
 * match: .page--admin .tcards is (0,2,0) against .tcards at (0,1,0).
 */
.tlist { display: none; }

.tlist__row { padding: 12px 15px; border-top: 1px solid rgba(27, 67, 50, .07); }
.tlist__row--eligible { background: rgba(201, 162, 39, .07); }
.tlist__top { display: flex; align-items: baseline; gap: 10px; }
.tlist__student {
  flex: 1; min-width: 0;
  font-size: 13.5px; font-weight: 700; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tlist__streak { flex-shrink: 0; font-size: 12px; font-weight: 700; color: var(--ink); }
.tlist__mid { margin-top: 5px; display: flex; align-items: center; gap: 10px; }
.tlist__bar {
  flex: 1; height: 5px; border-radius: 3px;
  background: rgba(27, 67, 50, .1);
  position: relative; overflow: hidden;
}
.tlist__bar > span { display: block; height: 100%; border-radius: 3px; background: var(--gold); }
.tlist__bar--full > span { background: var(--green-btn); }
.tlist__togo { flex-shrink: 0; font-size: 11px; color: var(--muted); }
.tlist__tutor { margin-top: 4px; font-size: 11px; color: var(--muted); }
.tlist__foot {
  margin: 0; padding: 11px 15px;
  border-top: 1px solid rgba(27, 67, 50, .09);
  font-size: 11px; line-height: 1.6; color: var(--muted);
}
/* The action takes the mock's "to go" slot, so it cannot make the row taller
   than the shape the mock draws. */
.tlist__mid .trust__revoke, .tlist__mid .trust__promote { flex-shrink: 0; }

@media (max-width: 760px), (max-height: 499px) {
  .page--admin .tcards { display: none; }
  .page--admin .tlist { display: block; }
  /* The mock's head carries TRUST and See all only; the sentence moves to the
     foot of the card, where .tlist__foot draws it. */
  .page--admin .trust .card__blurb { display: none; }
}

/* ══ 11 · THE PAGINATION FOOTER, FIXED ONCE AT THE SOURCE ═══════════════════
 *
 * Every paginated card rendered its Previous / Page N of M / Next row hard
 * against the card's bottom edge. It is ONE rule and it had `padding-top: 10px`
 * with no bottom, so the clearance above the row existed and the clearance below
 * it did not, on every instance at once.
 *
 * ⚠ FIXED HERE RATHER THAN PER CARD, deliberately. Patching Tutors and Students
 * separately would leave the next paginated card to be born with the same bug,
 * and there is no second rule to keep in step: this selector is what every
 * pager on the page uses. */
.rlist__pager { padding-bottom: 12px; }

/* ══ 16 · THE PUSH-TONIGHT CHECKBOX ═════════════════════════════════════════
 * Standard control size, and centred on the TITLE line rather than on the whole
 * two-line block or hugging its top. The note wraps under the title without
 * moving the box, which is what `align-items: flex-start` plus a fixed box and
 * a matched line-height buys: the box's centre sits on the title's centre
 * because both are the same height at the top of the label. */
/* ⚠ THE UNSCOPED COPY OF THIS BLOCK IS DELETED, and its deletion is the point.
   It was my first attempt at the checkbox, five declarations at (0,1,0) against
   the `.page--admin .optout*` rules at (0,2,0) written twelve thousand lines
   earlier. Every one of them was dead the moment it was written, and the file
   read as though the control had been fixed. The live rules are the scoped ones
   in the mobile round's block below; scripts/audit-cascade.js finds this shape. */

@media (max-width: 760px), (max-height: 499px) {
  /* ══ 4a · THE PAGE SCROLLS CLEAR OF THE TAB BAR ═══════════════════════════
     ⚠ NOT A STUCK CONTAINER. Nothing was trapping the gesture: the page simply
     could not scroll far enough. .page--admin is the scroll container and its
     mobile bottom padding is 40px, while the fixed tab bar over it is 8px of
     padding plus a 52px row plus `max(14px, var(--sab))` underneath, which is
     74px on a phone with no home indicator and about 94px on one with. So
     between 34 and 54px of the page's end sat permanently behind the bar,
     unreachable and untappable however hard you scrolled. The Weekly earnings
     card is the last section, so what was buried was its foot and the System
     status line under it.

     The padding is written from the bar's own numbers rather than a round
     figure, so it cannot drift out of step if the bar is retuned. --sab, never
     env(), per the safe-area rule this file already carries.

     ⚠ SPECIFICITY: an id-free page-scoped rule loses to the shell's own
     `.page--admin .page` block, so this states the class twice to clear it. */
  .page--admin.page--admin {
    padding-bottom: calc(60px + max(14px, var(--sab)) + 20px);
  }

  /* ══ 6 · THE CASELOAD POPOVER CANNOT BE CLIPPED ═══════════════════════════
     ⚠ AND MY LAST VERDICT ON THIS WAS WRONG, FOR A REASON WORTH KEEPING. I
     probed the card's ancestors for a clipping one, found none, and called it
     non-reproducing. The probe walked SEVEN levels, which reaches .rgroup and
     stops: it never got to .page--admin, which on a phone is the scroll
     container. That container is overflow-y:auto, so its X axis computes to
     auto alongside it, and it clips absolutely positioned descendants on both
     axes. The card was anchored to the PILL with `left: 0` and a 210px minimum,
     so a pill sitting right of centre put its card past the container's right
     edge and the list was cut off there.

     Anchored to the ROW instead of the pill, spanning it. A card that is as wide
     as the row it belongs to cannot leave the container sideways, whatever the
     pill's position in the line, and it no longer depends on the pill's own
     inline box being in a useful place now that the name line wraps. */
  .page--admin .rlist .roster__row { position: relative; }
  .page--admin .spop { position: static; }
  .page--admin .spop__card {
    left: 15px; right: 15px; min-width: 0; width: auto;
    /* ⚠ 100%, NOT `100% - 6px`. Those six pixels put the card OVER the last six
       pixels of its own row, and the pill can sit in them now that the name line
       wraps. A card covering its own pill eats the closing tap: the tap lands on
       the card, the handler treats it as reading the list, and the popover
       refuses to close however many times it is tapped. */
    top: 100%;
  }
  /* ⚠ THE PILL OUTRANKS THE CARD AND GOES UNDER THE STICKY HEADER, and 61 did
     the first job while failing the second.
   *
   * WHAT THE STACK ACTUALLY IS, walked at 390 with the list scrolled: the pill's
   * ancestors to the root are .spop (static), .roster__name, .roster__main,
   * .roster__row (position:relative, z-index AUTO), .rlist, .rgroup, .card,
   * .cols, main.page--admin, body, html. NOT ONE of them creates a stacking
   * context: position without z-index creates none. So the pill's z-index is
   * compared against .shead's in the ROOT context, and 61 beat 30 outright.
   *
   * The card was put under the header last round and the PILL was left at 61,
   * which is the whole of the second report: the pills, not the cards, were
   * drawing over the heading and the chip rail.
   *
   * 21 keeps the pill above its own card (20) so the closing tap still reaches
   * it, which is the job the comment above describes, and loses to the header. */
  .page--admin .spop__pill { position: relative; z-index: 21; }
  /* The list keeps its own scroll, bounded so the card cannot run past the
     bottom of the viewport on a short phone. */
  .page--admin .spop__list { max-height: min(320px, 46vh); }

  /* ══ 8 · THE FIRST SECTION CLEARS THE STICKY UNIT ═════════════════════════
     The status chips began flush against the rail, because the unit is the
     first thing in the scroll container and nothing separated the two. Every
     other section is spaced by the shared 14px card margin; the chips are not a
     card, so they had none of it. */
  .page--admin .achips { margin-top: 14px; }
  /* The head's inset follows the composer's on a phone, so the rule above still
     lines up with the fields. */
  .page--admin .ann__head { padding: 13px 15px; }
  .page--admin .ann__head::after { left: 15px; right: 15px; }

  /* ══ 17 · THE SENT LOG DOES NOT CLIP AN ENTRY IN HALF ═════════════════════
     Measured at 390: the log capped at 196px over 788px of entries, so the
     boundary fell wherever it fell and the last visible row was cut through the
     middle. It DID scroll, which is why this reads as "clipped" rather than
     "stuck": a nested scroller inside a page that already scrolls is easy not to
     find, and half a row at the bottom edge looks like a rendering fault rather
     than an invitation.

     The phone takes the other option: the card grows and every entry renders
     whole. The page is the scroller, which is the one a thumb already knows.
     The desktop keeps its capped scroller, where the pane sits beside a
     composer and has a height to fit into. */
  /* ⚠ (0,3,0), AND THAT EXTRA CLASS IS LOAD-BEARING. The desktop rule is
     `.page--admin .ann__log`, which is (0,2,0) exactly as this would be, and it
     is declared LATER in the file, so an equal-specificity override here loses
     on order and the cap silently stayed at 196px. Measured, not assumed: the
     first attempt at this rule changed nothing at all. */
  .page--admin .ann__live .ann__log {
    max-height: none; overflow-y: visible; flex: none;
  }

  /* ══ 8 · THE COMPOSER'S INNER INSET ═══════════════════════════════════════
     The mobile mock's composer body is `padding:13px 15px 15px` (:114). Ours
     carried the desktop's 20px, so every field, label, counter, the holding
     notice and the send button sat 5px further from the card edge than the
     mock draws them, on a card only 338px wide. */
  .page--admin .ann__compose { padding: 13px 15px 15px; }
  .page--admin .ann__livehead { padding: 14px 15px 10px; }
  .page--admin .ann__row { padding: 11px 15px; }
  .page--admin .ann__empty { padding: 14px 15px; }
  .page--admin .ann__foot { padding: 11px 15px 16px; }

  /* ══ 15a · THE AUTO-SEND CHIP ══════════════════════════════════════════════
     The mock draws a full-width row: the label and its dot on the left, the
     TURN ON pill right-aligned with room around it. Ours crowded the two
     together because the chip sized to its content and the pill sat straight
     after the label. */
  .page--admin .achip--act { width: 100%; justify-content: flex-start; }
  .page--admin .achip--act > span:nth-of-type(2) { flex: 1; min-width: 0; }
  .page--admin .achip__btn, .page--admin .achip__form { margin-left: auto; }

  /* ══ 15c · THE PHONE TILE IS TITLE ONLY ═══════════════════════════════════
     Both mocks use the same short title; only the DESCRIPTION differs by
     surface. The phone mock draws none, so it is hidden here and nowhere else. */
  .page--admin .atile__desc { display: none; }

  /* 15b · the phone's shorter chip copy. */
  .page--admin .achip__long { display: none; }
  .page--admin .achip__short { display: inline; }
}

/* Above the phone breakpoint the chips take their longer copy. The tile titles
   no longer differ by surface at all; see the tile block below. */
.achip__short { display: none; }
.achip__long { display: inline; }

/* Below the desktop breakpoint the phone's row is the one that paints. */
.achips--desk { display: none; }

/* ══ 19 · THE PILLS SIT ON THE TITLE'S LINE ══════════════════════════════════
 * Desktop mock :65-79. The heading row is
 *   padding: 22px 26px 0; display: flex; align-items: flex-end;
 *   justify-content: space-between; gap: 20px; flex-wrap: wrap
 * with the h2 on the left and the four status pills right-aligned beside it.
 * Ours stacked the pills underneath, flush against the rule under the heading.
 *
 * ⚠ THE 20px GAP IS THE MOCK'S OWN, and it is what answers "no space above the
 * pill row": on a narrow desktop the row wraps, and the gap is the space between
 * the title and the pills when it does. Read from the declaration above rather
 * than chosen to look right.
 *
 * DESKTOP ONLY. The phone stacks them by design and has its own arrangement.
 */
@media (min-width: 761px) and (min-height: 500px) {
  /* ⚠ THE PILLS ARE NOT ON THE HEADING LINE, AND THIS IS A RULE CONFLICT I AM
     NOT RESOLVING ON MY OWN.

     Desktop item 19 asks for the mock's arrangement: title left, the four status
     pills right-aligned on the SAME line, as part of the sticky header unit.
     That requires the pills to live inside .shead. Tonight's mobile ruling
     defines that unit as "the Admin tools heading plus the chip rail below it",
     and putting the pills inside it makes the phone's sticky band 250px tall and
     drags it over the nav: measured, band top fell from 72 to 44 and its height
     went from 129 to 250. The two rulings cannot both hold with one copy of the
     markup.

     The gap IS applied, which is the newly reported half: 20px, the mock's own
     value from its heading row (:65), so the pills are no longer flush against
     the rule under the heading.

     Resolving it properly means a second copy of the pill row shown per
     breakpoint, which duplicates the auto-send POST form, and that is Trey's
     call rather than mine. */
  /* ⚠ THE DESKTOP COPY RENDERS, THE PHONE COPY DOES NOT. Two pill rows exist in
     the document because the two rulings put them in different parents: the
     desktop's on the heading line inside the sticky unit, the phone's below it.
     Only one paints on each surface. The auto-send POST form is NOT duplicated:
     one form is declared once and each row's button submits it by id. */
  .page--admin .achips--phone { display: none; }
  .page--admin .achips--desk {
    display: flex; margin: 0; flex: 1 1 auto;
    justify-content: flex-end; align-items: center;
  }
  .page--admin .shead__top {
    align-items: center; justify-content: space-between;
    gap: 20px; flex-wrap: wrap;
  }
  /* The contextual counter belongs to the phone's rail, which the desktop does
     not have, so it has nothing to change with here. */
  .page--admin .shead__note { display: none; }
}

/* ══ 17b/17c · ONE INNER INSET FOR EVERY DIALOG ══════════════════════════════
 * The School year dialog put its eyebrow, body and content narrower than the
 * rule beneath them and carried too much air above the eyebrow; Notifications
 * had the opposite fault, its heading and closing text jammed against the
 * edges. Both are symptoms of each dialog carrying its own numbers.
 *
 * One standard, applied at the shared panel: the head and the body take the same
 * horizontal inset, so a rule drawn across the head lines up exactly with the
 * text under it, and the vertical rhythm is the same in every dialog.
 */
.page--admin .amodal__panel > .card__head,
.page--admin .amodal__panel > .amodal__section { padding: 18px 22px; }
.page--admin .amodal__panel .card__blurb,
.page--admin .amodal__panel .lbl,
.page--admin .amodal__panel .in { max-width: none; width: 100%; }
/* The eyebrow sits on the same inset as the rule below it, not further in. */
.page--admin .amodal__panel .eyebrow { margin: 0 0 6px; }

/* ══ THE ROSTER, AT THE MOCK'S OWN VALUES (desktop mock :224-226, :725, :755) ══
 *
 * ⚠ READ FROM THE DECLARATIONS, NOT FROM THE RENDER, for the reason recorded
 * earlier in this file: the mock lays out in a fixed artboard and measuring its
 * rendered boxes once produced a confident set of numbers that were all wrong.
 *
 *   name      13.5px / 700 / #1D2B23      tutor cell  min-height 64, pad 14px 18px
 *   subline   11.5px / #5C6B62, mt 2px    student cell min-height 60, pad 12px 18px
 *
 * The row heights were the item: the rows rendered too short because nothing set
 * a minimum at all, so a row was only as tall as its two text lines.
 */
.page--admin .rlist .roster__row { min-height: 64px; }
.page--admin .rlist--cols2 .roster__row { min-height: 60px; }
.page--admin .roster__nametext { font-size: 13.5px; font-weight: 700; color: var(--ink); }
.page--admin .roster__meta { margin-top: 2px; font-size: 11.5px; color: var(--muted); }

/* ══ 11 · THE ROSTER HEADER TAKES THE SETTINGS INSET, ON THE DESKTOP TOO ══════
 * Same arithmetic as the phone: the band centres a 15px eyebrow against a 36px
 * control, so 14px of padding lands the eyebrow 24px down while SETTINGS, whose
 * head carries no control, puts its own at 14px. 4px is what solves for it.
 */
.page--admin .roster__toggle { padding-top: 4px; padding-bottom: 4px; }

/* ══ 16 · THE SETTINGS SUBTEXT WRAPS RATHER THAN RUNNING OFF ══════════════════
 * "What the counters and lists show" was truncating and overflowing its cell.
 * A note that runs out of its box tells the reader less than one that takes two
 * lines.
 */
.page--admin .sstrip__note {
  white-space: normal; overflow: visible; text-overflow: clip;
  overflow-wrap: anywhere;
}
.page--admin .sstrip__cell { align-items: flex-start; }

/* The row rule, from the mock's own cellStyle (:725). Missing on both rosters:
   the cards read as an undivided block of names. */
.page--admin .rlist .roster__row { border-top: 1px solid rgba(27, 67, 50, .07); }

/* ══ THE TOOL TILES, PER THE DESKTOP MOCK (:82-96) ══════════════════════════
 * ⚠ THE DESCRIPTIONS RENDER ON DESKTOP, and this supersedes the earlier
 * amendment that hid them. The mock draws a short title with a description line
 * under it, and Trey has ruled that the mock wins here.
 *
 * ⚠ AND NOTHING LEAVES ITS TILE. `.atile__title` carried
 * `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, which is
 * what cut "Training roster and mock scorin" mid-word and sliced the BETA badge
 * in half: an ellipsis on a flex item with a badge inside it clips the badge
 * rather than the text. The titles are short now AND the clipping is gone, so a
 * label that ever did outgrow its tile would wrap and the tile would grow.
 *
 * SPECIFICITY: `.page--admin .atile__title` is (0,2,0) against the base
 * `.atile__title` at (0,1,0), so these win. Checked with
 * scripts/audit-cascade.js rather than assumed.
 */
/* ⚠ INSIDE THE DESKTOP QUERY, AND THE FIRST VERSION WAS NOT. Written unscoped
   these are (0,2,0), exactly like the phone's own `.page--admin .atile` and
   `.page--admin .atile__desc`, and declared LATER, so they beat the phone from
   outside its media query: the descriptions rendered at 390, the tiles grew, and
   the tile grid went from 128px to 256px tall. That is the same trap that has
   now cost five fixes today, and this time it was a DESKTOP ruling reaching on
   to the phone. A ruling about one surface is scoped to that surface. */
@media (min-width: 761px) and (min-height: 500px) {
  .page--admin .atiles { align-items: stretch; }
  .page--admin .atile {
    align-items: flex-start; gap: 11px;
    padding: 14px 15px; min-height: 0;
  }
  .page--admin .atile__title {
    white-space: normal; overflow: visible; text-overflow: clip;
    font-size: 13.5px; font-weight: 700; line-height: 1.25;
  }
  .page--admin .atile__beta { flex-shrink: 0; overflow: visible; }
  .page--admin .atile__desc {
    display: block; margin-top: 2px;
    font-size: 11.5px; line-height: 1.45; color: var(--muted);
    white-space: normal; overflow: visible; text-overflow: clip;
  }
  .page--admin .atile__icon { width: 30px; height: 30px; font-size: 14px; }
}

/* ══ THE NIGHT'S MOBILE ROUND ═══════════════════════════════════════════════
 * Every one of these was reported live against production after e69e76f, and
 * several were claimed fixed before. Where a claim failed, the reason is written
 * beside the rule rather than left for the next person to rediscover.
 */

/* ── 7 · THE ROSTER HEADER SITS WHERE THE SETTINGS HEADER SITS ──────────────
   ⚠ THE LAST PASS DID CHANGE THE CARD, AND THAT IS WHY THE GAP SURVIVED. The
   card went flush (`padding: 0 24px`) and it is in the served stylesheet. The
   remaining depth is not padding at all, it is CENTRING: the band holds a 36px
   ADD button beside a 15px eyebrow and centres them, so the eyebrow's top edge
   lands (64 - 15) / 2 = 24px below the card, while SETTINGS, whose head has no
   control on the line, puts its eyebrow at its own 14px.

   Matching Settings therefore means solving for the padding that puts a centred
   eyebrow at 14px against a 36px control: 14 - (36 - 15) / 2 = 3.5, so 4px. The
   band comes out 44px against the Settings head's 43px, and the two eyebrows
   sit on the same inset. */
.page--admin .roster__toggle { padding-top: 4px; padding-bottom: 4px; }

/* ── 11 · THE ANNOUNCEMENTS HEADING TAKES THE SAME INSET ────────────────────
   .ann__head and .sstrip__head both already declare `padding: 14px 20px`, so
   the extra depth was never in the heading rule: the announcements CARD keeps
   its own `card--pad` padding because the flush rule excludes `.ann`, and that
   padding sits above the heading. The exclusion goes; the card is a frame like
   every other one on the page. */
/* ⚠ A FLOOR UNDER THE LAST ROW. The card was `padding: 0` on every side, so the
   "Who's read it" link ended flush against the card's bottom edge with nothing
   under it. 14px is the card's own interior rhythm rather than a new number:
   .ann__head is `14px 20px`, so the space below the last row now matches the
   space above the first. The other three sides stay 0, which is what makes the
   head and the rows meet the card's edges. */
.page--admin section.card--pad.ann { padding: 0 0 14px; }

/* ── 9 · THE COMPOSER'S FIELDS AND ITS RULE ARE THE SAME WIDTH ──────────────
   The head's rule is a border-bottom on a full-width element, so padding cannot
   inset it, and the fields sit 15px in. Drawn as an inset line instead, so the
   rule starts and ends exactly where a field does. */
.page--admin .ann__head { position: relative; border-bottom: 0; }
.page--admin .ann__head::after {
  content: ''; position: absolute; left: 20px; right: 20px; bottom: 0;
  height: 1px; background: rgba(27, 67, 50, .09);
}

/* ── 10 · THE PUSH-TONIGHT CHECKBOX, AND WHY THE LAST FIX DID NOTHING ───────
   ⚠ MY PREVIOUS RULE WAS UNSCOPED AND LOST ON SPECIFICITY. `.optout__box` is
   (0,1,0). The Edit-student form's Parent-opted-out checkbox had already been
   scoped `.page--admin .optout__box` at (0,2,0), twelve thousand lines earlier,
   carrying `width:16px; height:16px; margin:2px 0 0`. That rule wins everywhere
   on this page whatever order mine is written in, so the box kept its old size
   and its top margin and the commit changed nothing a reader could see.

   THIS IS THE THIRD TIME TODAY the same trap has cost a fix: a plain-class rule
   written later, beaten by a page-scoped rule written earlier. The lesson is
   that on this page a new rule must be AT LEAST as specific as the oldest one
   touching the same property, not merely later in the file.

   Both checkboxes want the same thing, so this is scoped to match and applies to
   the composer and the Edit-student form alike. */
.page--admin .optout__box {
  flex-shrink: 0;
  width: 20px; height: 20px;
  /* ⚠ MARGIN ZERO, NOT `2px 0 0`. That top margin is what pinned the box to the
     top of a two-line label. It centres on the TITLE's line box below. */
  margin: 0;
  align-self: flex-start;
}
.page--admin .optout { display: flex; align-items: flex-start; gap: 10px; }
.page--admin .optout__title { display: block; line-height: 20px; }
.page--admin .optout__note { display: block; margin-top: 2px; line-height: 1.5; }

/* ══ NO PERSON'S NAME IS EVER TRUNCATED ═════════════════════════════════════
 *
 * Ruled: full names render, always, on every surface of this page. Truncation
 * was spread across a dozen rules that each looked reasonable on its own, and
 * together they cut "Caroline Pousson" to "Caroline Pous…" and a parent's name
 * to nothing at all in a narrow column.
 *
 * ⚠ EVERY ONE OF THESE CARRIES A PERSON'S NAME. Elements that carry a LABEL
 * keep their ellipsis, because a clipped label is a cosmetic problem and a
 * clipped name is a wrong answer to "who is this".
 *
 * They wrap instead, so the row grows rather than hiding anything. That is the
 * trade the ruling makes and it is the right way round.
 */
.page--admin .roster__nametext,
.page--admin .roster__meta,
.page--admin .tcard__student,
.page--admin .tcard__tutor,
.page--admin .tlist__student,
.page--admin .tlist__tutor,
.page--admin .paylist__name,
.page--admin .paylist__meta,
.page--admin .pay__name {
  white-space: normal; overflow: visible; text-overflow: clip;
  overflow-wrap: anywhere; min-width: 0;
}

/* ══ THE DESKTOP MOCK, SECTION BY SECTION ═══════════════════════════════════
 * Every number below is copied from Admin Tools Desktop.dc.html, cited inline.
 * The mock's artboard is 1682x2573, so it renders at true desktop scale and
 * these were read against its own render rather than guessed.
 */

/* ── 2 · SECTION SPACING, UNIFORM ────────────────────────────────────────────
   The mock wraps EVERY section in `padding:14px 26px 0` (:159, :218, :284,
   :458) with the tile strip at `16px 26px 0` (:81). So the gap between any two
   adjacent sections is 14px, everywhere, with no exceptions and no section
   pair touching. Settings-to-Students and Trust-to-Weekly-earnings had none at
   all, which is what made those two read as one run-on panel. */
/* ⚠ NOT ONLY DIRECT CHILDREN, which is the whole reason this was uneven. The
   roster sections sit inside a wrapper, so a child-combinator rule reached the
   others and left those two on whatever they already had, which was 22px. That
   is why the spacing read as arbitrary rather than simply absent. */
/* ⚠ .sstrip IS A SECTION AND IS NOT A .card, which is why it was skipped by
   every rule and every sweep here. It sits between the Students roster and
   Trust, and the roster above it had its margin zeroed by a :last-child that
   lands on the last child of an INNER container rather than the last section on
   the page. Those two together are the Settings-to-Students zero gap.

   ⚠ NO :last-child ZEROING. A trailing 14px costs nothing and the selector
   could not be written correctly here without knowing which container the last
   section happens to live in. */
.page--admin .card,
.page--admin section.card,
.page--admin .sstrip { margin-top: 0; margin-bottom: 14px; }
/* ⚠ THE WRAPPER HAS ITS OWN TWO NUMBERS AND BOTH HAD TO GO. `.cols` carries
   `margin-top: 18px` and `.cols--stack > .card + .card` carries another
   `var(--rt-s5)` between the rosters, so the spacing between sections was the
   sum or the collapse of whichever pair happened to be adjacent, which is why
   it read as arbitrary. The card margins above are the one number now.

   The second selector needs (0,4,0) to beat `.cols--stack > .card + .card`,
   which is (0,3,0): the same per-property cascade trap that cost a pass
   earlier in this page's history. */
.page--admin .cols { margin-top: 0; }
.page--admin .cols--stack > .card { margin-top: 0; }
.page--admin .cols--stack > .card + .card { margin-top: 0; }

/* ── 3 · THE ⋯ MENU STAYS INSIDE ITS OWN COLUMN ─────────────────────────────
   The mock draws a vertical rule between roster columns (:293 border-right on
   the Trust cell, and the roster cells' own 18px side padding) and puts the
   dots 18px inside that rule. Ours had neither: the grid is gap:0 with no rule
   and the row carries no horizontal padding, so the dots sat about 5px from
   the next column's name and read as belonging to it.

   ⚠ NOT SOLVED WITH ROW PADDING, which is the mock's own mechanism. Trey ruled
   that admin-roster-edges keeps its ban on horizontal padding for .roster__row,
   so the inset is carried by a margin on the action column instead, which that
   guard does not name. Same pixels, a rule the guard permits. */
.page--admin .rlist--cols3 .roster__row,
.page--admin .rlist--cols2 .roster__row { border-right: 1px solid rgba(27, 67, 50, .07); }
/* ⚠ `of .roster__row`, AND WITHOUT IT THIS CLEARED THE WRONG COLUMN. The grid's
   children are not only rows: a full-width child sits ahead of them, so plain
   nth-child counts from the wrong start. Measured at 1440 the first row was
   child 2, which made nth-child(3n) land on the MIDDLE column of each band and
   nth-child(2n) on the FIRST of each pair.
 *
 * That is both reported faults at once and they were one bug: the three-column
 * tutors grid lost the rule between columns two and three while keeping a stray
 * one at the card's right edge, and the two-column students grid lost its only
 * divider the same way. Counting rows among rows is immune to whatever else the
 * grid holds. */
.page--admin .rlist--cols3 .roster__row:nth-child(3n of .roster__row),
.page--admin .rlist--cols2 .roster__row:nth-child(2n of .roster__row) { border-right: 0; }

/* ⚠ AND THE RULE FALLS AT THE MIDDLE OF THE GUTTER. The row bans horizontal
   padding (admin-roster-edges guards it), so the 18px inset was carried on the
   action column alone: content stopped 18px short of the rule on its left and
   the next column's NAME began 0px after it, which is what "nearly touching
   Alison MacLeod" is. The same 18px on the leading child makes the two sides
   equal, so the line sits centred between the dots on its left and the name on
   its right. Carried on the CHILD, not on the row, which is the mechanism the
   action column already uses and the one that guard permits. */
.page--admin .rlist--cols3 .roster__row > :first-child,
.page--admin .rlist--cols2 .roster__row > :first-child { margin-left: 18px; }
/* ⚠ EVERY COLUMN, THE LAST ONE INCLUDED. The mock's cell carries `14px 18px`
   throughout, so the dots are 18px inside the cell's edge whether or not there
   is a rule beyond it. Dropping the inset on the last column, as this first
   did, put those dots hard against the card edge. Only the RULE is dropped. */
.page--admin .roster__acts { margin-right: 18px; }

/* ⚠ THE NAME LINE WRAPS AT EVERY WIDTH NOW, and the cap is why. The mock draws
   three roster columns across a 1682px artboard, which gives each about 540px
   and lets a name and three pills sit on one line comfortably. Inside the
   1080px shell each column is about 345px, and with the pills at flex-shrink 0
   the row could not fit them: the action column was pushed 12px PAST its own
   right edge and the dots landed outside the cell.

   The media query that would have dropped to two columns keys on the VIEWPORT,
   which is 1440 or 2400 while the content column is fixed at 1080, so it never
   fired and could not have.

   Wrapping keeps the mock's three-column hierarchy, which is the thing that
   scales, and gives up single-line rows, which is the thing that cannot. The
   name still ellipsizes first, so a long name yields before a flag does. */
.page--admin .roster__name { flex-wrap: wrap; row-gap: 4px; }

/* ⚠ TWO ROSTER COLUMNS INSIDE THE CAP, WHICH IS HOW THE MOCK'S ROW SURVIVES IT.
   What the mock fixes is the CELL, not the column count: three columns across
   its 1682px artboard give each about 540px, and the 64px single-line row with
   9px pills is drawn for a cell that wide. Three columns inside the 1080px
   shell give 345px cells, and the row can only answer that by wrapping to 83px,
   which is not the mock's row at all. Two columns give about 516px, within a
   few pixels of the mock's own cell.

   That is what "the proportions scale into the capped shell" means here: the
   cell is the proportion, and the column count is what gives to preserve it.

   ⚠ GUARDED BY A DESKTOP QUERY OF ITS OWN. `.page--admin .rlist--cols3` is
   (0,2,0) and the phone's single column `.rlist--cols3` is (0,1,0), so written
   unscoped this beats the phone from outside its media query and the roster
   renders two columns at 390. Media queries add no specificity; only the
   selector does.

   ⚠ AND WRITTEN HERE, NOT BESIDE THE PHONE'S WRAP RULE. The two wrap rules read
   identically, so an earlier attempt at this edit matched the PHONE's copy and
   dropped an @media inside the phone's @media. Nested @media is invalid, the
   browser discarded the block whole, and the page kept rendering three columns
   while the stylesheet read as though it had been fixed. */
@media (min-width: 761px) and (min-height: 500px) {
  /* ⚠ THREE COLUMNS, BY RULING, SUPERSEDING MY TWO-COLUMN JUDGMENT CALL. I went
     to two because three inside the 1080px cap gives 345px cells and the mock's
     64px single-line row cannot hold a name plus chips at that width, so rows
     wrap and grow. Trey has accepted that cost and ruled the mock's three. The
     wrap is kept clean below: chips never overlap the divider or the dots. */
  .page--admin .rlist--cols3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* ⚠ THE DIVIDERS ARE THE MOCK'S OWN VALUES, read from its cell declarations:
     `border-right: 1px solid rgba(27,67,50,.07)` between columns (Trust cell,
     :293) and `border-top: 1px solid rgba(27,67,50,.07)` between rows
     (cellStyle, :725). They were missing entirely on both rosters.

     ⚠ AND THEY ARE DRAWN PER COLUMN POSITION, not on every cell. A border-right
     on all of them puts a rule down the card's outer edge as well, which the
     mock does not draw. nth-child picks the last column in each grid and drops
     it there. */
  .page--admin .rlist--cols3 .roster__row { border-right: 1px solid rgba(27, 67, 50, .07); }
  .page--admin .rlist--cols3 .roster__row:nth-child(3n of .roster__row) { border-right: 0; }
  .page--admin .rlist--cols2 .roster__row { border-right: 1px solid rgba(27, 67, 50, .07); }
  .page--admin .rlist--cols2 .roster__row:nth-child(2n of .roster__row) { border-right: 0; }

  /* The dots keep one inset in EVERY column, last one included, so the first
     column does not hug tighter than the others. */
  .page--admin .rlist--cols3 .roster__row .roster__acts,
  .page--admin .rlist--cols2 .roster__row .roster__acts { margin-right: 18px; }
}

/* ── 1 · TRUST: CELLS IN ONE CARD, NOT FOUR DETACHED CARDS ──────────────────
   Desktop mock :290-303. The four are cells of a single grid divided by
   vertical rules, gap 0, no per-card border, background or radius. Ours drew
   four separate bordered cards with a 10px gap between them. */
.page--admin .tcards {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0; padding: 0; margin: 0;
}
.page--admin .tcard {
  display: block;
  padding: 14px 20px;
  border: 0; border-right: 1px solid rgba(27, 67, 50, .07);
  border-radius: 0; background: transparent;
}
.page--admin .tcard:last-child { border-right: 0; }
.page--admin .tcard--eligible { background: rgba(201, 162, 39, .07); }
.page--admin .tcard__student {
  font-size: 13.5px; font-weight: 700; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.page--admin .tcard__tutor {
  margin-top: 2px; font-size: 11.5px; font-weight: 400; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.page--admin .tcard__meter {
  margin-top: 9px; display: flex; align-items: center; gap: 9px;
}
.page--admin .tcard__bar {
  flex: 1; height: 5px; border-radius: 3px;
  background: rgba(27, 67, 50, .1);
  position: relative; overflow: hidden;
}
.page--admin .tcard__count {
  flex-shrink: 0; font-size: 11.5px; font-weight: 700; color: var(--ink);
}
.page--admin .tcard__act { margin-top: 5px; }
.page--admin .tcard__togo { font-size: 11px; color: var(--muted); }
/* The sentence lives at the foot of the card at every width, per both mocks. */
.page--admin .trust .card__blurb { display: none; }
.trust__foot {
  margin: 0; padding: 12px 20px;
  border-top: 1px solid rgba(27, 67, 50, .09);
  font-size: 11.5px; color: var(--muted);
}

/* ── THE SECTION CARD IS A FRAME, NOT A PADDED BOX ──────────────────────────
   Every card in the desktop mock has NO padding of its own: the head band
   carries `14px 20px`, the table rows carry `12px 20px`, and the card supplies
   only the border and the radius. Trust still had the portal's 37px, which put
   its cells 66px from the card edge against the mock's 20 and was most of why
   the section did not read as the same drawing. The earnings card was already
   flush; its head was not. */
.page--admin .trust.card { padding: 0; }
.page--admin .trust .card__head,
.page--admin .pay .card__head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 14px 20px; margin: 0;
  border-bottom: 1px solid rgba(27, 67, 50, .09);
}
/* One row, with the link on the far right, per :286-288 and :460-464. */
.page--admin .trust .card__head .eyebrow,
.page--admin .pay .card__head .eyebrow { margin: 0; }
.page--admin .trust__seeall { margin-left: auto; font-size: 11.5px; font-weight: 600; color: var(--green-btn); }
/* The earnings head keeps its sentence, on the same line as the eyebrow rather
   than as a paragraph beneath it, which is where the mock puts the week range. */
.page--admin .pay .card__blurb {
  margin: 0; flex: 1 1 auto; min-width: 0;
  font-size: 11.5px; color: var(--muted);
}

/* ── 4 · THE SENT LOG FILLS ITS COLUMN AND SITS OFF THE EDGE ────────────────
   Desktop mock :135-153.

   TWO FAULTS, ONE CAUSE. `.ann__cols` was `align-items: start`, so the right
   column was only as tall as its own content and stopped well short of the
   composer beside it, leaving the divider running past the bottom of a pane
   that had already ended. And `.ann__live` carried a single `16px 20px 18px`
   for the whole pane, so the log's own rules and its scrollbar were inset with
   everything else and the list read as clipped against the edge rather than
   running to it.

   The mock builds the pane as a flex COLUMN with min-height 0: a fixed head, a
   flexible capped scroller that owns the remaining height, and a fixed foot.
   Each part carries its own `20px`, so the rules span the pane and the scroller
   reaches its edges. */
.page--admin .ann__cols { align-items: stretch; }
.page--admin .ann__live {
  display: flex; flex-direction: column; min-height: 0;
  padding: 0;
  /* ⚠ THE COLUMN MUST BE TOLD TO FILL ITS ROW. align-items:stretch on the grid
     makes the column as tall as the composer, but the column's own height stays
     `auto` unless this says otherwise, so the flex children inside it had no
     height to divide and the scroller took only its content. That is what left
     the rail stopping short with its last entry sliced through. */
  height: 100%;
}
.page--admin .ann__livehead {
  flex-shrink: 0; margin: 0;
  padding: 14px 20px 10px;
}
.page--admin .ann__livehead--spaced { padding-top: 18px; }
/* ⚠ flex:1 AND min-height:0 TOGETHER. flex:1 alone will not shrink a flex item
   below its content, so the cap would be ignored and the pane would grow. */
.page--admin .ann__log {
  flex: 1; min-height: 0; max-height: 196px;
  /* ⚠ X IS PINNED SHUT ON PURPOSE. With only overflow-y declared, the X axis
     computes to `auto` alongside it, making this a scroll container on BOTH
     axes: it then clips any absolutely positioned descendant, which is the
     documented cause of the admin dropdown incident. Nothing in the log is
     positioned today, and this is what stops the next thing that is. */
  overflow-y: auto; overflow-x: hidden;
  border-top: 1px solid rgba(27, 67, 50, .07);
}
/* ⚠ THE CAP COMES OFF NOW THAT THE COLUMN HAS A HEIGHT. The mock caps the log
   at 196px because in the mock the rail is only as tall as its own content. Ours
   now stretches to the composer's full height, so a 196px cap left dead space
   below the list AND kept a scroll boundary that could cut a row through the
   middle. flex:1 with min-height:0 lets it take exactly the room between the
   fixed head and the fixed foot: the rail fills the card, and the list only
   scrolls when there is genuinely more than fits. */
.page--admin .ann__live .ann__log { max-height: none; }
.page--admin .ann__row { padding: 11px 20px; border-top: 0; border-bottom: 1px solid rgba(27, 67, 50, .06); }
.page--admin .ann__empty { padding: 14px 20px; border-top: 0; }
/* ⚠ THE CAPTION UNDER A GROUP HEADING, AND ITS MODIFIER WAS INERT HERE.
   `.ann__empty--quiet` exists to tighten exactly this line -- it declares
   `padding: 8px 0 0` -- and at (0,1,0) it loses outright to the
   `.page--admin .ann__empty` rule above at (0,2,0). So SENT EARLIER's subtext
   took the full 14px meant for an EMPTY STATE, which is a different thing: an
   empty state is a paragraph standing in for a list, this is a caption
   belonging to the heading directly above it.

   ⚠ AND ONLY THE TOP IS TAKEN BACK. Raising the modifier's own specificity
   instead would have brought `0` side padding with it and pulled the caption
   flush to the card edge, out of line with every row beside it. This is not a
   negative margin and not a nudge: it is the value the modifier always asked
   for, applied at the specificity this page needs. */
.page--admin .ann__empty--quiet { padding-top: 8px; }
.page--admin .ann__foot {
  flex-shrink: 0; margin: 0;
  padding: 11px 20px 16px;
  border-top: 1px solid rgba(27, 67, 50, .07);
}

/* ── THE ROSTER CARDS ARE FRAMES TOO ────────────────────────────────────────
   The mock's roster card opens straight onto its band: card padding 0, band
   `14px 20px` (:161-162). Ours carried the portal's `22px 24px 24px`, which put
   about 90px of empty card above "TUTORS · 7" before the eyebrow even started.

   ⚠ VERTICAL ONLY, AND FOR THE SAME REASON AS EVERYTHING ELSE HERE. The mock
   gets its horizontal inset from padding on the band and the row, which
   admin-roster-edges forbids and Trey ruled stays forbidden. The card keeps
   carrying that inset instead, so the rule is `0 24px` rather than `0`. The
   visible cost is that the band's rule and the row dividers stop 24px short of
   the card edge where the mock runs them the whole way. */
.page--admin section.card--pad:not(.card--flush):not(.ann) { padding: 0 24px; }

/* ── 5 · WEEKLY EARNINGS, THE MOCK'S TABLE ───────────────────────────────────
   Desktop mock :458-505, cell styles resolved at :791-807.

   The mock is a FLEX row with fixed column widths, not a fr grid: TUTOR takes
   the slack and every figure column is a fixed width so the numerals line up
   down the page regardless of how long a name is. Ours was a 2fr/1fr grid of
   flat 13px grey, which is what "flat gray table rows" describes. */
.page--admin .pay__row {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 20px;
  border-top: 0; border-bottom: 1px solid rgba(27, 67, 50, .06);
  font-size: 12.5px; color: var(--muted);
}
.page--admin .pay__who { flex: 1; min-width: 0; gap: 7px; align-items: center; }
.page--admin .pay__name { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.page--admin .pay__trend { font-size: 9px; line-height: 1; }
.page--admin .pay__row > [data-label="SESSIONS"] { width: 88px; flex-shrink: 0; }
.page--admin .pay__row > [data-label="PAID HRS"] {
  width: 70px; flex-shrink: 0; display: flex; flex-direction: column; gap: 1px;
}
.page--admin .pay__row > [data-label="STUDENTS"] { width: 78px; flex-shrink: 0; }
.page--admin .pay__row > [data-label="REPORTS"] { width: 78px; flex-shrink: 0; }
/* ⚠ THE MONEY IS SET IN THE DISPLAY FACE. This is the single biggest reason the
   section read as a plain grey table: every figure was the UI face at one
   weight. Revenue is ink, owed is the deep green, both 15px serif and both
   right aligned so the columns line up on the decimal. */
.page--admin .pay__row > [data-label="REVENUE"],
.page--admin .pay__row > [data-label="OWED"] {
  width: 96px; flex-shrink: 0; text-align: right;
  font-family: var(--serif); font-size: 15px; font-weight: 400;
}
.page--admin .pay__row > [data-label="REVENUE"] { color: var(--ink); }
.page--admin .pay__row > [data-label="OWED"] { color: var(--green-deep); }
.page--admin .pay__taught { margin-top: 0; font-size: 10.5px; color: var(--gold-ink); }
/* Behind on filing: gold and bold. Level: plain. Mock reportStyle at :806. */
.page--admin .pay__short { font-weight: 700; color: var(--gold-ink); }
.page--admin .pay__quiet {
  font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--muted);
}
/* The column band: a real banded header, not bare small caps on the page. */
/* ⚠ THE HEAD BAND'S CELLS ARE BARE SPANS with no data-label, so the width rules
   above miss them entirely and every caption bunched up at the left instead of
   sitting over its column. Reached by position, in the template's own order:
   TUTOR, SESSIONS, PAID HRS, STUDENTS, REPORTS, REVENUE, OWED. */
.page--admin .pay__row--head > :nth-child(1) { flex: 1; min-width: 0; }
.page--admin .pay__row--head > :nth-child(2) { width: 88px; flex-shrink: 0; }
.page--admin .pay__row--head > :nth-child(3) { width: 70px; flex-shrink: 0; }
.page--admin .pay__row--head > :nth-child(4) { width: 78px; flex-shrink: 0; }
.page--admin .pay__row--head > :nth-child(5) { width: 78px; flex-shrink: 0; }
.page--admin .pay__row--head > :nth-child(6),
.page--admin .pay__row--head > :nth-child(7) { width: 96px; flex-shrink: 0; text-align: right; }
.page--admin .pay__row--head {
  padding: 9px 20px;
  background: rgba(27, 67, 50, .03);
  border-bottom: 1px solid rgba(27, 67, 50, .09);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
}
.page--admin .pay__row--total {
  padding: 13px 20px;
  border-top: 0; border-bottom: 0;
  background: rgba(27, 67, 50, .03);
  font-size: 12.5px; font-weight: 600; color: var(--ink);
}
.page--admin .pay__row--total .pay__name { font-size: 12.5px; font-weight: 700; }
.page--admin .pay__row--total > [data-label="REVENUE"],
.page--admin .pay__row--total > [data-label="OWED"] { font-size: 17px; font-weight: 400; }
.page--admin .pay__band {
  padding: 10px 20px;
  border-top: 1px solid rgba(27, 67, 50, .09);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em;
}
.page--admin .pay__staff { padding: 12px 20px; font-size: 12.5px; }

/* ══ THE STATUS CHIPS: TWO ARRANGEMENTS, ONE SET OF CHIPS ═══════════════════
 *
 * Desktop mock (:69-79): all four chips inline in one wrapping row, the
 * auto-send chip last.
 * Mobile mock (:74-86): the auto-send chip FIRST and full width, then the two
 * health chips two up on their own line, then the quiet chip full width.
 *
 * ⚠ display: contents IS WHAT KEEPS THE TWO INDEPENDENT. Above 760px the phone's
 * grouping box is removed from layout altogether, so the desktop row is the same
 * four flex children it always was and no desktop rule is expressed in terms of
 * the phone's grouping. Below it, the box becomes the mock's two-up line.
 *
 * The auto-send chip is moved by `order` rather than by a second copy of the
 * markup, and that is a deliberate call: a duplicate would mean a duplicate
 * POST form and a second trigger for the one confirm dialog. Mechanism was left
 * to me; the arrangement is the mock's either way. */
.achips__pair { display: contents; }

@media (max-width: 760px), (max-height: 499px) {
  .page--admin .achips { flex-direction: column; align-items: stretch; gap: 7px; }
  .page--admin .achips__pair { display: flex; gap: 6px; }
  /* Two up, each taking half and neither pushing the other off. */
  .page--admin .achips__pair > .achip { flex: 1; min-width: 0; }
  /* First on the phone, last on the desktop, one declaration. */
  .page--admin .achip--act { order: -1; }
}

/* ══ WEEKLY EARNINGS ON A PHONE: A LIST, NOT THE TABLE STACKED ══════════════
 *
 * Admin Tools Mobile.dc.html:336-358, data mapping at 825-846.
 *
 * ⚠ THE TABLE'S data-label CAPTIONS ARE THE BUG THIS REPLACES. Stacked at
 * 390 a quiet row read "SESSIONS NO SESSIONS": a column heading and its cell,
 * printed as if they were a sentence. Scaling the type could never fix that,
 * because the component was the desktop table and not the mock's row.
 *
 * Same swap discipline as .tlist: by class in a media query, never `hidden`,
 * because an author origin `display` beats [hidden]{display:none} and both
 * structures would paint. See the .roster__row incident in CLAUDE.md.
 */
.paylist { display: none; }

.paylist__row, .paylist__total {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 13px 15px;
  border-bottom: 1px solid rgba(27, 67, 50, .06);
}
.paylist__total { background: rgba(27, 67, 50, .03); border-bottom: 0; }
.paylist__main { flex: 1; min-width: 0; }
.paylist__top { display: flex; align-items: center; gap: 6px; min-width: 0; }
.paylist__name {
  flex-shrink: 1; min-width: 0;
  font-size: 13.5px; font-weight: 700; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.paylist__trend { flex-shrink: 0; font-size: 9px; line-height: 1; }
.paylist__trend--up { color: #1B7F4B; }
.paylist__trend--down { color: var(--gold-ink); }
.paylist__delta { flex-shrink: 0; font-size: 10.5px; color: var(--muted); white-space: nowrap; }
.paylist__meta {
  margin-top: 3px; font-size: 11px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.paylist__reports { margin-top: 2px; font-size: 11px; font-weight: 400; color: var(--muted); }
/* Short filings are the one thing on this row worth chasing, so they are the
   one thing drawn in gold. */
.paylist__reports--short { font-weight: 700; color: var(--gold-ink); }
/* ⚠ THE META LINE, NOT A VALUE UNDER A CAPTION. This is the whole difference
   between the phone list and the stacked table cell it replaces. */
.paylist__none {
  margin-top: 3px;
  font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--muted);
}
.paylist__money { flex-shrink: 0; text-align: right; }
.paylist__owed {
  font-family: var(--serif); font-size: 19px; line-height: 1.1; color: var(--green-deep);
}
.paylist__hours { margin-top: 2px; font-size: 10.5px; color: var(--muted); }
.paylist__held { font-size: 10px; color: var(--gold-ink); }
.paylist__rev { margin-top: 3px; font-size: 10.5px; color: var(--muted); }
.paylist__tcount { font-size: 12.5px; font-weight: 700; color: var(--ink); }
.paylist__towed {
  font-family: var(--serif); font-size: 21px; line-height: 1.1; color: var(--green-deep);
}

@media (max-width: 760px), (max-height: 499px) {
  /* .pay__row--head and .pay__row--total both carry .pay__row, so this covers
     the whole table including its heading strip. */
  .page--admin .pay__row { display: none; }
  .page--admin .paylist { display: block; }
}

/* ══ THE ROSTER CARDS ON A PHONE ═════════════════════════════════════════════
   ⚠ LAST IN THE FILE ON PURPOSE. A media query adds NO specificity, so these
   have to beat the base column rules written above them by POSITION. Written
   any earlier they would lose to `.page--admin .rlist--cols3 .roster__row`
   however correct they were, which is the exact trap scripts/audit-cascade.js
   exists to catch. */
@media (max-width: 760px), (max-height: 499px) {
  /* ⚠ THE DISCLOSURE IS DESKTOP ONLY. "Show deactivated" and "Show archived"
     do not render on a phone at all, by ruling. Hidden rather than removed from
     the markup, so the desktop keeps it untouched and the records it reaches
     are unchanged. */
  .page--admin .roster__showarch { display: none; }

  /* ⚠ AND THE ADD BUTTON HAS TO TAKE OVER THE PUSH THE DISCLOSURE USED TO DO.
     This is the one thing hiding that link broke, and it is worth naming
     exactly because it is not obvious from either rule on its own.

     ON THE DESKTOP the disclosure carries `margin-left: auto` and IT is what
     eats the band's free space, carrying itself and everything after it to the
     right edge. The add button's own auto margin would fight that, so
     `.page--admin .roster__showarch + .amodal__open` zeroes it: the button sits
     snug beside the link rather than pushing away from it.

     `display: none` does NOT remove the element from the DOM, so that adjacent
     sibling selector still matched here and still zeroed the button. But a
     hidden element generates no box and contributes no margin, so the auto
     margin that HAD been doing the pushing was gone and the button had nothing
     of its own left. It collapsed left, hard against the count label. Nothing
     moved the button; what moved was the only thing holding it out.

     Giving it back its own auto margin is what holds it right now. Same
     selector as the desktop rule so the specificity is identical and position
     decides, which is why this block sits where it does in the file. */
  .page--admin .roster__showarch + .amodal__open { margin-left: auto; }

  /* ⚠ 14px ABOVE THE ADD BUTTON, MEASURED RATHER THAN CHOSEN. The reference is
     the SETTINGS card header: .sstrip__head computes padding-top: 14px on the
     live page at 390, and its text sits 1px below its card's top edge, so 14px
     IS the space above that header.
   *
   * WHAT THE SPACE ACTUALLY IS HERE. The add button is not below the filter: it
   * carries margin-left:auto and sits on the band's FIRST line, to the right of
   * the "TUTORS · N" label, with the filter wrapping beneath both. Measured at
   * 390 the band ran 1133-1228 with the button at 1137, four pixels inside it.
   * So the room above the button is the BAND's own top margin, which was 24px,
   * and that is the number that was too thick.
   *
   * Both cards take it, which is items 2d and 3c: they are the same band rule. */
  .page--admin .rgroup > .roster__toggle { margin-top: 14px; }

  /* ⚠ EVERY ROW IS ENCLOSED ON ALL FOUR SIDES. The rows carried a right border
     and no left one, so the card's outline never closed down its left edge.
   *
   * ⚠ AND THE COLUMN RULE MUST NOT REACH A ONE-COLUMN GRID, which is the cause
   * of the row-specific exclusion that was reported. The base rule clears the
   * border on the LAST COLUMN of each band with
   * :nth-child(3n of .roster__row) / (2n of .roster__row). On a phone the grid
   * collapses to a single column, so "every third cell" becomes "every third
   * ROW": measured at 390, Barbro Checkett was row 3 and Caroline Pousson row 6
   * and both lost their right border, while the students card lost every second
   * row's. Nothing about Barbro was special; she was simply third.
   *
   * Restated for ALL rows here, the nth-child ones included, so the treatment is
   * identical down the whole list. */
  .page--admin .rlist--cols3 .roster__row,
  .page--admin .rlist--cols2 .roster__row,
  .page--admin .rlist--cols3 .roster__row:nth-child(3n of .roster__row),
  .page--admin .rlist--cols2 .roster__row:nth-child(2n of .roster__row) {
    border-left: 1px solid rgba(27, 67, 50, .07);
    border-right: 1px solid rgba(27, 67, 50, .07);
  }
}

/* ⚠ AHEAD OF THE OVERLAY SECTION, NOT AT THE END OF THE FILE. That section
   runs to the last line and test/survey-submitting asserts every selector
   inside it is the overlay's own, so a block appended after it reads as a
   stray overlay rule. Anywhere below the base column rules wins by position,
   which is all these need. */

/* ══ THE STUDENT'S SUBJECT, FROM THE LIST ════════════════════════════════════
   design_handoff_tutor_subjects 1h. Inside the tutor group on Add student and
   Edit student, because the subject belongs to the PAIRING and not to the
   child.

   ⚠ NOTHING HERE HIDES THE <select>. The script hides it and reveals the field
   that replaces it; a rule doing that would hide it whether or not the script
   ran, and a control with no replacement is how a form becomes unusable with
   scripting off. */
/* ⚠ THE ANCHOR. The results below are taken OUT OF FLOW and hung off this box,
   which is the whole of the fix for the fault Trey reported: in flow, the list
   was real height inside a `column-count: 2` form, so seven results grew the
   left column and FAMILY TIMEZONE jumped into the right one, and one result
   moved it back. Content was migrating between columns as somebody typed. Out
   of flow the list contributes no height at all, so the form's layout is
   byte-identical with the list open, closed and at every length.

   ⚠ THE MULTICOL THAT CAUSED THAT IS GONE, and this rule STAYS. Two real grid
   columns cannot migrate content between them, so the original fault cannot
   recur; but an in-flow list would still push every field below it down its own
   column as somebody typed, which is the same complaint one layer quieter. */
.ssub { position: relative; margin-top: 6px; }

.ssub__field { display: block; }
.ssub__field[hidden] { display: none; }

/* 16px, the same floor every field on these forms keeps: below it iOS zooms the
   whole form on focus. box-shadow is named because the global `input:focus` at
   (0,1,1) paints a 3px gold ring, and it loses to this (0,2,0) only where this
   block says so -- unnamed, it draws a second focus state inside a box that
   already has its own border. */
.ssub__in {
  width: 100%; min-height: 48px; box-sizing: border-box;
  font-size: 16px;
}
.ssub__in:focus { box-shadow: none; border-color: var(--green-deep); }

/* ⚠ FLOATING, ANCHORED, AND CAPPED. Shared with the no-match block below so the
   two cannot drift: whichever is showing occupies the same box.

   z-index 30 rather than a stacking context on .ssub, deliberately. .ssub is
   `position: relative` with `z-index: auto`, so it does NOT create one and this
   list joins the dialog's own context -- which is what lets it paint over the
   fields after it AND over the next column. Giving .ssub a z-index would trap
   the list inside it and put it back underneath.

   CAPPED AND SCROLLABLE rather than growing: sixteen subjects is 700px of list,
   and a dropdown that runs off the sheet is the same complaint in a new place.
   Six rows, then it scrolls, with the scroll contained so a flick at the bottom
   does not carry on into the form behind it. */
.ssub__results, .ssub__none {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 30;
  margin-top: 5px;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 4px;
  box-shadow: 0 10px 28px rgba(20,55,42,.18);
}
.ssub__results {
  max-height: 264px;
  overflow: hidden auto;
  overscroll-behavior: contain;
}
.ssub__results[hidden] { display: none; }

/* 44px, because every row here is a tap target. */
.ssub__hit {
  display: flex; align-items: center; gap: 8px;
  width: 100%; min-height: 44px; box-sizing: border-box;
  padding: 0 13px; margin: 0;
  border: 0; border-bottom: 1px solid rgba(27,67,50,.07);
  background: transparent; cursor: pointer; text-align: left;
  font-family: inherit; font-size: 14px; color: var(--ink);
}
.ssub__hit:last-child { border-bottom: 0; }
.ssub__hit:hover { background: rgba(201,162,39,.1); }
.ssub__hitname { flex: 1; min-width: 0; }

/* ⚠ THE MARK IS A NOTE AND NEVER A GATE. A subject the chosen tutor does not
   teach is offered exactly the same way, with this simply absent: refusing it
   would block a tutor about to be certified, or a gap covered deliberately.
   Green because it is a reassurance rather than a warning, and readable rather
   than decorative at 9px. */
.ssub__teaches {
  flex-shrink: 0;
  font-size: 9px; font-weight: 700; letter-spacing: .08em; color: #155F39;
}

/* ⚠ THE REFUSAL, AND IT IS THE POINT OF THE CONTROL. Typing something off the
   list offers nothing and says where a subject is added instead. No red: this
   is a rule being stated, not an error. */
.ssub__none { padding: 13px 14px; }
.ssub__none[hidden] { display: none; }
.ssub__nonetitle { font-size: 13px; font-weight: 600; color: var(--ink); }
.ssub__nonep { margin: 3px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); }

/* One subject per pairing, said once under the first row rather than on each. */
.ssub__note { margin-top: 8px; }

/* ⚠ EVERYTHING BELOW THE OVERLAY MARKER BELONGS TO THE OVERLAY, which is why
   these two blocks sit ABOVE it rather than at the end of the file.
   test/survey-submitting.js reads that section as "the marker to EOF" and
   requires every selector in it to be overlay-scoped, so an appended block
   is not a tidiness question: it fails a real rule about what that section
   is allowed to style. The roster block was moved off the end for the same
   reason one commit earlier. */

/* ══ THE SUBJECT LIST, IN SETTINGS ═══════════════════════════════════════════
   design_handoff_tutor_subjects 1e. Inside the Settings modal, beside the
   standard rate and the school year.

   ⚠ EVERY ROW IS A <details>, AND THE MENU IS ITS OPEN STATE. Four actions, two
   of which need a text field, so a floating popover would have to reproduce a
   form anyway. A disclosure needs no script at all, which is the same bar the
   rest of this page clears: `script-src-attr 'none'` refuses an inline handler
   outright, and this works with JavaScript off. */
.subj__list { margin-top: 12px; }

.subj__row { border-top: 1px solid rgba(27,67,50,.07); }
.subj__row:last-child { border-bottom: 1px solid rgba(27,67,50,.07); }

/* THE ROW ITSELF. 44px minimum, because opening the menu is a tap target. The
   marker is suppressed in both spellings: the disclosure triangle is drawn as
   the mock's own ellipsis at the end of the row instead. */
.subj__sum {
  min-height: 44px; box-sizing: border-box;
  display: flex; align-items: center; gap: 12px; padding: 11px 2px;
  cursor: pointer; list-style: none;
}
.subj__sum::-webkit-details-marker { display: none; }
.subj__sum::marker { content: ''; }
.subj__name { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; color: var(--ink); }
.subj__use { flex-shrink: 0; font-size: 11.5px; color: var(--muted); }
.subj__dots {
  flex-shrink: 0; width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; color: var(--muted);
}

/* ⚠ AN ARCHIVED ROW IS NOT AN ERROR STATE, and the only thing that changes is
   that new records cannot pick it. Greyed on THIS screen, which is the list of
   what is offered; on a record that holds it, it renders in the same ink as any
   other subject and nothing here reaches that. */
.subj__row--arch .subj__name { font-weight: 400; color: var(--faint); }
.subj__arch {
  flex-shrink: 0; display: inline-flex; align-items: center;
  padding: 3px 9px; border-radius: 100px;
  background: rgba(27,67,50,.06); border: 1px solid rgba(27,67,50,.16);
  font-size: 9px; font-weight: 700; letter-spacing: .08em; color: var(--muted);
}

/* THE MENU. The mock's own panel, opened in place under the row it belongs to
   rather than floating over the ones beneath it. */
.subj__menu {
  margin: 0 0 12px; padding: 12px 14px;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 5px;
}
.subj__mhead {
  margin: -12px -14px 12px; padding: 10px 14px;
  background: rgba(27,67,50,.03);
  border-bottom: 1px solid rgba(27,67,50,.09);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--muted);
}
.subj__actrow { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.subj__actrow .in { flex: 1 1 auto; min-width: 0; }

.subj__ends {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  padding-top: 12px; border-top: 1px solid rgba(27,67,50,.07);
}
.subj__arcbtn, .subj__delbtn {
  min-height: 44px; padding: 0 14px;
  border: 1px solid rgba(27,67,50,.2); border-radius: 4px;
  background: var(--off-white); cursor: pointer;
  font-family: inherit; font-size: 13.5px; color: var(--gold-ink);
}
/* ⚠ DEAD, NOT HIDDEN, WHEN RECORDS POINT AT IT. The row already said what the
   subject costs; a button that vanishes leaves an admin wondering where it
   went, and the sentence below the pair says which number to change. The
   service refuses it the same way whatever posts, so this is the courtesy and
   not the guard. */
.subj__delbtn { color: var(--faint); }
.subj__delbtn[disabled] { cursor: default; opacity: 1; }
.subj__delbtn--live { color: var(--ink); }

/* THE REFUSAL, beside the button it explains rather than as a band under the
   whole menu: the row above already stated the count, so this only has to say
   which number has to change. Gold ink, the colour this page uses for a state
   worth reading. No red anywhere -- nothing here is an error, it is a rule. */
.subj__why { font-size: 11.5px; line-height: 1.6; color: var(--gold-ink); }

/* A name wants more room across the row than a date does. */
.subj__add { flex: 1 1 200px; min-width: 140px; }

@media (max-width: 720px) {
  /* ⚠ THE BASIS-BECOMES-HEIGHT TRAP, which this file already records twice at
     .tagform__date and .tagform__row .money. Once .tagform__row turns into a
     column, `flex: 1 1 200px` is read as 200px TALL and the field renders as an
     empty box with its text floating in the middle. Returned to the content's
     own height, at the same specificity, after the rule it undoes. */
  .subj__add { flex: 0 0 auto; width: 100%; }
  .subj__actrow { flex-direction: column; align-items: stretch; }
  .subj__actrow .sreq__go { min-height: 44px; width: 100%; }
}

/* ══ SUBJECTS THEY TEACH, THE TYPEAHEAD ══════════════════════════════════════
   design_handoff_tutor_subjects 1d. On Add tutor and Edit tutor, above the
   DIRECTORY PROFILE divider: everything under that divider is optional
   decoration shown in the Rolodex, and subjects are not decoration.

   ⚠ THE <select multiple> IS THE CONTROL AND THE SCRIPT HIDES IT. Nothing here
   hides it: a stylesheet rule would hide it whether or not the script ever ran,
   and a control with no replacement is how a form becomes unusable with
   JavaScript off. Without the script the select is simply visible and works. */
.tsub {
  margin-top: 6px; padding: 16px;
  background: var(--cream);
  border: 1px solid rgba(27,67,50,.18); border-radius: 5px;
}
.tsub__select[hidden] { display: none; }
.tsub__count {
  margin-left: 8px;
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  text-transform: none; color: var(--gold-ink);
}

/* ⚠ THE TAGS ARE ABOVE THE INPUT AND THAT IS WHY THIS BEATS A DROPDOWN. A bare
   picker showing nothing until it is opened means an admin cannot see what a
   tutor teaches while editing them. */
.tsub__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 9px; }
.tsub__tags[hidden] { display: none; }
.tsub__tag {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 0 6px 0 11px; border-radius: 100px;
  background: var(--green-deep);
  font-size: 12.5px; font-weight: 600; color: var(--off-white);
}
.tsub__x {
  flex-shrink: 0; width: 20px; height: 20px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: rgba(255,255,255,.14);
  font-family: inherit; font-size: 11px; line-height: 1;
  color: var(--off-white); cursor: pointer;
}

.tsub__field { display: block; }
.tsub__field[hidden] { display: none; }

/* ⚠ 16px, AND IT IS NOT A TYPOGRAPHIC CHOICE. Below 16 iOS zooms the whole form
   on focus. The same floor every other field on this page keeps.

   AND box-shadow: none, WHICH IS LOAD-BEARING. The global form rule at the top
   of this file gives `input:focus` a 3px gold glow at (0,1,1); it loses to this
   (0,2,0) only where this block names the property, and unnamed it paints a
   second focus ring inside a box that already has its own border. */
.tsub__in {
  width: 100%; min-height: 48px; box-sizing: border-box;
  margin: 0; padding: 0 13px;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.22); border-radius: 4px;
  font-family: inherit; font-size: 16px; color: var(--ink);
}
.tsub__in::placeholder { color: var(--faint); opacity: 1; }
.tsub__in:focus {
  outline: none; box-shadow: none;
  border-color: var(--gold);
}

.tsub__results {
  margin-top: 5px;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 4px;
  overflow: hidden;
}
.tsub__results[hidden] { display: none; }

/* 44px, because every row here is a tap target. */
.tsub__hit {
  display: flex; align-items: center;
  width: 100%; min-height: 44px; box-sizing: border-box;
  padding: 0 13px; margin: 0;
  border: 0; border-bottom: 1px solid rgba(27,67,50,.07);
  background: transparent; cursor: pointer; text-align: left;
  font-family: inherit; font-size: 14px; color: var(--ink);
}
.tsub__hit:last-child { border-bottom: 0; }
.tsub__hit:hover { background: rgba(27,67,50,.04); }

/* ⚠ THE REFUSAL, AND IT IS THE POINT OF THE WHOLE CONTROL. Typing something off
   the list offers nothing and says where a subject is added instead. No red:
   this is not an error, it is the rule being stated. */
.tsub__none {
  margin-top: 5px; padding: 13px 14px;
  background: var(--off-white);
  border: 1px solid rgba(27,67,50,.18); border-radius: 4px;
}
.tsub__none[hidden] { display: none; }
.tsub__nonetitle { font-size: 13px; font-weight: 600; color: var(--ink); }
.tsub__nonep {
  margin: 3px 0 0;
  font-size: 11.5px; line-height: 1.5; color: var(--muted);
}


/* ══ THE SETTINGS STRIP ON A DESKTOP ═════════════════════════════════════════
   Five cells, each a centred stack: the title, then the value or the toggle,
   then the subtext.

   ⚠ THE QUERY IS THE EXACT COMPLEMENT OF THE PHONE'S, and that is the whole of
   the mobile guarantee. The phone block reads
   `(max-width: 760px), (max-height: 499px)`, a comma, which is OR. Its negation
   is therefore an AND of both floors, not a single min-width: a short landscape
   window at 900px wide is MOBILE by that rule, and a min-width query alone
   would have reached it. Nothing in here can render below either floor.

   ⚠ AND IT SITS AHEAD OF THE OVERLAY SECTION rather than at the end of the
   file, for the reason the roster block above gives: test/survey-submitting
   reads everything after the overlay banner as the overlay's own. */
@media (min-width: 761px) and (min-height: 500px) {
  /* ONE ROW OF FIVE, NOT A WRAPPING BAND. As a flex row with
     `flex-wrap: wrap` and a 240px floor the five broke into two lines below
     about 1200px, and two lines is what makes the strip read as ragged blocks:
     equal heights only ever apply WITHIN a flex line. A five-column grid is one
     row at every desktop width, and grid stretches its items to the tallest by
     default, which is the equal-height requirement met by the layout rather
     than by a number somebody has to maintain. */
  .page--admin .sstrip__row {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: auto auto auto;
  }

  /* ⚠ `display: contents` IS DOING THE STRUCTURAL WORK. The value is a SIBLING
     of .sstrip__text while the title and the subtext are its CHILDREN, so no
     amount of ordering on the cell alone can put the value between them. Taking
     the wrapper's box away promotes both lines to children of the cell, and
     then all three are orderable against each other. */
  /* ⚠ SUBGRID, SO THE THREE LINES ALIGN ACROSS ALL FIVE CELLS AND NOT MERELY
     WITHIN EACH. Five independent stacks are five different heights -- the
     serif rate is 29px tall, a switch is 24 and a plain value is 20 -- so
     centring each one in its own cell still leaves the titles on five
     different baselines, which is the ragged reading the ruling is against.
     Sharing the row's tracks puts every title, every value and every subtext
     on one line across the strip.

     IT DEGRADES SAFELY. An engine without subgrid drops the declaration as
     invalid and the cell stays an ordinary auto-row grid, which still stacks
     the three in order; only the cross-cell alignment is lost. */
  .page--admin .sstrip__cell {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    justify-items: center; align-content: start;
    text-align: center; gap: 7px;
    padding: 18px 14px;
  }
  .page--admin .sstrip__text { display: contents; }
  .page--admin .sstrip__name { order: 1; }
  .page--admin .sstrip__money,
  .page--admin .sstrip__value,
  .page--admin .sstrip__sw { order: 2; }
  .page--admin .sstrip__note { order: 3; }

  /* ⚠ NOTHING TRUNCATES, AND THE MECHANISM IS REMOVED RATHER THAN OUTSIZED.
     .sstrip__name and .sstrip__note both carried the three-declaration ellipsis
     kit -- white-space: nowrap, overflow: hidden, text-overflow: ellipsis --
     which is what rendered "Standard ..." and "Sch...". All three are undone
     here; widening the cell instead would only move the width at which it
     happens. A line that does not fit WRAPS and the cell grows, and because the
     row is a grid every other cell grows with it. */
  .page--admin .sstrip__name,
  .page--admin .sstrip__note {
    white-space: normal; overflow: visible; text-overflow: clip;
    max-width: 100%;
  }
  /* The subtext is the only line that reads as a sentence, so it gets a
     measure. The title and the value are short enough to wrap on their own. */
  .page--admin .sstrip__note { line-height: 1.4; }

  /* ⚠ THE CHEVRON GOES AND NOTHING REPLACES IT. All three cells that carried
     one are already <button class="sstrip__cell">, so the whole cell was
     ALWAYS the tap target and the chevron was only ever saying so. Dropping it
     removes a glyph, not a door: no wiring changes and each still opens the
     dialog its data-amodal-open names. */
  .page--admin .sstrip__chev { display: none; }
}

/* ══ THE CALENDAR'S TIMEZONE LINE ════════════════════════════════════════════
   design_handoff_calendar_timezone. One muted caption naming the zone the
   calendar is drawn in, on every view, for every viewer.

   ⚠ IT IS DELIBERATELY NOT THE EYEBROW'S TREATMENT. The eyebrow is 600 weight,
   .12em tracked and --gold-ink, and on this page gold is the colour that asks
   for attention. This is a fact, not a warning: title case beside small caps is
   what makes it read as an aside rather than as a second label. */

/* ⚠ .cal__headzone, NOT .cal__zone, AND THE NAME MATTERS TWICE. A dead
   `.cal__zone` rule still sits above -- A3 removed its markup from every view
   and only the declaration survives -- and reusing the name silently inherited
   its `margin: 10px 0 0`, which is what made the phone cost 24px instead of 14.
   Worse, test/calendar-page slices a page at `lastIndexOf('cal__zone')`: with
   no such class the slice ran to the end of the document, and an element near
   the TOP of the page would have cut that block to nothing and made two
   assertions pass against an empty string. The suffixed names are chosen so
   none of them contains `cal__zone` as a substring either.

   The dead rule is left where it is. Its comment is part of the A3 trail. */

/* ── PHONE: A THIRD LINE, UNDER THE HEADING ───────────────────────────────────
   It cannot ride the eyebrow at 390. The three word pills take about 174px of
   the header row, leaving roughly 170 for the text block, and the eyebrow plus
   the label and the zone needs about 285 at 10px. It would wrap, which costs
   the same height as its own line and looks like a fault.

   ⚠ THE WRAPPER GOES AWAY HERE so its children become items of .cal__headtext
   and the zone can be ordered below the heading. That is what lets the markup
   print the line ONCE and leave the presentation to the stylesheet, the same
   pattern .cal__toggle already uses for its two label forms. */
.cal__headtext { display: flex; flex-direction: column; }
.cal__eyebrowline { display: contents; }
/* ⚠ line-height 1.4 AND NO MARGIN, WHICH IS WHAT MAKES THE COST 14px. The
   brief budgets about 14 for this line and the default leading plus a 3px
   margin measured 18. 10px at 1.4 is 14 exactly, and the heading's own 1.2
   leading already leaves air above it, so nothing reads cramped. */
.cal__headzone { order: 1; line-height: 1.4; }

/* ⚠ nowrap, BECAUSE .cal__headtext IS min-width: 0 AND THE HEADING CAN BE LONG.
   The zone line must never be the thing that forces the row wider; if anything
   has to give it is the heading, which is where the ellipsis already lives. */
.cal__headzone {
  white-space: nowrap;
  font-size: 10px; font-weight: 500; letter-spacing: 0; color: var(--muted);
}
.cal__headzonename { font-weight: 600; color: var(--ink); }
/* Quieter than either thing it separates. The character is the middot the lane
   header already uses; it is NOT a dash, which is banned on every surface. */
.cal__headzonedot { color: rgba(27, 67, 50, .28); }
/* ⚠ AND ON A PHONE THERE IS NOTHING TO SEPARATE, SO IT GOES. The middot is a
   SEPARATOR between the eyebrow and this caption, which is what it does on the
   desktop line. Below the breakpoint the caption is its own line and the
   character sits at the head of it with nothing on its left, reading as a
   stray mark rather than as punctuation. Caught in the render, not in the
   markup: the README draws the phone line as "Calendar Time Zone: Arizona".

   Hidden rather than dropped from the template, because the element is printed
   ONCE and the stylesheet is what chooses the presentation. */
.cal__headzonedot { display: none; }

/* ── DESKTOP: UNDER THE HEADING TOO, AND THE ROW MUST NOT NOTICE ──────────
   ⚠ RULED BY TREY ON SEPT 12th, 2026, OVERRIDING THE BUNDLE'S DESKTOP
   PLACEMENT. It used to ride the eyebrow here: the wrapper took a box again and
   made one line of the eyebrow and the zone. It now sits under the heading on
   both widths, which is what the phone already did, so the wrapper stays
   display: contents everywhere and the zone stays order: 1. There is no desktop
   override of the placement left to keep in step.

   ⚠ AND THE RIGHT-HAND CONTROLS MAY NOT MOVE. Not down, not up, not sideways.
   That is the hard constraint on this change, and an ordinary third line breaks
   it: MEASURED at 1280, .cal__head runs 60 to 138 and its height is set by
   .cal__headtext at 54, not by .cal__actions at 38, which is flex-end inside
   it. A caption in flow takes the text block to 68, the row to 92, and every
   control down by 14px.

   So the caption is taken OUT of flow and the row is given back exactly the
   space it would have taken:

     position: absolute on the caption, so .cal__headtext still measures 54 and
     the row still measures 78. top: 100% puts it immediately under the text
     block; left: 0 is the heading's own left edge, which is what "left aligned
     with the heading" means and is why the offset parent is .cal__headtext
     rather than the row.

     padding-bottom on .cal__head reserves the line the caption now occupies.
     Padding sits OUTSIDE the content box, so the box the controls are
     bottom-aligned to still ends at 138 and they do not move; what moves is
     everything BELOW the header, down by one caption, which is the honest cost
     of adding a line and is not what the constraint protects.

   ⚠ THE 14px IS THE CAPTION'S OWN HEIGHT, not a guess: 10px at line-height 1.4.
   If either changes this number changes with it, which is why they are written
   next to each other. test/calendar-zone-caption pins the controls' coordinates
   so a drift fails rather than ships.

   ⚠ THE LONGHAND IS DELIBERATE AND HAS TO COME AFTER. The desktop .cal__head
   above sets the padding SHORTHAND, which would reset this; a later longhand at
   equal specificity wins on source order. Same cascade fact this file has been
   bitten by twice, used on purpose here. */
@media (min-width: 761px) and (min-height: 500px) {
  .cal__headtext { position: relative; }
  .cal__headzone {
    position: absolute; top: 100%; left: 0;
    /* Out of flow it no longer shrinks with the row, and the heading beside it
       is nowrap, so nothing here can force the row wider either. */
    margin: 0;
  }
  .cal__head { padding-bottom: 14px; }

  /* ⚠ AND THE SEPARATOR STAYS HIDDEN HERE TOO, WHICH REVERSES A RULE THAT
     LANDED HOURS AGO AND IS NOT A DISAGREEMENT WITH IT.

     67786f9 hid the middot below the breakpoint and 5b515c8 pinned it at both
     widths, on this reasoning, quoted from the rule this replaces: "It has an
     eyebrow to its left again, so it is punctuation again." That was exactly
     right while the caption rode the eyebrow line on a desktop.

     Trey's ruling of Sept 12th moved the caption under the heading on THIS
     width too. There is no eyebrow to its left any more, on either width, so
     the character would sit at the head of its own line with nothing on its
     left: the stray mark 67786f9 describes, which is the condition it was
     written to prevent. Applying its rule is what removes its override.

     So the base `display: none` now holds everywhere and this block says why
     rather than silently dropping a line somebody wrote on purpose. */
}

/* ══ THE SECOND CLOCK ════════════════════════════════════════════════════════
   design_handoff_second_clock. What the family's clock reads, under the TIME
   picker on the New session sheet.

   ⚠ A GOLD PILL, AND THE PILL IS THE CEILING. Four weights were drawn and two
   rejected: plain gold ink at 600 read as a caption and got skimmed, and a deep
   green plate with gold text outweighed the picker it sat under, which on a
   sheet whose loudest element must be the BOOK button made it the second heavy
   thing. Anything heavier than this competes with the primary action.

   It is the same shape DEFAULT and HELD already use, so it reads as a fact
   about this record rather than as a caption. */
/* ⚠ THE SAME INSET AS THE CONTROL ROW, AND BY THE SAME TWO RULES. The pills sit
   BELOW .sheet2__three rather than inside its TIME cell, so they do not inherit
   that row's `padding: 18px 20px 0` and landed hard against the sheet's left
   edge on a phone while the picker sat 20px in. Measured: pill left 0, picker
   left 20. The desktop rule below zeroes the sides for both, exactly as it
   already does for the row, so the two stay aligned at both widths.

   ⚠ THE WRAPPER IS ONLY PRINTED WHEN THERE IS A PILL. An empty one would be a
   reserved row, which is the one thing the absence signal cannot survive. */
.sheet2__secondrow { padding: 0 20px; }
.sheet2__second {
  display: inline-block;
  margin-top: 9px;
  padding: 5px 12px;
  border-radius: 100px;
  background: rgba(201, 162, 39, .18);
  border: 1px solid rgba(168, 132, 42, .4);
  font-size: 12.5px; font-weight: 700; line-height: 1.4; color: var(--gold-ink);
  /* ⚠ ONE LINE, ALWAYS. The phrase is a sentence about a person and breaking
     it mid-way reads as two facts; with the 100px radius a wrapped pill also
     stops being a pill and becomes an ellipse. */
  white-space: nowrap;
}
/* ⚠ NO RESERVED HEIGHT WHEN ABSENT, WHICH IS WHY THERE IS NO RULE FOR IT. The
   template prints nothing when the zones match, so there is no empty box to
   collapse and no min-height to remember. The absence IS the signal.

   AND NO ANIMATION. The line appearing is information, not an event. */

/* A group's pills sit side by side with air between them and wrap as a unit if
   the sheet is narrow: each names its own student, so two running together
   would read as one phrase. */
.sheet2__second { margin-right: 6px; }

/* ⚠ THE TUTOR'S OWN ZONE, ADMIN ONLY. One word after the name, no separator,
   quieter than the name it follows: it is context for the picker, not a second
   value to read. The role gate is the enclosing field, which only renders for
   an admin. */
.sheet2__tzone { font-size: 11.5px; color: var(--muted); }
/* ⚠ THE ZONE WORD SITS BESIDE THE NAME, AND A MODIFIER IS WHAT MAKES THAT
   POSSIBLE WITHOUT BREAKING THE STUDENT ROW.
 *
 * WHY IT RENDERED UNDERNEATH: .sheet2__name is `display: block`, so it takes a
 * full line and pushes any inline sibling onto the next one. That is a defect
 * in how the word was added, not a width fallback: it wrapped at every size,
 * including a 1280px desktop with "Trey Checkett" in the field and hundreds of
 * pixels to spare.
 *
 * ⚠ AND .sheet2__who CANNOT SIMPLY BECOME A ROW, because the STUDENT field
 * uses the same wrapper and stacks the name over the subject on purpose. The
 * modifier is applied to the tutor's copy only.
 *
 * Baseline rather than centre, so an 11.5px word sits on the same line as a
 * 14.5px name rather than floating against its box. min-width: 0 on the name so
 * a long one ellipses rather than pushing the word out of the field. */
.sheet2__who--inline { display: flex; align-items: baseline; gap: 8px; }
.sheet2__who--inline .sheet2__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet2__who--inline .sheet2__tzone { flex-shrink: 0; }

/* ══ PAY HISTORY AND TRENDS ════════════════════════════════════════════════
   design_handoff_pay_history. The week arrows on the Pay list, one tutor's
   week, and the Over time screen.

   ⚠ AHEAD OF THE OVERLAY SECTION, NOT AT THE FOOT OF THE FILE.
   test/survey-submitting asserts every selector below that marker is overlay
   scoped, so appending there is what breaks a guard that means what it says. */

/* ── THE WEEK, WITH ARROWS ──────────────────────────────────────────────── */

/* Payout Friday rides the eyebrow's own row, right aligned, so it qualifies the
   figures rather than captioning the page. Its duplicate in the sticky header
   is gone; see the RAIL array's note in admin.ejs. */
/* ⚠ flex-basis 100%, OR NEITHER OF THESE ROWS IS A ROW. The pay card's head is
   itself a wrapping flex container (.page--admin .pay .card__head), so a child
   of it is a flex ITEM and shrinks to its content: measured before this, the
   eyebrow row was 211px inside a 336px head, which is why Payout Friday read as
   sitting inline after the heading instead of against the right edge, and why
   the forward arrow floated mid row instead of at the wall. Filling the line is
   what makes space-between and the centred label mean anything. */
.pay__eyebrowrow {
  flex: 1 1 100%;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}
.pay__payout {
  flex-shrink: 0;
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em; color: var(--gold-ink);
}

/* ⚠ THE ARROWS REUSE .cal__warrow--wide RATHER THAN REDECLARING 44 BY 30.
   That size carries a recorded exception in SPEC.md for its 30px height, below
   the 44px floor, and the brief is explicit: reuse the exception, do not write a
   second one and do not ship them narrower. So the geometry lives in exactly one
   rule and this block only has to put them on screen here.

   The calendar hides .cal__warrow above its breakpoint because its own header
   changes shape there. The Pay card keeps the same control at every width: a
   past week has to be reachable on a desktop too, and there is no second
   treatment to switch to. */
/* Arrows at the two edges with the label centred between them: the label takes
   the free space, so each arrow is pushed to its own wall. */
.payweek {
  flex: 1 1 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 8px;
}
.payweek .cal__warrow { display: flex; }
.payweek__label {
  flex: 1; min-width: 0; text-align: center;
  font-size: 12.5px; font-weight: 700; color: var(--ink);
}
/* ⚠ DISABLED IS A SPAN, NOT A GREYED LINK, so there is nothing to tap and
   nothing for a screen reader to offer. There is no next week to pay. */
.payweek__arrow--off { opacity: .35; }

/* A row with sessions is an anchor now; it must not pick up link colour or
   underline, because it is a row and reads as one. */
.paylist__row--open { text-decoration: none; color: inherit; }
.paylist__row--open:hover { background: rgba(27,67,50,.03); }

/* The chevron sits with the money so the figures and the affordance read as one
   right hand column.

   ⚠ THE TOTALS ROW RESERVES THE SAME WIDTH WITHOUT DRAWING ONE. The totals are
   not tappable, and without the reservation every figure in that row shifts
   left of the rows it totals. --hold is empty and aria-hidden: it is spacing. */
.paylist__chev {
  flex-shrink: 0; width: 14px; text-align: right;
  font-size: 17px; line-height: 1; color: #8A948C;
}
.paylist__chev--hold { visibility: hidden; }

/* The trends link, quiet, at the foot of the list. */
/* ⚠ THE LIST'S OWN GUTTER, so the footer does not rest on the wall. Every row
   above carries `padding: 13px 15px`; this carried none at all and its text
   started on the card's edge while the figures above it started 15px in.

   Right aligned, per the ruling, and both lines together: the link and the line
   above it share an edge rather than one hanging off each side. */
/* ⚠ ONE ROW, THE SAME SHAPE AS THE CALENDAR'S OWN FOOTER. Recent changes sits
   left and Connect calendar right on .cal__foot; this is that row, with the
   since line left and the trends link right. It was a stacked column with both
   items right aligned, which is two rows saying what one says.

   space-between rather than an auto margin, because both children are always
   present here: the since line renders only when there is a first week, and the
   link is inside the same anchor, so there is no single-child case to protect
   against the way .cal__foot has one. */
.paytrend {
  /* ⚠ WRAPS ONLY WHEN THE TWO GENUINELY CANNOT SHARE A ROW. Ruled by Trey on
     Sept 12th, 2026, answering the measurement flagged when this became one
     row: the since line has 111px before it meets the link, and every month
     fits except MARCH, which needs 116. March is one of the five AP spells in
     full and is the widest of them.

     Three declarations do it together and none is optional:

       flex-wrap on the container lets the LINK drop to its own line.
       white-space: nowrap on the since line stops it breaking mid text, which
         is what it would otherwise do first, giving two ragged lines inside one
         row rather than two clean rows.
       margin-left: auto on the link keeps it right aligned on the line it lands
         on. space-between alone puts a lone item at flex-start, so a wrapped
         link would have gone hard LEFT.

     Every other month stays exactly as shipped: one row, since left, link
     right. */
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 12px;
  margin-top: 14px;
  /* ⚠ THE CARD'S OWN RHYTHM, WHICH THIS ROW WAS THE ONLY ONE MISSING. Every
     other row on the card is `13px 15px`; this carried `13px 15px 0`, so it had
     no floor at all and the ADMIN & OTHER STAFF band sat flush against it.
     MEASURED: 0px between the two before, 13px after, which is the gap every
     row above it already keeps. */
  padding: 13px 15px;
  border-top: 1px solid var(--hair);
}
/* Neither may be the thing that forces the row wider than the card. */
.paytrend__since { min-width: 0; white-space: nowrap; }
.paytrend__go { flex-shrink: 0; margin-left: auto; }
.paytrend__since { font-size: 11px; color: var(--faint); }

/* ⚠ THE LINK IS THE TEXT, NOT THE ROW, and the padding here is the whole
   mechanism. This was one <a class="paytrend"> wrapping both children, so the
   since line and the empty span between them were inside the anchor: tapping
   anywhere on the footer lit the entire block and iOS painted its tap
   highlight over all of it. Now .paytrend is a plain row and only this is a
   link.

   ⚠ THE PADDING IS CANCELLED BY AN EQUAL NEGATIVE MARGIN, on purpose. A bare
   12.5px line gives a 19px tall target, which is not tappable. 13px of padding
   above and below takes the hit area to the full height of the row, and the
   -13px margin returns the LAYOUT box to exactly the 19px it was, so the row
   does not grow and the baseline does not move: with padding-top and
   margin-top equal and opposite, the distance from the margin edge to the
   first baseline is unchanged, which is what `align-items: baseline` measures.
   Horizontally it stays the width of its own text, which is the point. */
.paytrend__go {
  font-size: 12.5px; font-weight: 600; color: var(--green-btn);
  text-decoration: none;
  padding: 13px 0; margin-block: -13px;
}

/* ⚠ THE DESKTOP TABLE'S GUTTER AND THE TABLE'S OWN RHYTHM. Above the
   breakpoint .paylist is display:none and this footer follows the seven column
   table instead of the phone list, so it takes that table's measurements:

     20px, not 15px.  `.page--admin .pay__row` insets every row 20px and
       `.page--admin .pay__row--total` repeats it. MEASURED at 15px: the Since
       line started at x147 against a card edge of x127, five pixels left of
       every figure in the column above it.
     no top margin.   The 14px is the phone list's gap. On the table the totals
       row carries a tinted background and `border-bottom: 0`, and the ADMIN &
       OTHER STAFF band that follows sits flush against it with a hairline of
       its own. 14px of card showing between the tint and the rule read as a
       dropped row rather than the foot of the table.

   ⚠ AND IT KEEPS ITS BORDER, which the first draft of this block removed on
   the theory that the desktop totals row draws a bottom one. It does not:
   `.page--admin .pay__row--total` sets `border-bottom: 0`, same as
   `.paylist__total` does on the phone. MEASURED, computed border-bottom on
   that row is 0px at 1280. Removing this line left the footer with no
   separator at all. The 13px vertical padding is unchanged from the phone and
   is also what the totals row above uses, so the link's -13px cancel still
   matches its row and needs no override here. */
@media (min-width: 761px) and (min-height: 500px) {
  .paytrend { padding: 13px 20px; margin-top: 0; }
}

/* ── ONE TUTOR'S WEEK, AND THE TRENDS ───────────────────────────────────── */

.paycrumb { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted); }
.paycrumb__link { color: var(--green-btn); font-weight: 600; text-decoration: none; }
.paycrumb__sep { color: var(--faint); }
.paycrumb__here { color: var(--ink); font-weight: 600; }

.payhead { margin-top: 12px; }
.payhead__name { font-family: var(--serif); font-size: 26px; font-weight: 400; color: var(--ink); margin: 0; }
.payhead__week { margin: 4px 0 0; font-size: 12.5px; color: var(--muted); }

/* The green card. Its own sizes, per the mock. */
.paycard {
  margin-top: 14px; padding: 16px 18px 18px; border-radius: 8px;
  background: var(--green-deep); color: var(--off-white);
}
.paycard__label { font-size: 10px; font-weight: 700; letter-spacing: .16em; color: var(--gold-light); }
.paycard__money { font-family: var(--serif); font-size: 32px; line-height: 1.1; margin-top: 6px; }
.paycard__sub { margin: 4px 0 0; font-size: 12px; color: rgba(255,255,255,.72); }
.paycard__pair { display: flex; gap: 28px; margin-top: 14px; }
.paycard__stat { display: flex; flex-direction: column; gap: 2px; }
.paycard__n { font-family: var(--serif); font-size: 22px; line-height: 1; }
.paycard__cap { font-size: 9.5px; font-weight: 700; letter-spacing: .12em; color: rgba(255,255,255,.6); }
/* ⚠ THE GAP IS STATED IN WORDS, which is the whole reason this paragraph
   exists. Paid hours against taught hours is not an error to be reconciled; it
   is two attributions, and arithmetic alone reads as one of them being wrong. */
.paycard__gap {
  margin: 14px 0 0; padding-top: 12px; border-top: 1px solid rgba(255,255,255,.16);
  font-size: 11.5px; line-height: 1.6; color: rgba(255,255,255,.76);
}
.paycard__gap b { color: var(--gold-light); font-weight: 700; }

.paysess__head {
  display: flex; justify-content: space-between;
  margin-top: 18px; padding-bottom: 6px; border-bottom: 1px solid var(--hair);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; color: var(--faint);
}
.paysess__none, .paysess__count { margin: 12px 0 0; font-size: 12px; color: var(--muted); }

/* ── A SESSION ROW, WHICH IS A DISCLOSURE ───────────────────────────────── */

/* ⚠ EVERY ROW STARTS CLOSED. Trey's amendment to the bundle, which draws two of
   them open. A week of 34 sessions with the reports open cannot be scanned; the
   grade rides the closed row instead and the report is one tap away. */
.payrow { border-bottom: 1px solid var(--hair); }
.payrow__sum {
  display: flex; align-items: flex-start; gap: 12px;
  min-height: 56px; padding: 12px 0; cursor: pointer; list-style: none;
}
/* Safari draws its own triangle, which list-style alone does not remove. */
.payrow__sum::-webkit-details-marker { display: none; }
.payrow__who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.payrow__student { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.payrow__meta { font-size: 11px; color: var(--muted); }
.payrow__state { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 2px; }
.payrow__grade { font-size: 11px; color: var(--muted); }
.payrow__grade b { color: var(--ink); font-weight: 700; }
.payrow__filed { font-size: 9.5px; font-weight: 700; letter-spacing: .08em; color: var(--faint); }
/* Gold, never red: an unfiled survey is late, not an error, and nothing in this
   product uses red. */
.payrow__unfiled { font-size: 9.5px; font-weight: 700; letter-spacing: .08em; color: var(--gold-ink); }
.payrow__money { flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.payrow__pay { font-size: 13.5px; font-weight: 600; color: var(--ink); }
/* ⚠ MUTED, AND NEVER $0.00. The money is owed the moment the survey lands, and
   a zero reads as worthless rather than pending. */
.payrow__pay--owed { color: var(--muted); }
.payrow__pending { font-size: 9.5px; font-style: italic; color: var(--gold-ink); }
.payrow__chev { flex-shrink: 0; font-size: 16px; line-height: 1.6; color: #8A948C; }
.payrow[open] .payrow__chev { transform: rotate(90deg); }

/* ── THE REPORT, IN THE PARENT EMAIL'S OWN LAYOUT ───────────────────────── */

.payrep { margin: 0 0 16px; padding: 14px 15px; border-radius: 6px; background: var(--off-white); border: 1px solid var(--line); }
.payrep__top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.payrep__eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .16em; color: var(--gold-ink); }
.payrep__when { font-size: 10.5px; color: var(--muted); }
.payrep__open { margin: 10px 0 0; font-family: var(--serif); font-style: italic; font-size: 13px; line-height: 1.65; color: var(--body); }
.payrep__sec { margin-top: 12px; padding-top: 11px; border-top: 1px solid rgba(27,67,50,.08); }
.payrep__label { display: block; font-size: 10px; font-weight: 700; letter-spacing: .16em; color: var(--gold-dark); }
.payrep__body { margin: 6px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--body); }
/* The gold band, as the family sees it. */
.payrep__home { margin-top: 13px; padding: 12px 13px; border-radius: 4px; background: var(--gold); }
.payrep__homelabel { display: block; font-size: 10px; font-weight: 700; letter-spacing: .16em; color: var(--green-deep); }
.payrep__homebody { margin: 6px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--green-deep); }
/* The deep green grade block, with the estimate set large beside it. */
.payrep__grade {
  margin-top: 8px; padding: 12px 13px; border-radius: 4px; background: var(--green-deep);
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
}
.payrep__gradetext { flex: 1; min-width: 0; }
.payrep__gradelabel { display: block; font-size: 10px; font-weight: 700; letter-spacing: .16em; color: var(--gold-light); }
.payrep__gradebody { margin: 6px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--off-white); }
.payrep__gradenote { margin: 6px 0 0; font-size: 10px; line-height: 1.5; color: rgba(255,255,255,.62); }
.payrep__gradebig { flex-shrink: 0; font-family: var(--serif); font-size: 20px; line-height: 1.1; color: var(--gold-light); white-space: nowrap; }
.payrep__next { margin: 12px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--body); }
.payrep__next b { color: var(--ink); }
.payrep__none { margin: 0 0 16px; font-size: 12px; color: var(--muted); }

.payback { display: inline-block; margin-top: 18px; font-size: 12.5px; font-weight: 600; color: var(--green-btn); text-decoration: none; }

/* ── THE SLOT HELD AND NOT USED ─────────────────────────────────────────────
   A no-show and a same-day cancellation each pay the tutor a flat amount. Its
   own row type, above the lessons, because a slot-hold line has no survey, no
   grade and no report: rendered as a session it would be a row with every field
   empty, which reads as a lesson nobody wrote up. */
.payslot {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 11px 14px; border-bottom: 1px solid var(--hair);
  background: rgba(201,162,39,.07);
}
.payslot__who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.payslot__student { font-size: 13px; font-weight: 600; color: var(--ink); }
.payslot__meta { font-size: 11px; color: var(--muted); }
.payslot__tag {
  align-self: flex-start; margin-top: 3px; padding: 1px 6px; border-radius: 3px;
  background: rgba(201,162,39,.16); border: 1px solid rgba(168,132,42,.45);
  font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--gold-dark);
}
.payslot__pay { flex-shrink: 0; font-size: 13px; font-weight: 600; color: var(--ink); white-space: nowrap; }
.payslot__note { margin: 9px 0 4px; padding: 0 14px; font-size: 11px; line-height: 1.6; color: var(--muted); }
/* The same explanation, one line, on the Pay list's own row. */
.paylist__slot { font-size: 11px; color: var(--gold-ink); }

/* ── THE APPROVAL GATE ──────────────────────────────────────────────────────
   design_handoff_billing Part 3. No red anywhere: gold carries every warning. */

.billrec {
  margin-top: 16px; padding: 13px 15px; border-radius: 8px;
  background: var(--green-deep); color: var(--off-white);
}
.billrec__eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .14em; color: var(--gold-light); }
.billrec__line { margin: 6px 0 0; font-size: 12.5px; line-height: 1.6; }
.billrec__note { margin: 7px 0 0; font-size: 11px; line-height: 1.6; color: rgba(255,255,255,.72); }

/* Held back, in gold, at the top, marked not in this total. */
.billheld {
  margin-top: 16px; padding: 13px 15px; border-radius: 8px;
  background: rgba(201,162,39,.1); border: 1px solid rgba(168,132,42,.45);
}
.billheld__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.billheld__eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--gold-ink); }
.billheld__sum { font-family: var(--serif); font-size: 16px; color: var(--ink); }
.billheld__row {
  display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
  padding: 9px 0; border-top: 1px solid rgba(168,132,42,.3); margin-top: 9px;
}
.billheld__who { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.billheld__name { font-size: 13px; font-weight: 600; color: var(--ink); }
.billheld__why { font-size: 11px; line-height: 1.5; color: var(--muted); }
.billheld__amt { font-family: var(--serif); font-size: 15px; color: var(--ink); white-space: nowrap; }
.billheld__back { flex: 0 0 auto; }
.billheld__note { margin: 9px 0 0; font-size: 11px; line-height: 1.6; color: var(--gold-ink); }

.billsec { margin-top: 18px; }
.billsec__head { margin: 0 0 8px; font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--green-deep); }
.billsec--unset .billsec__head { color: var(--gold-ink); }
.billsec__note { margin: 8px 0 0; font-size: 11px; line-height: 1.6; color: var(--muted); }

.billfam { background: var(--off-white); border: 1px solid var(--line); border-radius: 8px; margin-bottom: 8px; }
.billfam__top {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 12px 14px; cursor: pointer; list-style: none; min-height: 44px;
}
.billfam__top::-webkit-details-marker { display: none; }
.billfam__who { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.billfam__name { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.billfam__meta { font-size: 11px; color: var(--muted); }
.billfam__amt { flex-shrink: 0; font-family: var(--serif); font-size: 16px; color: var(--ink); white-space: nowrap; }
.billfam__body { padding: 0 14px 12px; border-top: 1px solid var(--hair); }
/* The exception itself, gold, stating the comparison rather than a verdict. */
.billflag {
  align-self: flex-start; margin-top: 2px; padding: 2px 7px; border-radius: 3px;
  background: rgba(201,162,39,.16); border: 1px solid rgba(168,132,42,.45);
  font-size: 10.5px; line-height: 1.45; color: var(--gold-ink);
}

/* One line each for the ordinary families. Never a full re-listing. */
.billone {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 14px; background: var(--off-white);
  border: 1px solid var(--line); border-radius: 6px; margin-bottom: 5px;
}
.billone__name { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.billone__meta { flex: 1; min-width: 0; font-size: 11px; color: var(--muted); }
.billone__amt { font-family: var(--serif); font-size: 14px; color: var(--ink); white-space: nowrap; }

.billhold { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--hair); }
.billgate { margin-top: 20px; padding: 15px 16px; border-radius: 8px; background: var(--off-white); border: 1px solid var(--line); }
/* The three consequences, before the password and never after it. */
.billgate__says { margin: 0 0 14px; padding-left: 18px; }
.billgate__says li { margin-bottom: 5px; font-size: 12.5px; line-height: 1.6; color: var(--body); }

.billbtn {
  min-height: 44px; padding: 0 16px; border-radius: 6px; border: 1px solid var(--green-btn);
  background: var(--off-white); color: var(--green-btn);
  font-size: 12px; font-weight: 700; letter-spacing: .04em; cursor: pointer;
}
.billbtn--quiet { min-height: 38px; font-size: 11px; font-weight: 600; }
.billbtn--go { width: 100%; margin-top: 12px; background: var(--green-deep); border-color: var(--green-deep); color: var(--off-white); }
/* ⚠ DISABLED IS VISIBLY UNAVAILABLE, never merely unresponsive. */
.billbtn--go:disabled { background: var(--line); border-color: var(--line); color: var(--muted); cursor: not-allowed; }

/* ── THE SCREENS AROUND THE DIGEST ─────────────────────────────────────────
   Failed charges, cards on file, one family's record, prepared requests.
   No red anywhere: gold carries every warning. */

.billfail {
  margin-top: 14px; padding: 13px 15px; border-radius: 8px;
  background: var(--off-white); border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
}
.billfail__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.billfail__eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--gold-ink); }
.billfail__amt { font-family: var(--serif); font-size: 17px; color: var(--ink); }
.billfail__who { margin: 7px 0 0; font-size: 13px; color: var(--ink); }
.billfail__link { color: var(--green-btn); text-decoration: none; font-weight: 600; }
.billfail__week { margin-left: 6px; font-size: 11px; color: var(--muted); }
.billfail__why { margin: 6px 0 0; font-size: 12.5px; color: var(--gold-ink); }
.billfail__ref { display: block; margin-top: 2px; font-size: 10.5px; color: var(--faint); }
.billfail__note { margin: 7px 0 0; font-size: 11px; line-height: 1.6; color: var(--muted); }
.billfail__note--rule { color: var(--gold-ink); }

/* Copy, never send. Shown with a button and delivered by nobody. */
.billcopy { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--hair); }
.billcopy__label { display: block; font-size: 9.5px; font-weight: 700; letter-spacing: .08em; color: var(--gold-ink); }
/* ⚠ 16px, NOT 11.5px, AND THIS IS NOT A TASTE CHOICE. These are focusable
   controls: a readonly input still takes focus, and iOS zooms the whole page
   when a focused field computes under 16px. The portal-wide guard caught this
   at 11.5px, which is what the label above it uses and what looked right in
   isolation. A copy field that zooms the page on tap is worse than a slightly
   larger one. */
.billcopy__field, .billcopy__text { margin-top: 6px; width: 100%; font-size: 16px; }
.billcopy__text { line-height: 1.5; resize: vertical; }

.billreq { margin-top: 14px; padding: 13px 15px; border-radius: 8px; background: var(--off-white); border: 1px solid var(--line); }
.billreq__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.billreq__who { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.billreq__app {
  margin-left: 7px; padding: 1px 6px; border-radius: 3px;
  background: rgba(27,67,50,.07); font-size: 9px; font-weight: 700;
  letter-spacing: .06em; color: var(--green-deep);
}
.billreq__amt { font-family: var(--serif); font-size: 17px; color: var(--ink); }
.billreq__acts { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 11px; }
.billreq__state { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--muted); }
/* The ESA reference and the partial. Gold, because a shortfall is a warning. */
.billreq__ref { margin: 7px 0 0; font-size: 11px; color: var(--muted); }
.billreq__short {
  margin: 8px 0 0; padding: 9px 11px; border-radius: 5px;
  background: rgba(201,162,39,.1); border-left: 2px solid var(--gold);
  font-size: 11.5px; line-height: 1.6; color: var(--gold-ink);
}
/* 16px for the same reason every other focusable control here is: iOS zooms
   the page when a focused field computes under it. */
.billreq__field { margin-top: 6px; font-size: 16px; }

.billcard {
  display: flex; align-items: baseline; gap: 10px; padding: 10px 13px;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px; margin-bottom: 6px;
}
.billcard__brand { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: var(--green-deep); }
.billcard__last { flex: 1; font-size: 12.5px; color: var(--ink); }
.billcard__exp { font-size: 11px; color: var(--muted); }

/* ⚠ EVIDENCE, NOT A VERDICT. Facts on one side, what each does and does not
   mean on the other, because the last time this drew a conclusion it was wrong:
   a card marked "limited" charged off-session successfully. */
.billev { margin-top: 11px; padding: 12px 13px; border-radius: 6px; background: rgba(27,67,50,.04); }
.billev__head { display: block; font-size: 9.5px; font-weight: 700; letter-spacing: .08em; color: var(--gold-ink); }
.billev__row { display: flex; flex-direction: column; gap: 2px; margin-top: 9px; }
.billev__fact { font-size: 12px; font-weight: 600; color: var(--ink); }
.billev__means { font-size: 11px; line-height: 1.6; color: var(--muted); }
.billev__settled { margin: 11px 0 0; padding-top: 9px; border-top: 1px solid var(--hair); font-size: 11.5px; font-weight: 600; color: var(--gold-ink); }

.billhist {
  display: flex; align-items: baseline; gap: 10px; padding: 10px 13px;
  background: var(--off-white); border: 1px solid var(--line); border-radius: 6px; margin-bottom: 5px;
}
.billhist__when { flex: 0 0 auto; font-size: 11px; color: var(--muted); }
.billhist__carried {
  margin-left: 5px; padding: 1px 5px; border-radius: 3px;
  background: rgba(201,162,39,.16); border: 1px solid rgba(168,132,42,.45);
  font-size: 8.5px; font-weight: 700; letter-spacing: .06em; color: var(--gold-dark);
}
.billhist__what { flex: 1; min-width: 0; font-size: 12px; color: var(--ink); }
.billhist__why { display: block; font-size: 10.5px; color: var(--gold-ink); }
.billhist__ref { display: block; font-size: 10px; color: var(--faint); }
.billhist__amt { font-family: var(--serif); font-size: 14px; color: var(--ink); white-space: nowrap; }

/* ── THE BILLING DIGEST ─────────────────────────────────────────────────────
   design_handoff_billing, Part 2. Phase 1 is read only: nothing here styles a
   control that moves money, because none exists yet. */

.billsum {
  margin-top: 16px; padding: 15px 16px 16px; border-radius: 8px;
  background: var(--green-deep); color: var(--off-white);
}
.billsum__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.billsum__eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .14em; color: var(--gold-light); }
.billsum__count { font-size: 10.5px; color: rgba(255,255,255,.72); }
.billsum__total { margin: 6px 0 0; font-family: var(--serif); font-size: 30px; line-height: 1.1; }
.billsum__split {
  display: flex; flex-wrap: wrap; gap: 16px; margin-top: 12px;
  padding-top: 12px; border-top: 1px solid rgba(255,255,255,.16);
}
.billsum__part { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.billsum__part b { font-family: var(--serif); font-size: 17px; font-weight: 400; }
.billsum__part span { font-size: 10px; color: rgba(255,255,255,.72); }
/* ⚠ THE THIRD BUCKET IS WORDS, NOT A FIGURE, and this rule is what keeps it
   from looking like one that failed to load. Ruled: nothing records whether a
   request was sent, so no number is honest here. */
.billsum__untracked {
  margin: 12px 0 0; padding-top: 11px; border-top: 1px solid rgba(255,255,255,.16);
  font-size: 11px; line-height: 1.6; color: rgba(255,255,255,.72);
}

.billgroup { margin-top: 16px; border-radius: 8px; background: var(--off-white); border: 1px solid var(--line); overflow: hidden; }
/* Gold, never red: red in this portal means permanent destruction, and a family
   with no method set is unfinished rather than wrong. */
.billgroup--unset { border-color: rgba(168,132,42,.45); }
.billgroup__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 11px 14px; background: rgba(27,67,50,.04); border-bottom: 1px solid var(--hair);
}
.billgroup--unset .billgroup__head { background: rgba(201,162,39,.16); }
.billgroup__name { font-size: 10px; font-weight: 700; letter-spacing: .12em; color: var(--green-deep); }
.billgroup--unset .billgroup__name { color: var(--gold-ink); }
.billgroup__sum { font-family: var(--serif); font-size: 15px; color: var(--ink); }

.billrow { border-bottom: 1px solid var(--hair); }
.billrow:last-child { border-bottom: 0; }
.billrow__top {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 12px 14px; cursor: pointer; list-style: none; min-height: 44px;
}
.billrow__top::-webkit-details-marker { display: none; }
.billrow__who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.billrow__name { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.billrow__meta { font-size: 11px; color: var(--muted); }
.billrow__warn { margin-top: 3px; font-size: 11px; line-height: 1.5; color: var(--gold-ink); }
.billrow__amt { flex-shrink: 0; font-family: var(--serif); font-size: 16px; color: var(--ink); white-space: nowrap; }

.billlines { display: flex; flex-direction: column; padding: 0 14px 4px; }
.billline {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 7px 0; border-top: 1px solid var(--hair);
}
.billline__what { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.billline__who { font-size: 12px; color: var(--body); }
.billline__when { font-size: 10.5px; color: var(--muted); }
.billline__tag {
  align-self: flex-start; margin-top: 3px; padding: 1px 6px; border-radius: 3px;
  background: rgba(201,162,39,.16); border: 1px solid rgba(168,132,42,.45);
  font-size: 9px; font-weight: 700; letter-spacing: .06em; color: var(--gold-dark);
}
.billline__amt { flex-shrink: 0; font-size: 12px; font-weight: 600; color: var(--ink); white-space: nowrap; }
/* ⚠ A WAIVED FEE IS DIMMED, NEVER STRUCK THROUGH AND NEVER ABSENT. A strike is
   a dash by another name on a surface that allows none, and an absent line
   makes the week stop adding up. */
.billline__amt--nil { color: var(--faint); font-weight: 400; }
.billlines__foot { margin: 4px 0 12px; padding: 0 14px; font-size: 10.5px; line-height: 1.6; color: var(--muted); }

.billnote {
  margin: 16px 0 0; padding: 12px 14px; border-radius: 6px;
  background: rgba(201,162,39,.1); border-left: 2px solid var(--gold);
  font-size: 11.5px; line-height: 1.6; color: var(--body);
}

/* ── OVER TIME ──────────────────────────────────────────────────────────── */

.payrange { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.payrange__chip {
  min-height: 44px; display: inline-flex; align-items: center; padding: 0 14px;
  border-radius: 100px; background: var(--off-white); border: 1px solid rgba(27,67,50,.2);
  font-size: 11.5px; font-weight: 600; color: var(--muted); text-decoration: none;
}
.payrange__chip--on { background: var(--green-deep); border-color: var(--green-deep); color: var(--off-white); }

.paypanel { margin-top: 16px; padding: 14px 15px 16px; border-radius: 8px; background: var(--off-white); border: 1px solid var(--line); }
.paypanel__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.paypanel__title { font-size: 10px; font-weight: 700; letter-spacing: .14em; color: var(--green-deep); }
.paypanel__note { font-size: 10.5px; color: var(--muted); }
.paypanel__empty { margin: 14px 0 0; font-size: 13px; font-weight: 600; color: var(--gold-ink); }
.paypanel__why, .paypanel__foot { margin: 6px 0 0; font-size: 11px; line-height: 1.6; color: var(--muted); }

/* ⚠ LINES AND AREAS IN INLINE SVG, PER THE BUNDLE'S MOCK, WHICH SUPERSEDES THE
   PAIRED BARS THIS SCREEN FIRST SHIPPED WITH. Still no plotting library and
   still no script: inline SVG is markup, script-src stays 'self' with nothing
   added, and the panel prints.

   ⚠ THE STROKES ARE PINNED WITH vector-effect AND THAT IS NOT COSMETIC. The
   viewBox is 320 wide and stretched to the panel with preserveAspectRatio
   none, so the x axis scales and the y axis does not. Without the pin a 2px
   stroke is drawn 2px tall and 1.8px wide on the same line, and the gold and
   green weights stop matching where the lines run steeply. */
.payline { position: relative; margin-top: 10px; line-height: 0; }
.payline svg { display: block; width: 100%; }
.payline__area { fill: rgba(27,67,50,.09); }
/* ⚠ THE FALLBACK IS LOAD BEARING, AND IT IS ABOUT A THIRTEENTH TUTOR. The
   series classes cover twelve lines; a line past them sets no --series, and
   `stroke: var(--series)` with nothing behind it resolves to the initial value,
   which for a path is `none`. The line would be ABSENT rather than miscoloured,
   which is the one failure this panel must not have. Deep green is a collision
   with the leader and the legend and the tooltip still name every line, so a
   thirteenth tutor reads as ambiguous rather than as missing. */
.payline__stroke { fill: none; stroke: var(--series, var(--green-deep)); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
/* ⚠ ONE COLOUR PER SERIES, DECLARED ONCE AND READ BY EVERY SHAPE THAT REPEATS
   IT. The line, the resting dot, the legend key, the hover dot and the tooltip
   swatch are five drawings of the same series, and the tooltip bundle is
   explicit that the swatch must take its colour from the same class as the
   stroke rather than from a hex. Five literals is five chances for a tutor's
   line and their swatch to stop matching, and the one that drifts is the one
   nobody is looking at. --series is that single source; every rule below reads
   it and none of them names a colour. */
.payline__stroke--rev, .payline__dot--rev, .paykey__sw--rev,
.payhover__dot--rev, .paytip__sw--rev { --series: var(--green-deep); }
.payline__stroke--pay, .payline__dot--pay, .paykey__sw--pay,
.payhover__dot--pay, .paytip__sw--pay { --series: var(--gold); }
.payline__stroke--hrs, .payline__dot--hrs, .paykey__sw--hrs,
.payhover__dot--hrs, .paytip__sw--hrs { --series: var(--green-deep); }
.payline__stroke--stud, .payline__dot--stud, .paykey__sw--stud,
.payhover__dot--stud, .paytip__sw--stud { --series: var(--gold); }
.payline__stroke--filed, .payline__dot--filed, .paykey__sw--filed,
.payhover__dot--filed, .paytip__sw--filed { --series: var(--gold-ink); }
/* ⚠ A TUTOR LINE PER TUTOR, SO THE SERIES CLASSES RUN PAST THREE. The cap came
   off at Trey's ruling, and a class list that stopped at --t2 would have left
   every tutor past the third with no stroke colour at all: the base rule sets
   `fill: none` and an unset stroke on a path draws NOTHING, so the fourth line
   would have been absent rather than wrong, on a panel whose subject is the
   comparison between them.

   SIX COLOURS, THEN THE SAME SIX DASHED, which is twelve lines that can be told
   apart. The brand is green, gold and grey by construction, and a seventh flat
   colour drawn from it is a shade of one of the first six: shape separates what
   hue has run out of room to. Every colour is from the inventory in
   handoff/design-tokens.md and none of them is red.

   ⚠ THE DASH IS IDENTITY HERE, NOT UNIT. On the students panel a dash says the
   crossing point means nothing because the two lines are different units. On
   this panel every line is hours, the legend carries the same dash beside the
   name, and the tooltip swatch repeats it: within this panel it reads as which
   tutor, which is the only question it is asked. */
.payline__stroke--t0, .payline__dot--t0, .paykey__sw--t0,
.payhover__dot--t0, .paytip__sw--t0 { --series: var(--green-deep); }
.payline__stroke--t1, .payline__dot--t1, .paykey__sw--t1,
.payhover__dot--t1, .paytip__sw--t1 { --series: var(--faint); }
.payline__stroke--t2, .payline__dot--t2, .paykey__sw--t2,
.payhover__dot--t2, .paytip__sw--t2 { --series: var(--gold); }
.payline__stroke--t3, .payline__dot--t3, .paykey__sw--t3,
.payhover__dot--t3, .paytip__sw--t3 { --series: var(--sage); }
.payline__stroke--t4, .payline__dot--t4, .paykey__sw--t4,
.payhover__dot--t4, .paytip__sw--t4 { --series: var(--green-btn); }
.payline__stroke--t5, .payline__dot--t5, .paykey__sw--t5,
.payhover__dot--t5, .paytip__sw--t5 { --series: var(--gold-dark); }
.payline__stroke--t6, .payline__dot--t6, .paykey__sw--t6,
.payhover__dot--t6, .paytip__sw--t6 { --series: var(--green-deep); }
.payline__stroke--t7, .payline__dot--t7, .paykey__sw--t7,
.payhover__dot--t7, .paytip__sw--t7 { --series: var(--faint); }
.payline__stroke--t8, .payline__dot--t8, .paykey__sw--t8,
.payhover__dot--t8, .paytip__sw--t8 { --series: var(--gold); }
.payline__stroke--t9, .payline__dot--t9, .paykey__sw--t9,
.payhover__dot--t9, .paytip__sw--t9 { --series: var(--sage); }
.payline__stroke--t10, .payline__dot--t10, .paykey__sw--t10,
.payhover__dot--t10, .paytip__sw--t10 { --series: var(--green-btn); }
.payline__stroke--t11, .payline__dot--t11, .paykey__sw--t11,
.payhover__dot--t11, .paytip__sw--t11 { --series: var(--gold-dark); }

.payline__stroke--rev { stroke: var(--series); }
.payline__stroke--pay { stroke: var(--series); }
.payline__stroke--hrs { stroke: var(--series); }
/* ⚠ DASHED, NOT A SECOND COLOUR OF SOLID. Hours and students are different
   units on one axis: the dash is what says the crossing point means nothing.
   The tooltip swatch repeats the dash for the same reason. */
.payline__stroke--stud { stroke: var(--series); stroke-dasharray: 4 3; }
.payline__stroke--filed { stroke: var(--series); }
/* The leader keeps the panel's full weight; every line under it is 1.5, which
   is the bundle's own pairing and what keeps a crowded panel readable. */
.payline__stroke--t0 { stroke: var(--series); }
.payline__stroke--t1 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t2 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t3 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t4 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t5 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t6 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t7 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t8 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t9 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t10 { stroke: var(--series); stroke-width: 1.5; }
.payline__stroke--t11 { stroke: var(--series); stroke-width: 1.5; }
/* The second lap of six, dashed rather than a seventh hue. */
.payline__stroke--t6,
.payline__stroke--t7,
.payline__stroke--t8,
.payline__stroke--t9,
.payline__stroke--t10,
.payline__stroke--t11 { stroke-dasharray: 5 3; }
/* ⚠ THE DOTS ONLY APPEAR ON A SHORT SERIES, and the view decides that, not this
   file: under four weeks a reading is a point rather than a line drawn through
   one number. */
.payline__dot { fill: var(--series); }

/* ⚠ TICKS SIT AT THEIR POINT AND THE TWO ON THE ENDS ARE PINNED TO THE EDGES.
   The bars this replaced centred each label in its own 22px column, where a
   nowrap "Sept 5th" was twice the width it had. Centring the end labels on
   x=0 and x=100% would hang half of each outside the card. */
.payline__ticks { position: relative; height: 11px; margin-top: 7px; }
.payline__tick { position: absolute; top: 0; transform: translateX(-50%); font-size: 8.5px; line-height: 11px; color: var(--faint); white-space: nowrap; }
.payline__tick--first { transform: none; }
.payline__tick--last { transform: translateX(-100%); }

/* ⚠ LINE SWATCHES, NOT SQUARES, because the marks they stand for are lines and
   one of them is dashed. A square cannot say dashed. */
.paykey { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px; }
.paykey__item { display: inline-flex; align-items: center; gap: 6px; font-size: 10.5px; color: var(--muted); }
.paykey__sw { width: 14px; height: 2px; border-radius: 1px; }
.paykey__sw { background: var(--series); }
.paykey__sw--stud { height: 0; background: none; border-top: 2px dashed var(--series); border-radius: 0; }
/* ⚠ THE SWATCH REPEATS THE LINE'S SHAPE, so a dashed tutor line is a dashed
   key and a dashed tooltip swatch. A solid swatch beside a dashed line names
   the wrong tutor on the one panel where shape is what separates two of them. */
.paykey__sw--t6,
.paykey__sw--t7,
.paykey__sw--t8,
.paykey__sw--t9,
.paykey__sw--t10,
.paykey__sw--t11 { height: 0; background: none; border-top: 2px dashed var(--series); border-radius: 0; }

/* ── CHART TOOLTIPS ON OVER TIME ─────────────────────────────────────────────
   design_handoff_chart_tooltips. Hover or tap a week on any of the four charts
   and read every series on it.

   ⚠ HTML OVER THE SVG, NEVER INSIDE IT. The charts are drawn with
   preserveAspectRatio="none", so the box stretches horizontally and not
   vertically: anything inside scales with it. A 216px tooltip in there would be
   216 USER UNITS wide and render at whatever the container made of that, and a
   circle would go oval. Everything here is HTML positioned in percentages over
   the top, where a pixel is still a pixel.

   ⚠ THE LAYER IS display: contents, WHICH IS WHAT LETS THE PHONE READOUT SIT
   UNDER THE CHART AND TAKE SPACE. A wrapper with a box of its own would either
   have to be absolute, in which case the readout floats and overlaps the ticks
   and the legend under it, or static, in which case the guide and the dots
   measure against a box that has grown by the height of the readout. With the
   wrapper out of the layout, the marks position against .payline and the readout
   flows after the chart as an ordinary block.

   ⚠ AND EACH CHART DECLARES ITS OWN HEIGHT, because they differ: 96, 80, 88 and
   the filed panel at 56. The guide, the band and the dot box are pinned to that
   rather than to the container, so a readout below cannot stretch them. */
.payline--money  { --chart-h: 96px; }
.payline--tutors { --chart-h: 80px; }
.payline--mix    { --chart-h: 88px; }
.payline--filed  { --chart-h: 56px; }

.payhover { display: contents; }

/* ⚠ THE HIT AREA IS THE CHART, NOT THE PANEL, and it is a real element rather
   than a listener on the container. On a phone the readout is a block INSIDE
   this container, so a tap meant for Clear would otherwise reach the container
   and silently move the week being read. */
.payhover__hit { position: absolute; top: 0; left: 0; right: 0; height: var(--chart-h); z-index: 2; }

.payhover__band { position: absolute; top: 0; height: var(--chart-h); background: rgba(20, 55, 42, .05); pointer-events: none; }
/* 1px, the bundle's own value. It does the pointing; nothing else moves. */
.payhover__guide { position: absolute; top: 0; height: var(--chart-h); width: 1px; background: rgba(20, 55, 42, .32); pointer-events: none; }

.payhover__dots { position: absolute; top: 0; left: 0; right: 0; height: var(--chart-h); pointer-events: none; }
/* ⚠ HOVER ADDS A DOT, IT DOES NOT ENLARGE ONE. None of the ranges this screen
   offers draws resting dots: they are gated on `marked`, which is three weeks or
   fewer, and the pills are 4, 12, 26 and All time. One rule covers both cases,
   since on a short marked range the added dot lands over the resting one.
   The ring is the panel's own ground, so the dot reads over a line it sits on. */
.payhover__dot {
  position: absolute; width: 12px; height: 12px; box-sizing: border-box;
  border-radius: 50%; background: var(--series); border: 2px solid var(--off-white);
  transform: translate(-50%, -50%);
}

/* ── THE TOOLTIP ──────────────────────────────────────────────────────────── */
.paytip {
  position: absolute; top: 8px; left: 0; z-index: 3;
  width: 216px; box-sizing: border-box;
  padding: 11px 14px 12px;
  background: var(--green-deep); border-radius: 5px;
  box-shadow: 0 8px 22px rgba(20, 55, 42, .28);
  line-height: normal; pointer-events: none;
}
.paytip__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.paytip__week { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--gold-light); }
.paytip__rows { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
/* Labels left, values right, on their own baseline, so two money figures under
   each other compare by eye. */
.paytip__row { display: flex; align-items: baseline; gap: 9px; }
.paytip__label { flex: 1; min-width: 0; font-size: 12px; color: rgba(255, 255, 255, .7); }
.paytip__value { flex-shrink: 0; font-size: 12.5px; font-weight: 700; color: var(--off-white); }
.paytip__quiet { font-size: 12px; color: rgba(255, 255, 255, .7); }
.paytip__foot { margin: 9px 0 0; padding-top: 8px; border-top: 1px solid rgba(255, 255, 255, .16); font-size: 11.5px; color: rgba(255, 255, 255, .62); }

/* ⚠ THE SWATCH REPEATS THE LINE, INCLUDING ITS SHAPE. Colour comes from
   --series, the same property the stroke reads, so the two cannot drift. */
.paytip__sw { flex-shrink: 0; width: 9px; height: 9px; border-radius: 2px; background: var(--series); box-sizing: border-box; }
/* ⚠ THE THREE DEEP GREEN SERIES GET A RING, because the tooltip's ground IS deep
   green and a swatch painted --green-deep on it is invisible. Named by the
   series that need it rather than given to all four, per the mock: the gold ones
   read on this ground unaided and a ring on them would be a second shape. */
.paytip__sw--rev, .paytip__sw--hrs, .paytip__sw--t0 { border: 1.5px solid var(--off-white); }
/* Dashed line, not a square, for the one series drawn dashed. */
.paytip__sw--dash,
.paytip__sw--t6,
.paytip__sw--t7,
.paytip__sw--t8,
.paytip__sw--t9,
.paytip__sw--t10,
.paytip__sw--t11 { width: 14px; height: 0; border-radius: 0; background: none; border-top: 2px dashed var(--series); }
/* ⚠ SEE .paytip__clear[hidden] BELOW. The display here is author origin and
   beats the user agent's [hidden], so the guard is not optional. */
.paytip__clear {
  flex-shrink: 0; min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: flex-end;
  margin: -12px -4px -12px 0; padding: 0 4px;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 600; color: rgba(255, 255, 255, .72);
  pointer-events: auto;
}
/* ⚠ THE GUARD. Without it the Clear button paints on a tip that has nothing to
   clear. Same defect class as the booking sheet of 16 Sept 2026. */
.paytip__clear[hidden] { display: none !important; }

/* ⚠ ON A POINTER THAT DOES NOT HOVER, THE READOUT GOES UNDER THE CHART. Forced
   rather than chosen: the filed panel is 56px tall and a 90px tooltip laid over
   it covers the thing being read. A block beneath covers nothing, holds still
   and needs no clamping or flipping. The script asks the computed position which
   mode it is in rather than carrying a second copy of this query. */
@media (max-width: 760px), (hover: none) {
  .paytip { position: static; width: 100%; margin-top: 12px; }
}

/* Nothing is pinned at rest, so this is belt and braces: a page printed with a
   week open prints the panel rather than the readout over it. */
@media print {
  .payhover { display: none; }
}

/* ⚠ THE STATS ROW NAMES ITS WEEK, RULED. It was the only figure block on the
   screen carrying no date, and it quoted the live week whatever week the reader
   had arrived from. It sits ON the rule rather than above it, so the caption and
   the figures under it read as one block and the panel gains 13px, not 30. */
.paystats__week {
  margin: 12px 0 0; padding-top: 11px; border-top: 1px solid var(--hair);
  font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--faint);
  text-transform: uppercase;
}
.paystats__week + .paystats { margin-top: 7px; padding-top: 0; border-top: 0; }
.paystats { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--hair); }
.paystat { display: flex; flex-direction: column; gap: 1px; }
.paystat b { font-family: var(--serif); font-size: 17px; font-weight: 400; color: var(--ink); }
.paystat span { font-size: 10px; color: var(--faint); }

/* Horizontal bars, sorted by size, leader filled solid so the ranking is the
   shape rather than a legend. */
.paybars { display: flex; flex-direction: column; gap: 7px; margin-top: 12px; }
.paybars__row { display: flex; align-items: center; gap: 9px; }
.paybars__name { flex: 0 0 76px; font-size: 11px; color: var(--body); }
.paybars__track { flex: 1; min-width: 0; height: 10px; border-radius: 2px; background: rgba(27,67,50,.07); }
.paybars__fill { display: block; height: 100%; border-radius: 2px; background: rgba(27,67,50,.34); }
.paybars__fill--lead { background: var(--green-deep); }
.paybars__val { flex: 0 0 30px; text-align: right; font-size: 11px; font-weight: 600; color: var(--ink); }

/* Counts in a two column grid, scaling to the zones present rather than to the
   mock's four. A handful of numbers do not need an axis.

   ⚠ A LONE CARD ON THE LAST ROW SPANS. With three or five zones the final card
   otherwise sits at half width beside a hole, which reads as a card that failed
   to load rather than as the end of the list. */
.payzones { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
.payzone { display: flex; flex-direction: column; padding: 11px 12px; border-radius: 5px; background: rgba(27,67,50,.04); }
.payzone:last-child:nth-child(odd) { grid-column: 1 / -1; }
.payzone__n { font-family: var(--serif); font-size: 22px; line-height: 1; color: var(--ink); }
.payzone__name { margin-top: 5px; font-size: 12px; font-weight: 600; color: var(--ink); }
/* The abbreviation is subtext because it is the half that MOVES, which is the
   whole subject of the line under the grid. */
.payzone__abbr { margin-top: 1px; font-size: 10.5px; color: var(--muted); }

/* ⚠ THE ONE PANEL THAT IS A PROBLEM RATHER THAN A MEASUREMENT, and the gold
   edge is what says so before the line is read. */
.paypanel--warn { border-left: 3px solid var(--gold); }
.payfiled { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-top: 9px; }
.payfiled__now { font-size: 12px; color: var(--muted); }
.payfiled__was { font-size: 12px; font-weight: 700; color: var(--gold-ink); }

/* ── FAMILY CREDIT ──────────────────────────────────────────────────────────
   design_handoff_billing Part 3, frames 5a, 5b, 5c and the audit's credit
   block.

   ⚠ GOLD CARRIES EVERY WARNING HERE AND NOTHING IS RED. The portal spends its
   danger token on permanent destruction, and a credit running out is a fact to
   notice rather than something being destroyed. Labels use #7C6210 and muted
   text #5C6B62, both over 4.5:1 on the creams below.

   ⚠ NOTHING HERE IS FIXED OR STICKY. These are flow elements inside panels the
   shell already positions, so the shell gate does not apply to this block. */

.fcred { margin: 22px 0 8px; }
.fcred__head {
  font-size: 12px; letter-spacing: .08em; color: #7C6210;
  margin: 0 0 10px; font-weight: 700;
}
.fcred__card {
  background: #FDF7E6; border: 1px solid rgba(201, 162, 39, .35);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.fcred__fig { margin: 0 0 2px; font-size: 15px; color: #25332C; }
.fcred__fig b { font-size: 20px; }
.fcred__sub, .fcred__note { margin: 0 0 6px; font-size: 12.5px; color: #5C6B62; }
.fcred__note { margin-top: 8px; }
.fcred__summary {
  cursor: pointer; font-size: 13px; font-weight: 600; color: #25332C;
  padding: 8px 0;
}
.fcred__adjust, .fcred__add { margin-bottom: 10px; }

/* The fourth summary line. ⚠ OUTSIDE THE THREE BUCKETS, and it reads as one
   sentence in two units rather than as a fourth figure in the same row. */
/* ⚠ ONE LINE, trimmed on Trey's instruction of 15 Sept 2026. The stacked bold
   figure and the explanatory sentence are gone, and the two helper classes they
   used went with them rather than being left behind to rot. */
.billsum__credit {
  margin: 10px 0 0; padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, .18);
  font-size: 12px; opacity: .9;
}

/* A credit row on the digest. ⚠ IT RENDERS AT $0.00 AND IS NEVER DROPPED. */
.billrow__credit { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 4px; }
.billrow__creditbadge {
  display: inline-flex; padding: 2px 8px; border-radius: 100px;
  background: rgba(201, 162, 39, .18); color: #7C6210;
  font-size: 10.5px; letter-spacing: .06em; font-weight: 700;
}
.billrow__creditsub { font-size: 11.5px; color: #5C6B62; }

/* The audit's stated block. ⚠ ABOVE THE TOTAL AND MARKED NOT IN IT. */
.billsec--credit { background: #FDF7E6; border-radius: 12px; padding: 12px 14px; }
.billcredit__fig { margin: 0 0 8px; font-size: 16px; font-weight: 700; color: #25332C; }
.billcredit__row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 6px 0; border-top: 1px solid rgba(201, 162, 39, .22);
}
.billcredit__who { display: flex; flex-direction: column; font-size: 13.5px; color: #25332C; }
.billcredit__meta { font-size: 11.5px; color: #5C6B62; }
.billcredit__amt { font-size: 13px; font-weight: 600; color: #7C6210; white-space: nowrap; }

/* The balance card on the family record. */
.billcreditcard {
  background: #FDF7E6; border: 1px solid rgba(201, 162, 39, .35);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.billcreditcard__head {
  font-size: 11px; letter-spacing: .08em; color: #7C6210; font-weight: 700;
}
.billcreditcard__fig { margin: 6px 0 0; font-size: 15px; color: #25332C; }
.billcreditcard__sub { margin: 2px 0 0; font-size: 12px; color: #5C6B62; }

/* An adjustment on the ledger. ⚠ ITS OWN GOLD LINE, never a rewrite. */
.billhist__adjust {
  display: inline-flex; padding: 2px 7px; border-radius: 100px;
  background: rgba(201, 162, 39, .18); color: #7C6210;
  font-size: 10px; letter-spacing: .06em; font-weight: 700;
}

/* ── DECIDING A SESSION, AND THE HANDLE ─────────────────────────────────────
   design_handoff_billing Part 3 and the handle field in Part 4.

   ⚠ GOLD CARRIES EVERY WARNING AND NOTHING IS RED. A dimmed menu item is a
   fact about a balance, not a destruction. Contrast floors per the brief:
   #6B5300 on page cream, #4F5C54 muted, #5A4600 on the gold highlight.

   ⚠ NOTHING HERE IS FIXED OR STICKY, so the shell gate does not apply. */

.linedec { margin-top: 6px; }
.linedec__open {
  cursor: pointer; font-size: 10.5px; letter-spacing: .06em; font-weight: 700;
  color: #6B5300; padding: 3px 0; list-style: none;
}
.linedec__open::-webkit-details-marker { display: none; }
.linedec__form {
  margin-top: 6px; padding: 10px 12px; border-radius: 10px;
  background: #FDF7E6; border: 1px solid rgba(201, 162, 39, .35);
}
.linedec__item {
  display: flex; flex-direction: column; gap: 1px;
  padding: 7px 0; border-top: 1px solid rgba(201, 162, 39, .22); cursor: pointer;
}
.linedec__item:first-of-type { border-top: 0; }
.linedec__label { font-size: 13px; color: #25332C; font-weight: 600; }
/* ⚠ EVERY ITEM NAMES ITS EFFECT ON THE BALANCE, so the sentence is part of the
   item rather than a tooltip: an effect nobody reads is an effect nobody knew
   about. */
.linedec__effect { font-size: 11px; color: #4F5C54; line-height: 1.45; }
/* ⚠ DIMMED, NOT ABSENT, and carrying its reason. A menu that changes length
   between two lines of the same family answers no question. */
.linedec__item--off { opacity: .55; cursor: default; }
.linedec__item--off .linedec__effect { color: #5A4600; }

.fhandle { margin-top: 4px; }
.billreq__handle { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 4px; }
.billreq__handlelabel { font-size: 10px; letter-spacing: .08em; color: #6B5300; font-weight: 700; }
.billreq__handleval { font-size: 13px; color: #25332C; }
.billcopy--inline { display: inline-flex; align-items: center; gap: 6px; }
/* ⚠ 16px, NOT 13px. It is a focusable input, and anything smaller makes iOS zoom
   the page the moment it is tapped. The suite refuses it, which is why this
   reads larger than the label beside it. */
.billcopy--inline .billcopy__text { width: auto; min-width: 150px; font-size: 16px; }

/* ⚠ PLACED ABOVE THE SURVEY OVERLAY SECTION, NOT APPENDED TO THE FILE.
   test/survey-submitting.js reads "the overlay section" as everything from its
   marker to the END OF THE FILE and asserts every selector in it is overlay
   scoped, so anything added after that marker is read as a stray overlay rule.
   New sections go here, before it. */

/* ── THE STUDENT RECORD: HEADER, GROUPS AND THE BILLING ROW ──────────────────
   design_handoff_edit_student, frames 1a and 2a.

   ⚠ NO NEW LAYOUT MECHANISM. The three-column contact rows are `.pair`, which
   is already a flex row with flex:1 children and already stacks below 760px, so
   a third child needed nothing. What IS new is here: the record header, the
   billing row's phone behaviour, and two note variants. */

/* THE RECORD HEADER. The name, what the record is in one line, and the save.
   ⚠ THE SECOND SAVE IS OUTSIDE THE FORM and reaches it by the `form`
   attribute, so there is one form, one action and one set of fields however the
   admin presses it. */
.rechead {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 18px; flex-wrap: wrap; margin-top: 10px;
}
.rechead__main { min-width: 0; }
.rechead__main .h1 { margin: 0; }
.rechead__sub {
  margin: 5px 0 0; font-size: 13px; line-height: 1.5; color: var(--muted);
}
.rechead__acts { display: flex; align-items: center; gap: 14px; flex: none; }
.rechead__cancel { font-size: 12.5px; color: var(--muted); }
.rechead__cancel:hover { color: var(--green-btn); }
/* ⚠ `.page--admin .submit` IS width:100% AND margin-top:18px, which is right at
   the foot of a form and wrong beside a heading. Both are restated rather than
   a new button being invented, so the two saves stay the same control. */
.page--admin .rechead__save { width: auto; margin-top: 0; padding: 11px 20px; }

/* THE GROUP HEADING'S OWN NOTE, sitting under `.lbl--group` and above the first
   field. One sentence for the whole group, which is what stops the same
   family-keyed warning being repeated on three fields. */
.page--admin .note--group { margin: 2px 0 0; }

/* ⚠ THE ONE HINT KEPT IN GOLD. Every other help line under a contact field was
   cut; this one stays because getting the parent email wrong emails somebody
   else's family a child's report. --gold-ink is the readable gold, 5:1 on
   paper, never --gold itself. */
.page--admin .note--keep { color: var(--gold-ink); }

@media (max-width: 760px) {
  /* ⚠ ONE PAIR SURVIVES THE PHONE: the rate and the method, because both are
     short and they are read together. `.pair` turns to a column at this width,
     so this row states every property it needs to win, flex-direction included,
     exactly as the block above this file's roster overrides does.

     THE HANDLE TAKES ITS OWN LINE. A third column at 390px is a column of about
     110px, and a Zelle identifier is an email address. */
  .pair--bill { flex-direction: row; flex-wrap: wrap; gap: 12px; }
  .pair--bill > div { flex: 1 1 calc(50% - 6px); min-width: 0; }
  .pair--bill > .fhandle { flex: 1 1 100%; margin-top: 0; }
}

/* ── THE TWO FOLDED PANELS AT THE FOOT OF THE STUDENT RECORD ─────────────────
   design_handoff_edit_student Part 5. Family credit and Text reminders, closed
   to a row each and carrying their state.

   ⚠ NATIVE <details>, WHICH IS WHY THEY ARE INDEPENDENT. Two elements with no
   shared state: opening one cannot close the other, there is nothing for a
   script to coordinate, and the keyboard and no-script cases are the platform's
   rather than ours. */
.fold {
  margin-top: 14px;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--off-white);
}
.fold + .fold { margin-top: 10px; }
.fold__row {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; cursor: pointer;
  /* The UA marker is replaced by .fold__go, so it is taken off both ways: the
     standard property and WebKit's pseudo element, which ignores it. */
  list-style: none;
}
.fold__row::-webkit-details-marker { display: none; }
.fold__name {
  font-family: var(--serif); font-size: 15px; color: var(--ink);
  flex: none;
}
/* THE STATE, READ WITHOUT OPENING ANYTHING. It takes the slack so the trigger
   stays pinned right whatever the row says, and truncates rather than wrapping
   the row to two lines. */
.fold__state {
  flex: 1 1 auto; min-width: 0;
  font-size: 13px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠ A FIXED WIDTH, SO THE TWO TRIGGERS MATCH. Two rows stacked with triggers of
   different widths read as two unrelated controls. The word changes with the
   state and the box does not. */
.fold__go {
  flex: none; width: 92px; text-align: center;
  padding: 8px 0; border: 1px solid var(--line); border-radius: 4px;
  background: var(--cream);
  font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--green-deep);
}
.fold__row:hover .fold__go { border-color: var(--gold); }
/* ⚠ THE WORD CHANGES, THE BOX DOES NOT. Both labels are in the markup as real
   text and one is shown per state, so the trigger keeps its 92px whichever it
   reads and the two rows stay matched open or closed. */
.fold__go--open { display: none; }
.fold[open] .fold__go--shut { display: none; }
.fold[open] .fold__go--open { display: inline; }
.fold__body {
  padding: 0 16px 16px;
  border-top: 1px solid var(--line);
}
/* The two partials inside already draw their own heading and spacing, so the
   body adds the gutter and nothing else. */
.fold__body > .fcred,
.fold__body > .txr { margin-top: 14px; }
/* ⚠ .txr KEEPS ITS OWN COMPONENT SET. It is a card with its own border, radius,
   padding and ground, specified at .txr above, and it is NOT restyled to match
   the panel around it. Only the flush edge is taken, so the card is not a second
   box drawn inside the first. */
.fold__body > .txr { border: 0; padding: 0; background: none; }

@media (max-width: 760px) {
  /* The state moves under the name rather than being ellipsed to nothing at
     390px, and the trigger keeps its width so the two rows still match. */
  .fold__row { flex-wrap: wrap; gap: 4px 12px; }
  .fold__name { flex: 1 1 auto; }
  .fold__state { flex: 1 1 100%; order: 3; white-space: normal; }
}


/* ══ SURVEY SUBMIT: THE IN-FLIGHT OVERLAY ═══════════════════════════════════
   design_handoff_submitting. Replaces the old submit state, which was a button
   label reading "Submitting..." at the bottom of a long scroll plus a page-wide
   dim. The dim reads as "broken" and the label is usually off-screen by the time
   the tutor has tapped it, so the only sign of life was somewhere they were not
   looking. This puts the feedback over the whole viewport and names the stage.

   THREE ANIMATIONS, ONE SHELL. 1a the pour, 1b the abacus, 1c the draft writing
   itself. Which one is picked is decided by the SERVER at render (see
   src/routes/survey.js) so it cannot change mid-wait. Whichever picture a tutor
   gets, the headline, the sub copy and the step line are identical — if one is
   ever edited, edit all three.

   1d, the named-stages layout, is NOT a fourth member of the rotation. It is the
   prefers-reduced-motion fallback, served instead of a frozen frame of art whose
   entire content is the motion.

   NOTHING LOADS AT SUBMIT TIME. Every one of these is CSS on divs: no image, no
   SVG file, no animation library, no webfont beyond the two the page already
   has. The overlay appears at the exact moment the connection is busy with the
   POST, and in the installed PWA it has to work from a cold worker cache.

   Written at the END of the file on purpose: every selector here is new, so
   nothing that follows can outrank it. */

/* ── 1a: the pour ─────────────────────────────────────────────────────────
   rt-tip and rt-lqrot MUST share percentages and easing. The liquid block
   counter-rotates against the beaker's tilt so its surface stays horizontal on
   screen while the vessel is tipped; if either track drifts, the surface tilts
   with the glass and the illusion dies mid-transition. */
@keyframes rt-tip {
  0%, 10%   { transform: rotate(0deg); }
  24%, 70%  { transform: rotate(32deg); }
  84%, 100% { transform: rotate(0deg); }
}
/* ⚠ THE INDEPENDENT `rotate` PROPERTY, NOT transform: rotate(). .pour__liq runs
   TWO tracks at once, the counter-rotation and the drain, and both now need to
   be composited. Two animations writing `transform` do not compose: the later
   one wins outright and the other silently does nothing. `rotate` and
   `translate` are separate animatable properties, so the two tracks stack
   instead of fighting. Safari has had them since 14.1. */
@keyframes rt-lqrot {
  0%, 10%   { rotate: 0deg; }
  24%, 70%  { rotate: -32deg; }
  84%, 100% { rotate: 0deg; }
}
/* DRAIN AND FILL ARE ONE WINDOW. rt-lqdrain (the pourer's level falling) and
   rt-fill (the receiver's rising) both move across 24%-70%, so one falls exactly
   as the other rises. Both then hold, and both reset at 89-90% — while the
   beaker is already upright, so the reset never reads as a jump. */
/* ⚠ EVERY TRACK IN THIS SECTION ANIMATES transform OR opacity AND NOTHING ELSE,
   and that is a rule rather than a style preference. See the note above
   @keyframes rt-fill for the failure that made it one. This one moved from `top`
   to translateY: same 53px of travel, same window, no layout. */
@keyframes rt-lqdrain {
  0%, 24%   { translate: 0 0;    opacity: 1; }
  70%       { translate: 0 53px; opacity: 1; }
  84%       { translate: 0 53px; opacity: 1; }
  89%       { translate: 0 53px; opacity: 0; }
  90%       { translate: 0 0;    opacity: 0; }
  96%, 100% { translate: 0 0;    opacity: 1; }
}
/* The stream lives strictly inside that same pour window. */
@keyframes rt-stream {
  0%, 25%   { opacity: 0; transform: scaleY(.14); }
  30%, 66%  { opacity: 1; transform: scaleY(1); }
  70%, 100% { opacity: 0; transform: scaleY(.14); }
}
/* ⚠ THIS IS THE ABACUS'S MECHANISM, AND THAT IS THE WHOLE POINT NOW.
 *
 * ── WHAT THE DEVICE ACTUALLY SAID ──────────────────────────────────────────
 *
 * Two rewrites of this track have failed on the same iPhone. It animated
 * `height` and did not run; it animated `clip-path` and did not run. In the same
 * flow, on the same phone, at the same moment, 1b's abacus runs perfectly. That
 * is the control case and it settles two things: the device is not the problem,
 * and reduced motion is not the problem, because both would have stopped the
 * abacus too.
 *
 * So this stops reasoning about which properties OUGHT to be cheap and copies
 * the one construction that is PROVEN to run on that exact device: a block
 * moved with `transform: translate` inside an ancestor with overflow: hidden.
 * That is all the abacus is. .pour__glass already has overflow: hidden, so the
 * glass is the rail and the liquid is the bead.
 *
 * TRANSLATED DOWN, NOT UP. The block is the full height of the glass and starts
 * pushed down by 93% of itself, so only its top 7% is inside the glass and reads
 * as a low level; at 32% the top 68% is inside. Same two levels as the height
 * animation had, same 24%-70% window as rt-lqdrain, so one still falls exactly
 * as the other rises.
 *
 * THE BUBBLES RIDE INSIDE IT and are positioned from its TOP edge, which IS the
 * liquid surface, so they still sit just under the surface and are still clipped
 * at it by this element's own overflow: hidden. That is the trick they depend
 * on and it survives the change; what does not survive is reading their
 * positions from the bottom, because the bottom is now off the glass.
 *
 * WHAT I GOT WRONG, recorded because it cost two deploys: I concluded from the
 * property split that this was a compositor question and reported it fixed
 * twice without the device agreeing. The split was real but it was not the
 * cause, and a stylesheet reading is not evidence about a phone. */
/* (superseded) Two earlier strategies for this track, named so they are not
   tried a third time: it animated `height`, and then `clip-path`. Neither ran
   on the device. The full reasoning that produced them was wrong and is not
   preserved. */
@keyframes rt-fill {
  0%, 24%   { transform: translateY(93%); opacity: 1; }
  70%       { transform: translateY(32%); opacity: 1; }
  84%       { transform: translateY(32%); opacity: 1; }
  89%       { transform: translateY(32%); opacity: 0; }
  90%       { transform: translateY(93%); opacity: 0; }
  96%, 100% { transform: translateY(93%); opacity: 1; }
}
@keyframes rt-bob {
  0%   { transform: translateY(0) scale(.8); opacity: 0; }
  35%  { opacity: .85; }
  100% { transform: translateY(-26px) scale(1.15); opacity: 0; }
}
/* ── 1b: the abacus ──────────────────────────────────────────────────────── */
@keyframes rt-bead {
  0%, 8%    { transform: translateX(0); }
  42%, 64%  { transform: translateX(160px); }
  94%, 100% { transform: translateX(0); }
}
/* ── 1c: the draft ───────────────────────────────────────────────────────── */
/* ⚠ THE ABACUS'S MECHANISM, for the reason set out above rt-fill: a bar moved
   with translate inside a box with overflow: hidden. This track has now failed
   twice on the device, animating `width` and then `scaleX`, while 1b runs
   perfectly on the same phone in the same flow. The bar is the line's own
   ::before, so the sheet's children are untouched and the nth-child stagger
   below still counts what it always counted. */
@keyframes rt-write {
  0%        { transform: translateX(-100%); opacity: .35; }
  55%       { opacity: 1; }
  70%, 100% { transform: translateX(0);     opacity: 1; }
}
@keyframes rt-caret {
  0%, 45%   { opacity: 1; }
  55%, 100% { opacity: .15; }
}
/* ── 1d and the shared step line ─────────────────────────────────────────── */
@keyframes rt-step {
  0%, 18%   { opacity: .32; }
  30%, 88%  { opacity: 1; }
  100%      { opacity: .32; }
}
@keyframes rt-ring {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
@keyframes rt-pulse {
  0%, 100% { opacity: .25; }
  50%      { opacity: .75; }
}

/* ── The overlay itself ──────────────────────────────────────────────────── */

/* THE PALETTE IS PER VARIANT, DECLARED AS LOCAL TOKENS. 1a and 1c sit on the
   deep green; 1b keeps the survey's own cream so it reads as the same page
   rather than as a modal from somewhere else. Every rule below paints from
   these, so the reduced-motion block at the bottom of this section can repaint
   the whole overlay by rewriting six values. */
.subst--pour, .subst--draft {
  --subst-ground:  var(--green-deep);
  --subst-head:    var(--off-white);
  --subst-sub:     rgba(255, 255, 255, .62);
  --subst-bar-on:  var(--gold-light);
  --subst-bar-off: rgba(255, 255, 255, .2);
  --subst-step:    rgba(255, 255, 255, .55);
}
.subst--abacus {
  --subst-ground:  var(--cream);
  --subst-head:    var(--ink);
  --subst-sub:     var(--muted);
  --subst-bar-on:  var(--green-btn);
  --subst-bar-off: rgba(27, 67, 50, .16);
  --subst-step:    var(--gold-dark);
}

/* ONE SCALE VARIABLE, AND THE STAGE'S RESERVED HEIGHT DERIVED FROM IT.
   1a's geometry is computed — the lip position, the stream's start, the 32°
   angle, 1b's 160px bead travel — so a breakpoint must SCALE the art, never
   re-derive it. A scaled transform does not affect layout size, so the stage's
   parent has to be told how much room the painted version needs; deriving that
   from the same two numbers is what stops the two from drifting apart the way
   they would if both were typed out per breakpoint. */
.subst {
  --subst-scale: 1;
  --subst-art-h: 220px;
}
@media (min-width: 760px) and (min-height: 500px)  { .subst { --subst-scale: 1.35; } }
@media (min-width: 1200px) { .subst { --subst-scale: 1.6; } }
@media (max-width: 360px)  { .subst { --subst-scale: .9; } }
/* Landscape phones and short desktop windows. Last, so it beats the width steps
   above on a viewport that matches both: a 844x390 phone is "wide" by the first
   rule and would take 1.35, which puts the step line off the bottom — the exact
   failure this screen exists to fix. */
@media (max-height: 620px) { .subst { --subst-scale: .72; } }

.subst--abacus { --subst-art-h: 156px; }
.subst--draft  { --subst-art-h: 280px; }

.subst {
  position: fixed; inset: 0; z-index: 300;
  /* 100dvh, NOT 100vh. On mobile Safari 100vh includes the collapsing address
     bar, so the step line at the bottom sits below the fold — which is the exact
     failure being fixed. */
  height: 100dvh;
  display: none;
  flex-direction: column; align-items: center;
  background: var(--subst-ground);
  font-family: var(--sans);
  /* In the installed PWA there is no browser chrome, so the overlay runs under
     the status bar. */
  padding-top: max(14px, var(--sat));
  overscroll-behavior: contain;
  touch-action: none;
}
/* THE ATTRIBUTE MUST WIN OVER THE DISPLAY ABOVE. `[hidden]` is a user-agent
   rule; an author-origin `display` on the same element beats it and the element
   stays on screen. That is not hypothetical here — it is exactly how the
   deactivated roster rows shipped visible to production. .subst defaults to
   display:none and only .subst--on shows it, so this is the belt to that brace,
   at a specificity (class + attribute) that outranks the modifier. */
.subst[hidden] { display: none; }
.subst--on { display: flex; }

/* The form's own dim (form.submitting) must never reach the overlay, which is
   why the overlay is a sibling of the form rather than a child of it. */
body.subst-lock { overflow: hidden; }

/* The middle takes the slack and centres in it; the foot is a flex item pinned
   after it rather than an absolutely positioned bar. Positioned this way the two
   cannot overlap at any height, which is what makes "the step line is visible
   without scrolling" a property of the layout rather than of a measurement. */
.subst__main {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 20px;
}
.subst__art { display: flex; flex-direction: column; align-items: center; }

.subst__stagewrap {
  display: flex; align-items: center; justify-content: center;
  min-height: calc(var(--subst-art-h) * var(--subst-scale) + 24px);
}
.subst__stage { position: relative; flex: none; transform-origin: 50% 50%; transform: scale(var(--subst-scale)); }

/* Centred and capped: a status line should not stretch across a monitor. */
.subst__body {
  margin-top: 40px; max-width: 460px; margin-inline: auto; text-align: center;
  display: flex; flex-direction: column; gap: 11px;
}
.subst__head {
  margin: 0; font-family: var(--serif); font-size: 23px; font-weight: 400;
  line-height: 1.3; color: var(--subst-head);
}
.subst__sub { margin: 0; font-size: 13px; line-height: 1.6; color: var(--subst-sub); }
@media (min-width: 1200px) {
  /* The headline grows; the sub copy does NOT. It is a status line, not a
     paragraph. */
  .subst__head { font-size: 28px; }
}

/* THE STEP LINE IS THE PART A LOOP CANNOT DO ON ITS OWN: it says which part is
   slow. Bar 1 done, bar 2 current and pulsing, bar 3 still to come. */
.subst__foot {
  flex: none; width: 100%;
  padding: 20px 30px calc(26px + var(--sab));
  display: flex; flex-direction: column; gap: 9px;
}
.subst__bars { display: flex; gap: 6px; }
.subst__bars > span { flex: 1; height: 3px; border-radius: 2px; background: var(--subst-bar-on); }
.subst__bars > span:nth-child(2) { animation: rt-pulse 1.6s ease-in-out infinite; }
.subst__bars > span:nth-child(3) { background: var(--subst-bar-off); animation: none; }
.subst__step {
  margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .1em;
  color: var(--subst-step);
}

/* ── 1a, the pour: geometry ──────────────────────────────────────────────────
   Stage 320x220. Computed, not eyeballed — the lip has to land over the
   receiving beaker's mouth AT THE TILTED ANGLE, or the stream starts in mid-air:

     receiver  92x118 at left:163, bottom:0   -> mouth at y=102, spanning x 163-255
     pourer    78x92  at left:72,  top:32     -> transform-origin at its
               bottom-right, the corner nearest the receiver; tipping 32deg puts
               the lip at ~(196, 46)
     stream    5x56   at left:196, top:46     -> runs lip (46) to mouth (102)

   32deg IS DELIBERATE. At 48deg a rectangular beaker reads as a diamond rather
   than a vessel.

   EVERY TRACK RUNS 5s. An earlier draft had the receiver's fill at 4.6s while
   everything else was 5s; the two beakers were then on different-length cycles
   and wandered out of phase no matter what else was done. Change one duration
   and you must change all five. */
.subst__stage--pour { width: 320px; height: 220px; }

.pour__jug {
  position: absolute; left: 72px; top: 32px; width: 78px; height: 92px;
  transform-origin: 100% 100%;
  animation: rt-tip 5s cubic-bezier(.4, .02, .4, 1) infinite;
}
.pour__cup { position: absolute; left: 163px; bottom: 0; width: 92px; height: 118px; }

.pour__glass {
  position: absolute; inset: 0;
  border: 2.5px solid rgba(227, 181, 58, .72); border-top: none;
  overflow: hidden;
}
.pour__jug .pour__glass { border-radius: 3px 3px 11px 11px; }
.pour__cup .pour__glass { border-radius: 3px 3px 13px 13px; }

/* The lip, drawn wider than the vessel so it reads as a rim rather than as the
   top of the wall. */
.pour__rim {
  position: absolute; top: -2px; left: -7px; right: -7px; height: 2.5px;
  background: rgba(227, 181, 58, .72); border-radius: 2px;
}
/* Graduation marks. Small, but they are half of what stops the beaker reading
   as clip art. */
.pour__grad { position: absolute; left: -1px; right: -1px; height: 1px; background: rgba(227, 181, 58, .3); }
.pour__grad--a { bottom: 34px; }
.pour__grad--b { bottom: 58px; }

/* THE LIQUID MUST STAY LEVEL. A large block inside the beaker's overflow:hidden
   box, counter-rotated against the tilt, so its surface stays horizontal on
   screen while the vessel is tipped. It is bigger than the beaker in both
   directions on purpose: at 32deg the corners of a box that merely fitted would
   swing inside the glass and show as wedges of empty. */
.pour__liq {
  position: absolute; left: -90px; width: 258px; height: 240px; top: 35px;
  overflow: hidden; background: var(--gold-light); transform-origin: 50% 0;
  animation: rt-lqrot 5s cubic-bezier(.4, .02, .4, 1) infinite,
             rt-lqdrain 5s cubic-bezier(.4, .02, .4, 1) infinite;
}
/* FULL HEIGHT NOW, and clipped rather than grown. `top: 0` is what makes the
   box the whole glass, so rt-fill's inset percentages mean what they say; the
   height used to come from the animation itself, which is exactly the layout
   animation being removed. */
/* THE LIQUID IS THE BEAD AND THE GLASS IS THE RAIL. Full height of the glass,
   translated down so only its top band shows, and clipped by .pour__glass's
   overflow: hidden exactly as .aba__rail clips .aba__beads. Its own
   overflow: hidden is what clips the bubbles at the surface. */
.pour__fill {
  position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: hidden;
  background: var(--gold-light);
  transform: translateY(93%);
  animation: rt-fill 5s cubic-bezier(.35, .02, .5, 1) infinite;
}
.pour__stream {
  position: absolute; left: 196px; top: 46px; width: 5px; height: 56px;
  background: linear-gradient(180deg, rgba(227, 181, 58, .9), var(--gold-light));
  border-radius: 0 0 3px 3px; transform-origin: top center;
  animation: rt-stream 5s cubic-bezier(.3, 0, .5, 1) infinite;
}

/* BUBBLES ARE CHILDREN OF THE LIQUID, NOT OF THE BEAKER. That is the whole
   trick: the liquid has overflow:hidden, so a bubble pops AT THE SURFACE instead
   of floating past it, and the receiver's bubbles use `bottom` in percentages so
   they spread upward as the level grows. The pourer's sit inside the
   counter-rotating block, so they stay upright through the tilt and clip at that
   surface too. */
.pour__liq span, .pour__fill span {
  position: absolute; border-radius: 50%;
  animation-name: rt-bob; animation-timing-function: ease-in; animation-iteration-count: infinite;
}
.pour__liq span { background: rgba(253, 252, 249, .9); }
.pour__fill span { background: var(--off-white); }

/* Six in the pourer. Their visible x-window is local 90-168; outside it the
   beaker's own clip hides them. */
.pour__liq span:nth-child(1) { left: 102px; top: 14px; width: 5px; height: 5px; animation-duration: 2.2s; animation-delay: .3s; }
.pour__liq span:nth-child(2) { left: 116px; top: 26px; width: 3px; height: 3px; animation-duration: 1.8s; animation-delay: 1.2s; }
.pour__liq span:nth-child(3) { left: 126px; top: 12px; width: 6px; height: 6px; animation-duration: 2.6s; animation-delay: .8s; }
.pour__liq span:nth-child(4) { left: 138px; top: 30px; width: 4px; height: 4px; animation-duration: 2s;   animation-delay: 1.7s; }
.pour__liq span:nth-child(5) { left: 148px; top: 18px; width: 5px; height: 5px; animation-duration: 2.4s; animation-delay: 2.2s; }
.pour__liq span:nth-child(6) { left: 110px; top: 40px; width: 3px; height: 3px; animation-duration: 1.6s; animation-delay: 2.6s; }

/* Nine in the receiver. Sizes 3-7px, durations 1.6-2.8s, delays staggered out to
   2.6s so no pattern is visible.

   ⚠ THE PERCENTAGES ARE OF THE GLASS NOW, NOT OF THE LIQUID. They used to be
   read against a box that grew to 68% of the glass, so a bubble at 52% sat at
   35% of the glass at the fullest. The box is full height now and clipped, so
   every value here is its old one times .68 and each bubble sits exactly where
   it used to at the top of the pour. Below that level the clip hides it, which
   is the same behaviour the growing box gave and the reason they still pop at
   the surface. */
.pour__fill span:nth-child(1) { left: 14px; top: 6%;  width: 7px; height: 7px; animation-duration: 1.9s; animation-delay: 0s; }
.pour__fill span:nth-child(2) { left: 26px; top: 20%; width: 4px; height: 4px; animation-duration: 2.3s; animation-delay: 1.1s; }
.pour__fill span:nth-child(3) { left: 34px; top: 11%; width: 6px; height: 6px; animation-duration: 2.6s; animation-delay: .4s; }
.pour__fill span:nth-child(4) { left: 45px; top: 30%; width: 3px; height: 3px; animation-duration: 1.7s; animation-delay: 1.6s; }
.pour__fill span:nth-child(5) { left: 52px; top: 8%;  width: 5px; height: 5px; animation-duration: 2.4s; animation-delay: .7s; }
.pour__fill span:nth-child(6) { left: 62px; top: 24%; width: 4px; height: 4px; animation-duration: 2.1s; animation-delay: 1.3s; }
.pour__fill span:nth-child(7) { left: 70px; top: 14%; width: 6px; height: 6px; animation-duration: 2.8s; animation-delay: 1.9s; }
.pour__fill span:nth-child(8) { left: 20px; top: 38%; width: 5px; height: 5px; animation-duration: 2.2s; animation-delay: 2.4s; }
.pour__fill span:nth-child(9) { left: 57px; top: 46%; width: 3px; height: 3px; animation-duration: 1.8s; animation-delay: 2.1s; }

/* A hairline across the top of the dark ground, as in the mock. On .subst
   rather than on .subst__main so it sits at the true top edge of the screen
   rather than below the safe-area padding. */
.subst__main { position: relative; }
.subst--pour::before, .subst--draft::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: rgba(227, 181, 58, .3);
}

/* ── 1b, the abacus ──────────────────────────────────────────────────────────
   Three rails, three beads each, staggered .32s so they travel as a wave rather
   than in unison.

   THE 160px IS THE RAIL WIDTH MINUS THE BEAD GROUP: 236 - (3 x 22 + 2 x 5) =
   236 - 76 = 160. An earlier draft used 120px and the beads stopped in the
   middle of the rail, which looked like a bug rather than like counting. If the
   card's width ever changes instead of being scaled, this has to be recomputed.

   The card is sized to a 236px CONTENT width under this file's global
   box-sizing:border-box — 236 + 20 + 20 of padding + 2 of border = 278. */
.subst__stage--abacus { width: 278px; height: 156px; }

.aba__card {
  position: absolute; inset: 0; width: 278px; height: 156px;
  padding: 20px 20px 22px;
  background: var(--off-white); border: 1px solid rgba(27, 67, 50, .16);
  border-radius: 8px; box-shadow: 0 12px 34px rgba(20, 55, 42, .09);
  display: flex; flex-direction: column; gap: 17px;
}
.aba__rail { position: relative; height: 26px; display: flex; align-items: center; }
.aba__rail::before {
  content: ''; position: absolute; left: 0; right: 0; height: 2px;
  background: rgba(27, 67, 50, .2); border-radius: 2px;
}
.aba__beads {
  display: flex; gap: 5px;
  animation: rt-bead 2.8s cubic-bezier(.5, .02, .4, 1) infinite;
}
.aba__beads span { width: 22px; height: 22px; border-radius: 50%; background: var(--green-btn); }
/* THE STAGGER IS A NEGATIVE DELAY, and the two are not interchangeable.
   A delay of +.32s holds the rail at its first keyframe for .32s before it
   moves; -2.48s (one full 2.8s cycle less .32s) puts it at the SAME point of the
   wave but leaves it already running from the first frame. Steady state is
   identical, and 2.48 + .32 = 2.8 is the whole of the arithmetic.

   Why it is written the second way: an installed PWA on iPhone showed the top
   rail travelling and rails 2 and 3 frozen. Rail 1 is the only one with no
   delay. That is not proof — the resting positions differed between
   submissions, which points at an animation that ran and then stopped rather
   than one that never started — but a rail that is never waiting to begin
   cannot be dropped while it waits, and it costs nothing to remove the window.
   If you change the 2.8s duration, both numbers here have to move with it. */
.aba__rail:nth-child(2) .aba__beads { animation-delay: -2.48s; }
.aba__rail:nth-child(2) .aba__beads span { background: var(--gold); }
.aba__rail:nth-child(3) .aba__beads { animation-delay: -2.16s; }

/* ── 1c, the draft writing itself ────────────────────────────────────────────
   Six lines on a 214x272 sheet rotated -1.4deg, each line's width animating
   0 -> 100% on a .22s stagger, and a gold caret on the live line.

   The stage is 224x280 rather than 214x272: rotating the sheet grows its
   bounding box to about 220.5 x 277.1, and a stage that merely matched the sheet
   would clip its corners. */
.subst__stage--draft { width: 224px; height: 280px; }

.draft__sheet {
  position: absolute; left: 5px; top: 4px; width: 214px; height: 272px;
  background: var(--off-white); border-radius: 4px;
  box-shadow: 0 18px 40px rgba(3, 20, 10, .42);
  transform: rotate(-1.4deg);
  padding: 26px 22px;
  display: flex; flex-direction: column; gap: 13px; overflow: hidden;
}
.draft__title { height: 9px; width: 64px; border-radius: 2px; background: var(--gold); flex: none; }
/* ⚠ WIDTH IS STATIC AND THE SCALE IS WHAT MOVES. The line lays out at its full
   width and is scaled from the left edge, so the writing reads exactly as it did
   while the animated property is one a compositor can run. Without the origin it
   would grow from the middle outwards, which reads as nothing anybody writes. */
.draft__line {
  position: relative; overflow: hidden;
  height: 7px; border-radius: 2px; flex: none;
  width: 100%;
}
/* The ink. It is the ::before that travels, so the line keeps its own box and
   the animation is a translate inside a clip. The animation is declared here
   rather than on the line, which is why the nth-child delays below still land:
   animation-delay inherits nothing, so they are re-targeted at the pseudo. */
.draft__line::before {
  content: ''; position: absolute; inset: 0;
  background: rgba(27, 67, 50, .5);
  transform: translateX(-100%);
  animation: rt-write 3.6s ease-out infinite;
}
.draft__line--short { width: 78%; }
.draft__line--stub  { width: 56%; }
/* nth-CHILD of the sheet, not nth-of-type: every child here is a div, so
   nth-of-type would count the title and the footer too and stagger the wrong
   five elements. Children are 1 title, 2-6 lines, 7 the live line, 8 the foot. */
.draft__sheet .draft__line:nth-child(3)::before { animation-delay: .22s; }
.draft__sheet .draft__line:nth-child(4)::before { animation-delay: .44s; }
.draft__sheet .draft__line:nth-child(5)::before { animation-delay: .66s; }
.draft__sheet .draft__line:nth-child(6)::before { animation-delay: .88s; }
.draft__live { display: flex; align-items: center; gap: 5px; flex: none; }
/* THE SIXTH LINE ANIMATES LIKE THE OTHER FIVE, and the caret rides its end.
   The reference file gives this one flex:1, which in a flex row means
   flex-basis:0 and a used width taken from the grow share — so the animated
   width is never consulted and the "live" line sits finished while the five
   above it are still being written, which reads backwards. The README asks for
   six lines going 0 -> 100% on a .22s stagger; this is the sixth. flex-shrink
   stays at its default so the caret's 2px is never squeezed off the sheet. */
/* ⚠ flex:1 IS NOW THE RIGHT ANSWER, AND IT WAS THE WRONG ONE BEFORE. The note
   above is about animating WIDTH: with flex-basis:0 the used width came from the
   grow share, the animated width was never consulted, and the live line sat
   finished while the five above it were still being written. The animation is
   scaleX now, so the flex layout decides the line's full width and the transform
   writes it: the line takes the row's remaining space beside the caret and
   scales from its left edge like the other five. flex-shrink stays default so
   the caret's 2px is never squeezed off the sheet. */
.draft__live .draft__line { flex: 1 1 auto; width: auto; }
.draft__live .draft__line::before { animation-delay: 1.1s; }
.draft__caret { width: 2px; height: 14px; flex: none; background: var(--gold); animation: rt-caret .9s steps(1, end) infinite; }
.draft__foot { margin-top: auto; display: flex; flex-direction: column; gap: 7px; }
.draft__rule { height: 1px; background: rgba(27, 67, 50, .14); }
.draft__sig  { height: 6px; width: 44%; border-radius: 2px; background: rgba(201, 162, 39, .55); }

/* ── 1d, the named stages: the reduced-motion fallback ───────────────────────
   NOT a fourth member of the rotation. Stopping the motion on 1a/1b/1c and
   keeping the composition leaves a tutor staring at a still beaker, because for
   a loop whose entire content is the motion there is nothing left once it stops.
   So a reader who asked for no motion gets a different screen: the same three
   facts, named, with the one that already succeeded ticked off.

   Hidden by default and revealed only inside the reduced-motion query below, so
   the markup can ship in every variant and the choice stays a media query rather
   than something the server has to guess. */
.subst__stages { display: none; }

.subst__stagehead { display: flex; align-items: center; gap: 13px; }
.subst__almost {
  font-family: var(--serif); font-size: 25px; line-height: 1.2; color: var(--ink);
}
.subst__ring {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  border: 2.5px solid rgba(27, 67, 50, .15); border-top-color: var(--green-btn);
  animation: rt-ring .9s linear infinite;
}
.subst__list { list-style: none; margin: 26px 0 0; padding: 0; }
.subst__row {
  display: flex; align-items: center; gap: 13px; padding: 15px 0;
  border-bottom: 1px solid rgba(27, 67, 50, .1);
}
.subst__row:last-child { border-bottom: 0; }
.subst__row--now { animation: rt-step 2.2s ease-in-out infinite; }
.subst__rowlabel { font-size: 14.5px; color: var(--ink); }
.subst__row--now .subst__rowlabel { font-weight: 600; }
.subst__row--next .subst__rowlabel { color: #A9B1AA; }
.subst__mark {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  background: var(--green-btn); color: var(--off-white);
  display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.subst__spin {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid rgba(27, 67, 50, .18); border-top-color: var(--gold);
  animation: rt-ring 1s linear infinite;
}
.subst__todo {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 1.5px dashed rgba(27, 67, 50, .25);
}
.subst__stagenote { margin: 26px 0 0; font-size: 13px; line-height: 1.7; color: var(--muted); }

/* THE EIGHT-SECOND NOTE. It appears only once .subst--waited is set, which
   public/survey.js does on a timer after the overlay opens.

   Revealed by a CLASS rather than by removing a `hidden` attribute, deliberately.
   This element has an author-origin display, and an author display beats the user
   agent's [hidden] rule — which is exactly how the deactivated roster rows once
   shipped visible. With no attribute in play there is nothing for a display to
   beat.

   The sentence is only true because claimSurveySubmission() commits the survey in
   its own transaction BEFORE draftAndRouteReport() is awaited: by the time this
   shows, the answers are durable and the report is already in the queue. If that
   ordering ever changes, cut the sentence. */
.subst__wait {
  display: none;
  margin: 0; padding: 13px 15px;
  background: var(--off-white);
  border: 1px solid rgba(27, 67, 50, .14); border-left: 3px solid var(--gold);
  border-radius: 5px;
  font-size: 12.5px; line-height: 1.6; color: var(--muted);
}
.subst--waited .subst__wait { display: block; }

/* ── prefers-reduced-motion ──────────────────────────────────────────────────
   LAST IN THIS SECTION, so its token rewrites beat the per-variant blocks above
   on equal specificity. Whichever animation the server picked, a reader who
   asked for no motion gets 1d on the cream ground instead. */
@media (prefers-reduced-motion: reduce) {
  .subst--pour, .subst--abacus, .subst--draft {
    --subst-ground: var(--cream);
    --subst-head:   var(--ink);
    --subst-sub:    var(--muted);
  }
  /* The art, its copy and the step bars all belong to the composition being
     replaced; 1d carries its own headline, its own stage list and its own note. */
  .subst__art, .subst__foot { display: none; }
  .subst__stages {
    display: flex; flex-direction: column; justify-content: center;
    position: relative;
    flex: 1 1 auto; width: 100%; max-width: 460px;
    padding: 0 14px calc(96px + var(--sab));
    text-align: left;
  }
  /* The gold hairline belongs to the dark ground. 1d is on cream, and the
     variant class is still on the element, so it has to be turned off here. */
  .subst--pour::before, .subst--draft::before { content: none; }

  /* Pinned to the bottom of the panel, as in the mock. Only position is set
     here: the display rules above hold inside a media query unchanged, so
     restating them would only create a second place to keep in sync. */
  .subst__wait {
    position: absolute; left: 14px; right: 14px;
    bottom: calc(26px + var(--sab));
  }
}

/* WHAT STILL MOVES ON 1d, AND WHY IT IS ALLOWED TO. Two 22-26px rings and an
   opacity pulse on the current row. Reduced motion is a request not to be moved
   — large sweeps, travel, parallax — rather than a request to be told nothing is
   happening, and 1d exists precisely because a screen with no sign of life is
   the complaint being fixed. Nothing here moves a large area or travels across
   the viewport. */