/* iniSHial — notebook theme.
 *
 * Token set adapted from bookSHelf's `notebook` theme
 * (scripts/workflows/templates/themes/notebook/tokens.json), itself derived
 * from the "Notebook Tabs" preset in zarazhangrui/frontend-slides via Open
 * Design's fs-notebook-tabs example. MIT. See ATTRIBUTION.md.
 *
 * Handwriting is an ACCENT, never the body. This document is one a parent has
 * to read and legally initial -- a script face for policy text would hurt
 * comprehension and, on a small phone, exclude readers outright. Caveat is
 * used for initials, margin notes, and the masthead only.
 *
 * THE ACCENT IS A HIGHLIGHTER, NOT A COLOUR. Emphasis never recolours text:
 * every word on the page is --ink, and importance is carried by a marker band
 * or a strip of tape BEHIND the words. That is one contrast pair to get right
 * instead of a dozen, it survives both themes unchanged, and it is the one
 * emphasis a teacher actually makes on paper.
 */

@font-face {
  font-family: 'Caveat';
  /* Variable font, 400-700. Self-hosted: no CDN, per the open-source rule.
     Declared as plain truetype -- some Safari versions reject the
     -variations form and drop the face entirely. */
  /* WOFF2 first, the .ttf as the fallback for anything that cannot take it.
     The .ttf shipped raw at 403,648 bytes for a face used only as an accent --
     the masthead, the initials stamp, the margin notes -- and every page paid
     it. Subset to Latin-1 plus the punctuation a syllabus and a parent's
     initials can actually contain (753 glyphs -> 209) and compressed to WOFF2,
     it is 75,036: an 81% cut on the largest asset the parent path downloads.

     The wght 400-700 axis survives the subset; it is the same variable face,
     just carrying fewer glyphs. font-display: swap below means the text is
     readable throughout either way, so this buys reflow-free reading sooner
     rather than fixing a blank-text problem.

     Regenerate with:
       pyftsubset public/fonts/Caveat.ttf --output-file=public/fonts/Caveat.woff2 \
         --flavor=woff2 --layout-features='*' --name-IDs='*' \
         --unicodes="U+0020-007E,U+00A0-00FF,U+2010-2015,U+2018-201A,U+201C-201E,\
U+2020-2022,U+2026,U+2030,U+2039,U+203A,U+20AC,U+2122" */
  src: url('/fonts/Caveat.woff2') format('woff2'),
       url('/fonts/Caveat.ttf') format('truetype');
  font-weight: 400 700;
  font-display: swap;
}

/* ---- palette ----
 *
 * Light values live here unconditionally, so a browser without light-dark()
 * still gets a complete, readable cream sheet rather than a page of
 * guaranteed-invalid custom properties (which would compute to nothing and
 * take the body text with them). The dark half is layered on below.
 */
:root {
  --paper: #f8f6f1;
  --ivory: #fdfcf9;
  --sand: #efece3;
  --desk: #ded5c2;
  --desk-deep: #cdc2ab;
  --desk-ink: #3a3833;

  --ink: #1a1a1a;
  --ink-charcoal: #3a3833;
  --ink-olive: #4d4a43;
  --ink-stone: #6b6358;
  /* Was #a8a297, which measured 2.35:1 on --paper and 2.15:1 on --sand: a long
     way under the 4.5:1 floor, and it carries real text -- the editor's
     "Unsaved changes" / "Saved", the settings hint, the initials placeholder.
     Darkened to the lightest value that clears 4.5:1 on paper, ivory AND sand.

     That lands it very near --ink-stone, and the honest reading is that on this
     paper there is no room for a third, lighter ink that is still readable:
     every place this token is used is small text, so none of them qualify for
     the 3:1 large-text allowance. Kept as its own token rather than merged, so
     the eight call sites keep meaning "the lightest ink still legible" -- but
     the two are close enough now that merging them would lose nothing. */
  --ink-silver: #6e6b63;

  --border-cream: #efece3;
  --border-warm: #e4e0d5;
  --ring: #d8d2c4;

  /* The marker. --mark is the band, --mark-deep its edge and the colour of any
     line or outline that has to read as "the accent" without being text. */
  --mark: #ffe08a;
  --mark-deep: #b98d24;
  --tape: #eee2c6;
  --accent: #7a5c1f;
  --accent-deep: #5c451a;
  --accent-soft: #d8b45f;
  --focus: #2f6f9e;

  --danger: #8d3346;

  /* Index-tab hues — the notebook's wayfinding spine, order preserved */
  --tab-1: #98d4bb;
  --tab-2: #c7b8ea;
  --tab-3: #f4b8c5;
  --tab-4: #a8d8ea;
  --tab-5: #ffe6a7;

  --shadow-paper: 0 1px 2px rgba(0,0,0,.05), 0 12px 40px rgba(0,0,0,.16);
  --shadow-soft: 0 4px 24px rgba(0,0,0,.05);

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;

  --font-display: "Bodoni Moda", "Bodoni MT", "Didot", "Playfair Display",
                  "Iowan Old Style", Georgia, "Times New Roman", serif;
  --font-body: "DM Sans", "Helvetica Neue", "Segoe UI", Helvetica, Arial, system-ui, sans-serif;
  --font-hand: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* ---- the baseline grid ----
   *
   * --rule is the ruled-line pitch AND the unit every vertical measurement on
   * the page is built from. Body line-height is exactly one rule, so successive
   * lines of prose land on successive rules; anything between two paragraphs
   * (a heading, a card, a button, a form field) must consume a WHOLE number of
   * rules or every line after it drifts off the paper.
   *
   * --baseline is how far a 17px/32px line box's baseline sits below its own
   * top edge. It is the one number here that comes from the FONT rather than
   * from arithmetic, so it moves a little with whatever the reader actually has
   * installed. It is measured, not guessed -- see the drift figures in
   * ATTRIBUTION.md. Chasing an exact zero would mean self-hosting DM Sans,
   * which is a font download on every parent's phone to move text one pixel.
   *
   * Everything downstream follows from those: shift the ruled background down
   * by --baseline + --sit, and any line box starting at a multiple of --rule
   * from the top of the content box has a rule drawn just under its baseline.
   */
  --rule: 32px;
  --baseline: 23px;

  /* How far ABOVE the rule the text sits. Writing on lined paper rests on the
     line, not through it -- with --sit at 0 the rule cuts the descenders of
     every g, p and y on the page. 3px clears them and still reads as sitting
     on the line rather than floating. */
  --sit: 3px;

  /* The gap between the box grid and the DRAWN rules -- 6px.
   *
   * The ruled background is pushed down by --baseline + --sit so text baselines
   * land on it. That is right for text and wrong for anything with a fill: a
   * banner, a drop zone, a log box aligned to the box grid has its edges drawn
   * 6px BELOW the rules, so a line appears to cut across it just short of the
   * bottom. Lift such a box by this and its edges land on the lines.
   *
   * Use it as `position: relative; top: calc(-1 * var(--lift))` -- that moves
   * the paint without moving the box, so nothing below shifts off the grid.
   * Where there is a WRAPPER to put it on instead, prefer .on-rules: padding
   * costs no positioning and keeps the wrapper a whole number of rules. */
  --lift: calc(var(--rule) - var(--baseline) - var(--sit));

  --swipe: 420ms cubic-bezier(.22, 1, .36, 1);

  /* Overwritten by ui.js with the nav's measured height. The 0 fallback is what
     a no-JS reader gets: the other sticky bars land at the very top and the nav
     scrolls away, which is worse than ideal but never overlapping. */
  --nav-h: 0px;
}

/* light-dark() carries BOTH themes in one declaration, so a token can never
   drift between them -- the alternative is a second @media block holding a
   duplicate of every value, which is exactly how a dark theme ends up with one
   stale colour nobody notices. Browsers without it keep the light block above.
   The toggle then only has to set `color-scheme`; it never touches a colour. */
@supports (color: light-dark(#fff, #000)) {
  :root {
    color-scheme: light dark;

    --paper: light-dark(#f8f6f1, #1e1e1e);
    --ivory: light-dark(#fdfcf9, #26251f);
    --sand: light-dark(#efece3, #33322e);
    --desk: light-dark(#ded5c2, #141413);
    --desk-deep: light-dark(#cdc2ab, #0e0e0d);
    --desk-ink: light-dark(#3a3833, #ede8dc);

    --ink: light-dark(#1a1a1a, #ede8dc);
    --ink-charcoal: light-dark(#3a3833, #cfcabd);
    --ink-olive: light-dark(#4d4a43, #b3ada0);
    --ink-stone: light-dark(#6b6358, #a29c8e);
    /* Dark had the same problem the other way: #6f6a5f measured 2.86:1 on the
       dark --ivory and 2.38:1 on --sand. Lightened to clear 4.5:1 on all three. */
    --ink-silver: light-dark(#6e6b63, #9c9991);

    --border-cream: light-dark(#efece3, rgba(255,255,255,.06));
    --border-warm: light-dark(#e4e0d5, rgba(255,255,255,.11));
    --ring: light-dark(#d8d2c4, rgba(255,255,255,.16));

    /* The dark band is the same highlighter seen through a dim room: same hue,
       dropped far enough that --ink (near-white) rides it at ~7:1. */
    --mark: light-dark(#ffe08a, #6d5717);
    --mark-deep: light-dark(#b98d24, #d9b354);
    --tape: light-dark(#eee2c6, #46402f);
    --accent: light-dark(#7a5c1f, #e6c67f);
    --accent-deep: light-dark(#5c451a, #f0d79c);
    --accent-soft: light-dark(#d8b45f, #8a6f28);
    --focus: light-dark(#2f6f9e, #7fbde6);

    --danger: light-dark(#8d3346, #e79aab);
  }
  /* The whole toggle. Two declarations, because every colour above already
     knows what to do once the scheme is pinned. */
  :root[data-theme="light"] { color-scheme: light; }
  :root[data-theme="dark"] { color-scheme: dark; }
}

/* The page's drop shadow is the one token light-dark() cannot carry -- it is a
   comma-separated shorthand, and light-dark() takes exactly two values. So this
   one restates, and it is the ONLY thing that does. A stale shadow is a
   cosmetic bug; a stale colour would be an unreadable page. */
:root[data-theme="dark"] { --shadow-paper: 0 1px 2px rgba(0,0,0,.4), 0 12px 40px rgba(0,0,0,.5); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --shadow-paper: 0 1px 2px rgba(0,0,0,.4), 0 12px 40px rgba(0,0,0,.5); }
}

* { box-sizing: border-box; }

/* `hidden` means hidden.
   The browser's own rule for it is `[hidden] { display: none }` in the UA
   sheet, which ANY author rule outranks -- and this sheet has several that do:
   `button, .button` sets `display: inline-flex`, `.book-bar` sets `flex`. So
   `el.hidden = true` on a button or a page rail set the attribute, passed every
   assertion, and changed nothing on screen. The editor's page bar had been
   failing to hide that way since it was written.
   !important because the point is to beat those rules wherever they are; the
   two places that deliberately un-hide something (the print sheet, below) carry
   more specificity and still win. */
[hidden] { display: none !important; }

/* 17px rather than the browser's 16px default. This is a document a parent
   reads once, carefully, on a phone, and every rem-based measurement on the
   page scales with it. */
html { font-size: 17px; }

/* The desk the notebook sits on. */
body {
  margin: 0;
  background: var(--desk);
  background-image: radial-gradient(ellipse at 50% 0%,
    color-mix(in srgb, var(--desk) 82%, var(--paper)) 0%,
    var(--desk) 55%, var(--desk-deep) 100%);
  color: var(--ink);
  font: 1rem/var(--rule) var(--font-body);
  -webkit-text-size-adjust: 100%;
  min-height: 100vh;
}

/* Skip link: the nav is the same six items on every page, so a screen-reader
   or keyboard user would otherwise tab through all of them to reach the one
   thing that changed. */
.skip {
  position: absolute; left: .5rem; top: -3rem; z-index: 50;
  background: var(--paper); color: var(--ink);
  padding: .5rem .9rem; border-radius: var(--radius-sm);
  border: 1px solid var(--ring); font-weight: 600; text-decoration: none;
  transition: top 120ms ease-out;
}
.skip:focus { top: .5rem; }

/* ---- the top nav ----
 *
 * Logo left, destinations centre, session right. It is the same bar on every
 * page including the editor, which is the whole point: there is no screen you
 * can land on with no way back out.
 */

header.site {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--desk) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid color-mix(in srgb, var(--desk-ink) 14%, transparent);
  color: var(--desk-ink);
}
header.site nav {
  max-width: 62rem; margin: 0 auto;
  padding: .5rem .9rem;
  display: flex; align-items: center; gap: .7rem;
}

.brand {
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: -.01em;
  line-height: 1.2;
  color: var(--desk-ink);
  text-decoration: none;
  white-space: nowrap;
  flex: 0 0 auto;
}
/* The one piece of brand emphasis, and it is a marker stroke -- the letters
   stay the same ink as the rest of the word. */
.brand .sh {
  position: relative;
  isolation: isolate;
  padding: 0 .06em;
}
.brand .sh::before {
  content: ''; position: absolute; z-index: -1;
  left: 0; right: 0; top: .12em; bottom: .1em;
  background: var(--mark);
  border-radius: 1px 3px 2px 3px;
  transform: rotate(-.7deg);
}

.nav-main {
  list-style: none; display: flex; gap: .1rem;
  margin: 0; padding: 0;
  flex: 1 1 auto; justify-content: center;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.nav-main::-webkit-scrollbar { display: none; }

/* The theme toggle is excluded by name from every rule in this group. It sits
   inside .nav-end on the pages that keep it there for tab order, but it is no
   longer a nav CELL -- it is the corner FAB, and `.nav-end button` at (0,1,1)
   outranks the `.theme-toggle` placement rule at (0,1,0), so without this it
   was dragged back into the bar as a flat rectangle with a marker band. Named
   here rather than answered with a heavier selector on the toggle: the honest
   statement is that this is not one of these, not that it shouts louder. */
.nav-main a, .nav-end a, .nav-end button:not(.theme-toggle) {
  display: block; white-space: nowrap;
  /* Nav controls size themselves. Without this the admin pages' Sign out
     button inherited the two-rule height meant for buttons on the paper, which
     made the admin nav 28px taller than the public one -- so the sheet started
     lower on half the site. */
  min-height: 0;
  font: inherit; font-size: .92rem; font-weight: 600; line-height: 1.3;
  color: var(--desk-ink);
  text-decoration: none;
  padding: .38rem .6rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  cursor: pointer;
  position: relative;
  isolation: isolate;
}

/* Hover and current page are the SAME marker band; hover draws it, current
   keeps it drawn. So the pointer literally previews "you are here" rather than
   inventing a second visual language for the same idea. */
.nav-main a::before, .nav-end a::before, .nav-end button:not(.theme-toggle)::before {
  content: ''; position: absolute; z-index: -1;
  left: .18rem; right: .18rem; top: .3rem; bottom: .28rem;
  background: var(--mark);
  border-radius: 2px;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--swipe);
}
.nav-main a:hover::before, .nav-end a:hover::before, .nav-end button:not(.theme-toggle):hover::before,
.nav-main a:focus-visible::before, .nav-main a[aria-current="page"]::before {
  transform: scaleX(1);
}
.nav-main a[aria-current="page"] { color: var(--ink); }
/* The marker band is the WHOLE hover, for Sign out as much as for a nav link.
   `button:hover:not([disabled])` up in the button section is (0,2,1) and out-
   specifies the `background: transparent` above it at (0,1,1), so Sign out was
   painting an accent panel UNDER the band -- two hover languages on one
   control. Same specificity trap that once turned the flags mustard. */
.nav-end button:not(.theme-toggle):hover:not([disabled]) { background: transparent; }

.nav-end { display: flex; align-items: center; gap: .2rem; flex: 0 0 auto; }

/* Theme toggle. A single button that reports its own state, rather than a
   three-way control -- "follow the system" is the default nobody has to pick,
   and the two overrides are the only thing worth a click. */
/* Floats in the bottom-right corner at every width, on every page.
   The earlier nav-cell placement has been retired.

   This is the placement rule for the WHOLE SITE, which is why it hangs off
   `.theme-toggle` itself rather than off an opt-in `.theme-fab` beside it.
   That opt-in was how it started, and the result was five of eleven pages
   still wearing the toggle in the nav because nobody remembered to add a
   second class to the button -- a page-by-page decision that no page had any
   business making. The markup now says only WHAT the control is; where it
   sits is settled here, once. The button can stay inside <nav> for tab order:
   position:fixed takes it out of flow, not out of the document.

   `#bmc-wbtn` is pinned top-right by the rule further down, so the coffee chip
   and this corner never meet. */
.theme-toggle {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 30;
  width: 2.6rem; height: 2.6rem;
  min-height: 0;
  display: grid; place-items: center;
  padding: 0 !important; margin: 0;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--ring);
  box-shadow: 0 4px 14px rgba(0,0,0,.18);
}
/* The hand-drawn set in /icons.svg, pulled in with
   `<svg class="icon"><use href="/icons.svg#i-name"/></svg>`.
   Stroke colour comes from currentColor, so an icon takes the colour of
   whatever it sits in and needs no per-place rule. Size is set here and
   overridden where a particular slot wants a different one. */
.icon { width: 1.15rem; height: 1.15rem; flex: 0 0 auto; vertical-align: -.18em; }
/* Icons are decoration beside a label, never the hit target itself -- a click
   must always land on the button, not on the drawing inside it. */
.icon { pointer-events: none; }

/*
 * Narrow screens trade the WORDS on a control for its icon.
 *
 * Wide: the label reads, and `.icon.only` is not drawn at all -- the desktop
 * bars are unchanged. Narrow: the icon appears and the label goes to
 * .visually-hidden rather than display:none, which matters -- a button whose
 * text is display:none has no accessible name left, so a screen reader would
 * announce eight identically nameless buttons. Hidden this way the name
 * survives for anyone not looking at the screen, and the tooltip survives for
 * anyone who is.
 */
.icon.only { display: none; }

/* Off-screen but still in the accessibility tree. The clip-path/1px recipe
   rather than `display:none` or `visibility:hidden`, both of which remove the
   text from that tree along with the pixels. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

@media (max-width: 44rem) {
  .icon.only { display: block; }
  .to-icon .label { position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0; clip-path: inset(50%);
    overflow: hidden; white-space: nowrap; }
  /* Square once the words are gone, so a row of them reads as a row of marks
     rather than as buttons that lost their text. Tight, because the public nav
     carries FIVE of these plus the brand and two controls, and at .6rem the
     side padding alone came to ~100px -- enough on its own to push the row
     past 390px and wrap it. The tap target is the button, which stays a full
     rule tall; this only trims the dead width either side of the drawing.

     The class is doubled to raise specificity. `.nav-main a` and
     `.toolbar button` both set padding at (0,1,1); a plain `.to-icon` is
     (0,1,0) and loses to them, so the rule silently did nothing.
     `.to-icon.to-icon` is (0,2,0) and wins, without having to name every
     container that might ever hold one. */
  .to-icon.to-icon { padding-left: .34rem; padding-right: .34rem; }
}

.theme-toggle svg { width: 1.2rem; height: 1.2rem; }
.theme-toggle::before { display: none; }
.theme-toggle:hover { background: var(--paper) !important; border-color: var(--desk-ink); }
.theme-toggle .sun { display: none; }
.theme-toggle .moon { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .sun { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .moon { display: none; }
}
:root[data-theme="dark"] .theme-toggle .sun { display: block; }
:root[data-theme="dark"] .theme-toggle .moon { display: none; }
:root[data-theme="light"] .theme-toggle .sun { display: none; }
:root[data-theme="light"] .theme-toggle .moon { display: block; }

/* ---- the page ---- */

main {
  position: relative;
  /* Narrowed from 48rem when the body went to 17px. The measure is what
     matters, not the sheet: at 48rem a 17px line ran to ~90 characters, well
     past the 65-75 that stays comfortable to read. This holds it near 78. */
  max-width: 42rem;
  margin: var(--rule) auto calc(var(--rule) * 4);
  /* Two ruled lines of margin at the top of the sheet before anything is
     written. Has to stay a whole number of rules -- that is what lets the
     background offset below be a plain --baseline instead of a running total. */
  padding: calc(var(--rule) * 2) 1.6rem calc(var(--rule) * 3) 3.2rem;
  background-color: var(--paper);
  /* Faint ruled lines, pitched to --rule so body copy rides them. */
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(var(--rule) - 1px),
    color-mix(in srgb, var(--ring) 55%, transparent) calc(var(--rule) - 1px),
    color-mix(in srgb, var(--ring) 55%, transparent) var(--rule)
  );
  /* One rule per tile, instead of one tile the size of the whole sheet.
   *
   * Visually identical -- the gradient repeats either way -- but it is the
   * difference between the paper having lines on it and not. With the default
   * `auto`, the tile IS the padding box, and a non-zero background-position
   * (below) makes the browser rasterise that whole tile into an intermediate
   * before repeating it. Past roughly four megapixels it downsamples that
   * intermediate, and the 1px line inside each 32px band is what gets thrown
   * away: a long page came out with rules at 64px, 128px, even 224px apart
   * while the text on it stayed at 32px, so lines of writing floated in the
   * middle of blank bands.
   *
   * It scales with tile AREA, so it arrived with a wide page rather than a
   * long one: /how/ widens main to 68rem for its two-column steps, and that
   * alone pushed a 4832px page over the edge. Measured: 29 rules at a uniform
   * 32px pitch after this, against 7 before, on the same page.
   */
  background-size: 100% var(--rule);
  /* The rules are drawn at the BOTTOM of each band, so pushing the whole
     pattern down by --baseline puts a rule under the first baseline -- and,
     because every block below consumes whole rules, under every one after it.
     The extra --sit drops it clear of the descenders. */
  background-position: 0 calc(var(--baseline) + var(--sit));
  border-radius: 2px 6px 6px 2px;
  box-shadow: var(--shadow-paper);
}

/* Binder holes down the left edge. */
main::before {
  content: "";
  position: absolute;
  left: .82rem; top: 0; bottom: 0; width: 14px;
  background-image: radial-gradient(circle at 7px 7px, var(--desk) 6.5px, transparent 7px);
  background-size: 14px 96px;
  background-repeat: repeat-y;
  background-position: 0 3.2rem;
  opacity: .5;
  pointer-events: none;
}

/* The margin rule. Amber now rather than red pen -- the accent is the marker,
   and a second competing ink colour was the thing that read as "pink". */
main::after {
  content: "";
  position: absolute;
  left: 2.5rem; top: 0; bottom: 0; width: 1px;
  background: color-mix(in srgb, var(--mark-deep) 45%, transparent);
  pointer-events: none;
}

/* ---- type ---- */

/* Headings occupy whole rules like everything else. The `top` nudges are the
   only fudge on the page: a display serif at 29.6px has a taller content box
   than a 30px line, so its natural baseline lands a few pixels low. `position:
   relative` moves the glyphs onto the rule WITHOUT moving the box, so the grid
   below is untouched -- a margin tweak would have shifted every following line
   off the paper to fix one heading. */
h1 {
  font-family: var(--font-display);
  font-size: 1.85rem;
  font-weight: 600;
  line-height: var(--rule);
  letter-spacing: -.015em;
  margin: calc(var(--rule) * 2) 0 0;
  position: relative;
  top: -4px;
}
/* A heading that opens a box does not also need the two rules of air that
   separate it from a previous section -- the box edge already does that.
   Replaces the `style="margin-top:.2rem"` that used to be sprinkled through the
   pages, each of which knocked the grid sideways by a fraction of a rule. */
/* ...and through the view wrappers. Every public view is a <section class=view>
   inside <main>, and the signing one nests another <div> for its two states.
   Without reaching through both, a view's <h1> keeps the two rules of air meant
   to separate it from a PREVIOUS section, and that page starts two ruled lines
   lower than every other one. */
:is(.card, .done-banner, .initial-box, main, main > .view, main > .view > div)
  > :first-child { margin-top: 0; }
/* Labels carry their leading gap as padding, so :first-child needs that killed
   too or the first question sits a rule lower than everything else. */
:is(.card, .done-banner, main, form) > label:first-child { padding-top: 0; }
h2 {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 600;
  line-height: var(--rule);
  margin: calc(var(--rule) * 2) 0 0;
  position: relative;
  top: -1px;
}
h3 { font-family: var(--font-display); font-size: 1.05rem; line-height: var(--rule); margin: var(--rule) 0 0; }
p { margin: 0 0 var(--rule); }
ul, ol { margin: 0 0 var(--rule); padding-left: 1.4rem; }
li { line-height: var(--rule); }
.muted { color: var(--ink-stone); }
/* Smaller type, same rule pitch -- a note written on the paper still gets one
   ruled line per line of writing. */
.small { font-size: .875rem; line-height: var(--rule); }
code { font: 13px var(--font-mono); background: var(--sand); padding: .1rem .3rem; border-radius: var(--radius-sm); }

/* Links: same ink as the sentence around them, marked underneath. The band is
   only 3px tall at rest so it reads as an underline, and fills the word on
   hover -- it is the same gesture, finished.
   text-decoration stays on as the non-colour, non-background cue, since a
   background band is exactly what forced-colours mode and print throw away. */
a {
  color: inherit;
  position: relative;
  isolation: isolate;
  text-decoration: underline;
  text-decoration-color: var(--mark-deep);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
/* The band sweeps ACROSS the word, the way a highlighter is actually drawn.
   It used to scale up from the baseline, which reads as a bar rising rather
   than as a pen stroke — and it disagreed with .mark, which has swept sideways
   from the start. Same gesture everywhere now. */
a::before {
  content: ''; position: absolute; z-index: -1;
  left: -.12em; right: -.12em; top: .08em; bottom: 0;
  background: var(--mark);
  /* A marker does not start and stop square. */
  border-radius: 2px 5px 3px 6px;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--swipe);
}
a:hover::before, a:focus-visible::before { transform: scaleX(1); }
a.button::before, .brand::before { display: none; }
a.button, .brand { text-decoration: none; }

/* A pencilled aside in the margin. Decorative only. */
/* A line in the margin, in a hand.
 *
 * The `top` is the same fudge the display headings carry, for the same reason
 * and measured the same way: Caveat's baseline sits 1px higher inside a 32px
 * line box than the body face does, so the handwriting floated a pixel above
 * the rule while every line of prose around it rested on one. `position:
 * relative` moves the glyphs and not the box, so nothing below it shifts --
 * a margin would have bought the same pixel and cost the whole grid.
 *
 * One pixel is worth it here precisely because this line is usually stuck to a
 * piece of tape at a slight angle, and a rotated line that is ALSO sitting off
 * the rule reads as sloppy rather than as hand-placed. */
.note-hand {
  font-family: var(--font-hand);
  font-size: 1.15rem;
  line-height: var(--rule);
  color: var(--ink-charcoal);
  position: relative;
  top: 1px;
}

/* ---- the marker and the tape ----
 *
 * .mark  — a highlighter band swiped behind the words.
 * .tape  — a torn strip of masking tape the words are written on.
 *
 * Both are pseudo-element backgrounds driven by `transform`, never by
 * background-size: a transform is the one property that animates on the
 * compositor, so the swipe stays smooth on the cheap Chromebook a student is
 * actually holding. Both leave the text at --ink.
 */
/* Full ink, always -- including inside a .muted paragraph, which is where the
   marker is most often used. Inheriting the muted grey put --ink-stone on the
   band at 4.2:1 in light and 2.5:1 in dark, i.e. the act of emphasising a
   phrase made it the least readable text on the page. */
.mark, .tape { color: var(--ink); }

.mark {
  position: relative;
  isolation: isolate;
  padding: 0 .18em;
}
.mark::before {
  content: ''; position: absolute; z-index: -1;
  left: 0; right: 0; top: .08em; bottom: .04em;
  background: var(--mark);
  /* Uneven corners: a marker does not start and stop square. */
  border-radius: 2px 5px 3px 6px;
  transform: scaleX(0) rotate(-.5deg);
  transform-origin: left center;
  transition: transform var(--swipe);
}
.mark.swiped::before { transform: scaleX(1) rotate(-.5deg); }

/* No padding: the strip is grown by the pseudo-element's negative inset
   instead, so the tape can be bigger than the words without the inline-block
   growing its line box and pushing the next line off its rule. */
.tape {
  position: relative;
  isolation: isolate;
  display: inline-block;
  line-height: var(--rule);
  transform: rotate(-.9deg);
}
.tape::before {
  content: ''; position: absolute; z-index: -1;
  inset: -.05em -.75em;
  background: var(--tape);
  box-shadow: 0 1px 3px rgba(0,0,0,.13);
  /* The torn ends. A polygon costs nothing and is the whole reason it reads as
     tape instead of a rectangle. */
  clip-path: polygon(
    1.5% 6%, 98% 0%, 100% 94%, 2% 100%
  );
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--swipe);
}
.tape.swiped::before { transform: scaleX(1); }

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

/* A box drawn ON the paper, not a panel laid over it: the background is gone so
   the ruled lines run straight through, which is the only way the text inside a
   card can be seen to sit on them. Vertical padding is a rule minus the border,
   so the card as a whole still consumes a whole number of rules. */
.card {
  background: transparent;
  border: 1px solid var(--border-warm);
  border-radius: var(--radius-md);
  padding: calc(var(--rule) - 1px) 1.2rem;
  margin: var(--rule) 0;
}

/* No box at all -- just a section written further down the same page. For a
   choice between two things there is nothing to enclose, and the border was
   drawing a second rectangle inside the sheet. Padding goes to a whole rule
   now that there is no border to subtract, and the horizontal padding goes
   entirely, so the text lines up with the margin rule like everything else. */
.card.plain {
  border: 0;
  border-radius: 0;
  /* One blank ruled line above each section and none below: with a border there
     was a box to pad away from, and without one the old symmetric padding plus
     the margin stacked into three blank rules, which read as a gap rather than
     a break. Between two sections this lands as two lines -- the margin and
     the next section's own line. */
  padding: var(--rule) 0 0;
}

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

/* All of a label's vertical space is PADDING. The gap above it and the offset
   that drops its input's blank onto a ruled line both have to survive being
   next to another margin, and margins collapse: with margins here the input's
   6px bottom gap was eaten by the following label every time, so each field
   drifted a further 6px down the form. */
label {
  display: block; font-weight: 600; font-size: 1.05rem; line-height: var(--rule);
  margin: 0;
  padding: var(--rule) 0 calc(var(--baseline) + var(--sit));
}
.choice { padding: 0; }
.hint { display: block; font-weight: 400; color: var(--ink-stone); font-size: .95rem; line-height: var(--rule); }

/* Two rules tall. That is both the grid unit and a 60px touch target, which is
   comfortably over the 44px floor -- the two constraints happened to agree, and
   where they had not, the target would have won. */
/* Fill in the blank.
 *
 * No box: a rule of the paper, thickened, with the answer written on it. It is
 * the same object the rest of the page is made of, which is the whole reason
 * the form stops looking like a web form pasted onto a notebook.
 *
 * `display: block` because as an inline-block the field sits in a line box and
 * the strut adds leading under it.
 *
 * One rule tall, so the blank is one line of the page. That is a 32px target
 * rather than the 44px minimum -- what keeps it usable is that the label above
 * is part of the target (a <label for> focuses its field), which makes the real
 * hit area the label, its hint and the blank together. The label is also always
 * visible: a bare underline is a weaker affordance than a box, so
 * placeholder-as-label would have been unusable here.
 */
input[type=text], input[type=email], input[type=password], input[type=file] {
  display: block;
  /* One width for every blank, so they line up down the left AND right edge
     the way ruled answer lines do. */
  width: var(--blank, 62%);
  min-width: 12ch;
  height: var(--rule);
  padding: 0 .35rem;
  /* Only a bottom margin, and only 6px of it. The offset ABOVE lives on the
     label as padding -- see the note there. */
  margin: 0 0 calc(var(--rule) - var(--baseline) - var(--sit));
  font: inherit;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 3px solid var(--ink-stone);
  border-radius: 0;
}
input[type=text]:hover, input[type=email]:hover, input[type=password]:hover {
  border-bottom-color: var(--ink);
}
input:focus-visible { border-bottom-color: var(--mark-deep); }

/* The access code is read off an email and typed in once. Spaced out like the
   boxes on a form so a 1 and an I are told apart while typing. Same width as
   every other blank -- only the letterforms differ. */
.blank-code {
  text-transform: uppercase;
  letter-spacing: .18em;
  font-weight: 700;
}

/* The code, shown once, on the line. Spaced the same as the field it will be
   typed into so a 1 and an I are told apart while copying it down, and one
   whole rule tall so the page below it stays on the grid. */
.code-out {
  margin: 0 0 var(--rule);
  font-size: 1.35rem;
  line-height: var(--rule);
  letter-spacing: .22em;
  font-weight: 700;
  text-transform: uppercase;
}

input[type=file] {
  width: 100%; padding: calc(var(--rule) / 2 - .4rem) 0;
  margin: 0; border-bottom-width: 0;
}
/* display:block, not the default inline-block. As an inline-block a textarea
   sits in a line box and the strut adds its descender underneath -- 9px of
   invisible space that put everything below it off the grid, and which no
   amount of padding arithmetic would have explained. */
textarea {
  display: block;
  width: 100%;
  /* Same 6px tail as a blank, so a label + textarea pair is a whole number of
     rules the way a label + blank pair is. */
  margin: 0 0 calc(var(--rule) - var(--baseline) - var(--sit));
  padding: calc(var(--rule) / 2 - 1px) .75rem;
  font: inherit;
  line-height: var(--rule);
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--ring);
  border-radius: var(--radius-sm);
}
input:focus-visible, button:focus-visible, textarea:focus-visible, a:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The primary button is the marker again: ink on a highlighter band. It gets a
   real border so it is still a button under forced-colours, where the
   background is discarded. */
/* Two rules tall, so a button occupies exactly two lines of the paper and the
   text after it resumes on the grid. Its LABEL is centred rather than sat on a
   rule: a rule can only pass through the label or the button's edges, not both,
   and a control reads as an object placed on the page rather than as
   handwriting on it. */
button, .button {
  font: inherit;
  font-weight: 600;
  line-height: var(--rule);
  min-height: calc(var(--rule) * 2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 1.1rem;
  border-radius: var(--radius-sm) var(--radius-md) var(--radius-sm) var(--radius-md);
  border: 1px solid var(--mark-deep);
  background: var(--mark);
  color: var(--ink);
  cursor: pointer;
  transition: transform 110ms ease-out, background 140ms ease-out;
}
/* A link that navigates, styled as a button -- kept an <a> so it lands in the
   tab order and opens in a new tab the way a link is expected to. */
.button { text-decoration: none; }

/* A button that is the only thing in its paragraph sits BETWEEN two rules:
   top edge on one, bottom edge two rules down, so it fills those two lines
   exactly instead of straddling three.
 *
 * The offsets are padding on the wrapper, not margins on the button. Margins
 * would collapse with the previous paragraph's, which silently eats the
 * top offset and puts the button back off the grid -- the arithmetic looks
 * right in the stylesheet and is wrong on screen.
 *
 * Without :has() the button is still two rules tall, just not rule-aligned. */
p:has(> button:only-child), p:has(> .button:only-child) {
  padding: calc(var(--baseline) + var(--sit)) 0
           calc(var(--rule) - var(--baseline) - var(--sit));
  margin: 0 0 var(--rule);
}
/* Block-level so it does not sit in a line box (whose strut would add its own
   leading under it), but shrink-to-fit so it stays button-shaped. */
p > button:only-child, p > .button:only-child { display: flex; width: fit-content; }

/* A form's submit sits a blank line below the last answer -- it is a different
   kind of thing from the questions above it, and on a worksheet you do not
   sign immediately under the last blank. Scoped to buttons directly in a form,
   so the two nearly identical sign-in forms cannot drift apart, and so the
   landing page's choices are left alone. */
form > p:has(> button:only-child) {
  padding-top: calc(var(--rule) + var(--baseline) + var(--sit));
}
button:hover:not([disabled]), .button:hover { background: var(--accent-soft); color: var(--ink); }
/* Feedback on the press, not the release. */
button:active:not([disabled]), .button:active { transform: scale(.975); }
button[disabled] { opacity: .5; cursor: not-allowed; }
/* .secondary / .danger apply to both a <button> and an <a class="button">, so
   a link that navigates never has to be faked with a nested control. */
.secondary { background: transparent; color: var(--ink); border-color: var(--ring); }
.secondary:hover:not([disabled]) { background: var(--sand); color: var(--ink); }

/* ---- a button is a strip of masking tape ----
 *
 * A torn strip, not a filled rectangle. Same trick as .tape above -- the strip
 * is a pseudo-element BEHIND the label, so the button's own box keeps its
 * square edges for the focus ring, the hit area and the whole rules it owes
 * the grid, while the thing you see is ragged.
 *
 * --tape is already the palette's dull beige (it is what the .tape accent is
 * torn from), so this is the material the design already had, cut to a whole
 * strip instead of a scrap.
 *
 * No image. A downloaded tape PNG would be one more file to ship, a licence to
 * record in ATTRIBUTION.md, a fixed colour that cannot follow the dark theme,
 * and a raster that goes soft on a wide button. clip-path draws the same rip
 * at any size in either theme for nothing.
 *
 * EVERY button in the reading flow, not just the quiet ones. A page had a
 * yellow one and a beige one side by side and the highlighter was doing two
 * jobs -- "this is a button" and "you are about to press this". It now only
 * does the second. What makes an action primary is that it is the one at the
 * bottom of the form, which is how it reads on paper.
 *
 * .danger is out: red means something the tape cannot say.
 *
 * Compact chrome -- filter chips, tab strips, the editor's block tools -- opts
 * out in its own stylesheet with `::before, ::after { content: none }`. Those
 * are one rule tall and are the page's furniture, not actions in the flow.
 *
 * Three variants, one definition, two knobs:
 *
 *   --tape-rest    1  the strip is stuck down already   (on the paper; Publish)
 *                  0  plain text until the pointer lands (the toolbar's rest)
 *   --tape-inset      how far in the marker band starts -- the button's own
 *                     horizontal padding, so the band covers the WORDS and not
 *                     the whole piece of tape.
 */
main :is(button, .button):not(.danger), .toolbar button {
  --tape-rest: 1;
  --tape-inset: 1.1rem;
  position: relative; isolation: isolate;
  background: transparent; border-color: transparent;
}
/* The generic `button:hover` fill is (0,2,1) and would paint --accent-soft
   straight over the strip. These beat it and leave the tape to do the talking. */
main :is(button, .button):not(.danger):hover:not([disabled]), .toolbar button:hover:not([disabled]) { background: transparent; }

/* The strip itself.
 *
 * Real masking tape is slightly translucent and has a fine grain, so: 94%
 * opacity -- the ruled line ghosts faintly THROUGH the strip rather than being
 * cut by it, which is what tape over a line actually looks like -- and a 3px
 * fibre gradient at about 2% contrast. */
main :is(button, .button):not(.danger)::before, .toolbar button::before {
  /* `display` is set here on purpose. A link in prose wears the marker band as
     its own ::before, and `a.button::before { display: none }` above turns that
     off for a link styled as a button -- which silently swallowed the tape too,
     because the cascade is settled per PROPERTY and nothing else here declares
     one. The landing page's two calls to action were bare words for it. */
  display: block;
  content: ''; position: absolute; z-index: -1; inset: 0;
  background:
    repeating-linear-gradient(90deg,
      rgb(0 0 0 / .022) 0 1px, transparent 1px 3px),
    color-mix(in srgb, var(--tape) 94%, transparent);
  /* Torn short ends. Traced as one closed path: down the left edge, along the
     bottom, up the right edge, back across the top. The long edges stay
     straight -- those are the width of the roll, not a tear.
   *
   * The steps are deliberately uneven in BOTH axes and the two ends do not
   * match. Evenly spaced teeth of equal depth read as a perforation -- a
   * stamp, not something torn by hand.
   *
   * Percentages, so the rip scales with the button. That holds while these
   * buttons are all 90-150px wide; a much wider one would get teeth to match,
   * and would want a fixed-width mask instead. */
  clip-path: polygon(
    3% 0%, 0% 9%, 4% 19%, 1% 28%, 2% 41%, 5% 52%, 1% 61%, 3% 73%, 0% 84%, 2% 93%, 1% 100%,
    99% 100%, 96% 91%, 100% 81%, 97% 70%, 99% 58%, 95% 47%, 100% 36%, 97% 26%, 99% 14%, 96% 5%, 98% 0%
  );
  /* drop-shadow, not box-shadow: clip-path would cut a box-shadow off with the
     box, and this one has to follow the ragged silhouette to read as a strip
     lying ON the page. */
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / .18));
  transform: rotate(-.7deg);
  opacity: var(--tape-rest);
  transition: opacity 140ms ease-out;
}
main :is(button, .button):not(.danger):hover:not([disabled])::before,
main :is(button, .button):not(.danger):focus-visible::before,
.toolbar button:hover:not([disabled])::before,
.toolbar button:focus-visible::before { opacity: 1; }

/* The marker band, swiped across the label ON the tape -- the same band the nav
 * links and .mark use, so the whole site has one way of saying "this one".
 * Recolouring the strip said it in a second language, and a strip that changes
 * shade under the pointer reads as the tape itself reacting, which is not a
 * thing tape does.
 *
 * A separate ::after rather than more work on the strip: both sit at z-index -1
 * so they paint under the label, and ::after comes second, which puts the band
 * on TOP of the tape and still behind the words. No z-index arithmetic.
 *
 * The transition is `transform` only, on the compositor; the global
 * prefers-reduced-motion rule further down turns the swipe into a straight
 * appearance. */
main :is(button, .button):not(.danger)::after, .toolbar button::after {
  content: ''; position: absolute; z-index: -1;
  left: var(--tape-inset); right: var(--tape-inset);
  top: calc(50% - var(--rule) / 2 + 3px);
  height: calc(var(--rule) - 8px);
  background: var(--mark);
  /* Uneven corners: a marker does not start and stop square. */
  border-radius: 2px 5px 3px 6px;
  transform: scaleX(0) rotate(-.5deg);
  transform-origin: left center;
  transition: transform var(--swipe);
}
main :is(button, .button):not(.danger):hover:not([disabled])::after, main :is(button, .button):not(.danger):focus-visible::after,
.toolbar button:hover:not([disabled])::after, .toolbar button:focus-visible::after {
  transform: scaleX(1) rotate(-.5deg);
}
/* What does NOT get taped.
 *
 * Chrome: rails, tab strips, filter chips, the editor's block tools. All one
 * rule tall, all furniture for the page rather than actions in the reading
 * flow -- a torn strip behind every ⤒ and ✕ would bury the document under
 * stationery, and the flag rail has a shape and a colour of its own.
 *
 * The list lives here, next to the rule it cancels, and not in each page's own
 * stylesheet -- which is where it started, and where it silently did nothing:
 * `.filters button::before` is (0,1,3) and the tape rule is (0,2,2), because
 * `:is(button, .button)` is scored as the CLASS in it. Matching the tape's own
 * shape is what makes this land; being one line below it is what makes it win
 * the tie. */
main :is(.flags, .filters, .tabs, .pages, .adders, .tools, .page-tools, .sec-tools, .compact)
  :is(button, .button)::before,
main :is(.flags, .filters, .tabs, .pages, .adders, .tools, .page-tools, .sec-tools, .compact)
  :is(button, .button)::after,
/* ...and .compact on the control ITSELF, not just on a container. The list
   above is all wrappers, so a lone compact button -- a Reissue chip in a table
   row -- matched none of it and wore a torn strip. */
main :is(button, .button).compact::before,
main :is(button, .button).compact::after,
/* ...and a school result, for the same reason and by the same trick. A row in
   the school picker is a button, so it wore a torn strip the width of the whole
   dropdown -- name, town and badge under one piece of stationery, when the only
   part that identifies a school is its name. The tape now sits behind the name
   alone (.sr-name), and the row's own has to say no explicitly. Scored (0,2,2)
   to match the rule it cancels; one line below it is what wins the tie. */
main :is(button, .button).school-result::before,
main :is(button, .button).school-result::after { content: none; }

/* BMC's floating widget, pinned top-right and opened downward.

   Two pieces:
   - `#bmc-wbtn` is BMC's floating chip. BMC positions it with `bottom`.
   - `#bmc-iframe` is the panel that opens when the chip is clicked. BMC
     positions it with `bottom + 72px` and animates with `transform: scale(0)`
     from `transform-origin: right bottom`, so it currently grows upward from
     the bottom-right corner. We move it to the top and flip the origin to
     `right top`, so the panel hangs DOWN from the chip -- the curtain falls
     instead of rising.

   BMC's source uses `!important` on `bottom`, so we have to out-`!important`
   it the same way. If BMC ever moves to a more aggressively-scoped style,
   this can come back with stronger selectors. */
#bmc-wbtn,
#bmc-wbtn[data-position="Right"] {
  bottom: auto !important;
  /* Push the chip clear of the nav: nav-h is the measured nav height, plus a
     breath of breathing room so the chip doesn't kiss the divider. */
  top: calc(var(--nav-h) + 24px) !important;
  /* Override BMC's data-color brush with the page's own marker-yellow, so the
     chip agrees with the inline `.mark` highlighter used everywhere else. */
  background: var(--mark) !important;
  border: 1px solid var(--mark-deep) !important;
}

/* The chevron BMC swaps in once the panel is open. It ships as an inline
   <svg><path fill="white">, and white on our marker-yellow chip (#FFE08A) is
   about 1.4:1 -- the arrow all but disappears. A presentation attribute like
   `fill="white"` sits BELOW any stylesheet rule in the cascade, so this needs
   no specificity games; the !important is only there in case BMC later moves
   the colour into its own stylesheet. */
#bmc-wbtn svg path { fill: var(--ink) !important; }
/* The hand-drawn mug swapped in for BMC's stock emoji. Twice the usual icon
   size so it carries the button alone, and inked to --ink so it reads black
   on the light ground and near-white on the dark one. */
#bmc-wbtn svg.bmc-mug {
  width: 2.3rem; height: 2.3rem; vertical-align: middle;
  color: var(--ink);
}
#bmc-wbtn svg.bmc-mug path { stroke: currentColor !important; fill: none !important; }

/* The inline button on the landing page. Same 2x mug, same theme-flip ink. */
.bmc-button .icon { width: 2.3rem; height: 2.3rem; color: var(--ink); }
/* BMC + About sit side by side at the foot of the landing view. */
.bmc-row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.bmc-row .button .icon { width: 2.3rem; height: 2.3rem; color: var(--ink); }

/* BMC injects a thank-you message div with no class/id, position:fixed, as a
   direct child of body. The overlay wrapper `l` (also no id/class, fixed) is
   the miss: it has `top: 0; left: 0; z-index: 9999999` while the message div
   has the chip's offset. Exclude `l` by its `top: 0; left: 0` signature. */
body > div[style*="position: fixed"]:not([id]):not([class]):not([style*="top: 0"]):not([style*="top:0"]) {
  display: none !important;
}

/* BMC injects `#bmc-close-btn` (the X button on the iframe panel). No
   dismissal: the in-page invite was the hinge, and once the user clicks the
   chip the iframe should stay until it's done -- the typical close path is
   a confirmation, and the X got in the way. */
#bmc-close-btn {
  display: none !important;
}
/* BMC's iframe sits at `top: calc(var(--nav-h) + 100px)`, anchored to the chip
   rather than the bottom. BMC positions it with `bottom` by default; we flip
   it to `top` so the panel hangs down from the chip. The MutationObserver in
   index.html caps the inline height, since BMC re-assigns it on every click. */
iframe#bmc-iframe,
iframe[src*="buymeacoffee.com"] {
  bottom: auto !important;
  top: calc(var(--nav-h) + 100px) !important;
  transform-origin: right top !important;

  /* The height cap has to live HERE, not in the MutationObserver below in
     index.html. That observer sets an inline `style.height`, and an inline
     style loses to an `!important` declaration in a stylesheet -- which is
     exactly what BMC ships. Measured: the observer wrote 380px while the panel
     rendered 680px, so the cap had never once applied to the drawn box.

     Capped against the room actually left BELOW the chip rather than against a
     fraction of the viewport: the panel is anchored at `--nav-h + 100px`, so a
     percentage of the window says nothing about how much space remains under
     it. At 1280x800 that was 33px off the bottom; on a 390x740 phone, 151px.

     dvh, not vh: on a phone vh is the tallest the viewport ever gets, so a
     panel sized by it hides behind the browser's own chrome exactly when the
     address bar is showing. The vh line is the fallback for anything without
     dvh, and is deliberately first.

     min-height has to be released first, and that is the whole reason a cap
     looked applied but did nothing. BMC sets `min-height: 680px` (and `100%`
     on a phone), and in CSS min-height ALWAYS wins a conflict with max-height
     -- so the computed style read `max-height: 629px` and `height: 680px` at
     the same time, which looks impossible until you see the min. */
  min-height: 0 !important;
  max-height: calc(100vh - var(--nav-h) - 118px) !important;
  max-height: calc(100dvh - var(--nav-h) - 118px) !important;
}

/* BMC's overlay wrapper `l` is fullscreen and ignores the iframe's box. The
   iframe is `position: fixed` (its own positioning), so the wrapper cap
   doesn't crop it. Skip the wrapper; rely on the iframe's own height (capped
   by the MutationObserver in index.html) and clip overflow visually. */

/* In-page invitation that sits next to the BMC chip. The chip is a tiny
   pill; this carries the words the BMC iframe would otherwise show, so the
   teacher gets a real prompt without the page having to host its own payment
   surface. Anchored to the top-right next to the chip; tucked under the
   same `--nav-h` so they read as one corner. */
.bmc-invite {
  position: fixed;
  top: calc(var(--nav-h) + 24px);
  right: calc(18px + 64px + 12px); /* matches data-x_margin + chip width + gap */
  z-index: 30;
  max-width: 260px;
  padding: 14px 16px;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--border-warm);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  /* Out of the way on a phone: the chip is the only surface there. */
  display: none;
}
@media (min-width: 720px) {
  .bmc-invite { display: block; }
}
.bmc-invite-msg {
  margin: 0;
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink-olive);
  cursor: pointer;
}
.bmc-invite[hidden] { display: none; }

/* BMC's iframe is cross-origin so its internal CSS is unreachable. The widget
   shell around it injects a handful of `<div>`s/`<i>`/`<svg>` children -- the
   close icon and any caret/chevron it ships with -- into BMC's own wrapper
   divs. Force those surfaces to a dark ink in light mode so they read on the
   pale iframe chrome; in dark mode the wrapper usually paints lighter on its
   own, so leave it. light-dark() picks the right ink automatically. */
body > div[style*="z-index: 9999999"] i,
body > div[style*="z-index: 9999999"] svg path,
body > div[style*="z-index: 9999999"] .bmc-close-icon {
  color: var(--ink) !important;
  fill: var(--ink) !important;
  stroke: var(--ink) !important;
}

/* Under forced colours both pseudo-elements are discarded and only the box is
   left, so the box has to still be a button. The border is transparent rather
   than absent for exactly this: forced-colors repaints it, and keeping it
   costs no height. */
@media (forced-colors: active) {
  main :is(button, .button):not(.danger), .toolbar button { border-color: currentColor; }
}
.danger { background: transparent; color: var(--danger); border-color: var(--danger); }
.danger:hover:not([disabled]) { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }

/* Same offsets as a standalone button, so a row of controls also sits BETWEEN
   two rules rather than straddling them. Padding again, not margin -- see the
   note on the standalone case. */
.on-rules, .row {
  padding-top: calc(var(--baseline) + var(--sit));
  padding-bottom: calc(var(--rule) - var(--baseline) - var(--sit));
}
.row { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }

/* Tables are still the paper. One rule per row of data, so a roster reads like
   a ledger ruled by the same lines as everything else. Zero vertical padding --
   the line-height IS the row height. */
table { border-collapse: collapse; }
/* A full rule per row. The 1px that a bottom border steals is compensated by
   whoever draws that border -- see the admin pages. Subtracting it here instead
   cost every table a pixel per row, including the one inside an imported
   syllabus, and the drift accumulated down the rest of the document. */
th, td { line-height: var(--rule); padding-top: 0; padding-bottom: 0; }

/* Compact controls (tab strips, filter chips, block adders) are exactly one
   rule tall. They opt out of the two-rule sizing on the main button because
   they are chrome for the page, not actions in the reading flow -- but they
   still have to land on the grid or the content after them drifts. */
.compact { min-height: 0; height: var(--rule); line-height: calc(var(--rule) - 2px); padding: 0 .7rem; }

/* No box, for the same reason the cards lost theirs: a question with two
   answers on a worksheet is a heading and two ticks, not a panel. The legend
   is styled as the question it is, and the choices sit under it on their own
   ruled lines. */
fieldset {
  border: 0; border-radius: 0;
  padding: var(--rule) 0 0; margin: 0;
  min-inline-size: 0;   /* fieldsets default to min-content; this lets it shrink */
}
legend { font-weight: 600; font-size: 1.05rem; line-height: var(--rule); padding: 0; }

/* The answers run left to right on one ruled line, the way two boxes to tick
   sit next to each other on a form. Wraps to a second line on a phone, and
   the line height keeps it on the grid either way. */
.choices { display: flex; flex-wrap: wrap; gap: 0 1.8rem; align-items: baseline; }
/* Block, not flex, INSIDE each choice. A flex row centres its line box instead
   of aligning a baseline, which lifted "the parent or guardian" 7px off its
   rule -- the box is positioned against the text with vertical-align instead,
   which leaves the text where the grid put it. */
.choice {
  display: block;
  margin: 0; font-weight: 400; font-size: 1.05rem;
  height: var(--rule); line-height: var(--rule);
  cursor: pointer;
}

/* A square box you tick.
 *
 * These stay <input type=radio>: only one answer is valid, and the browser has
 * to enforce that. Only the drawing is a checkbox -- the square reads as
 * "tick this" far more plainly than a dot in a ring at this size.
 */
.choice input {
  appearance: none; -webkit-appearance: none;
  position: relative;
  display: inline-block;
  vertical-align: -.3em;
  width: 1.45rem; height: 1.45rem;
  min-width: 0;
  margin: 0 .5rem 0 0;
  border: 2px solid var(--ink-stone);
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
  transition: border-color 140ms ease-out, background 140ms ease-out;
}
.choice input:hover { border-color: var(--ink); }
/* No fill when ticked -- the tick is the answer. A highlighted box would be a
   second signal for the same thing, and on paper you do not shade the box in. */
.choice input:checked { border-color: var(--ink); }

/* The tick is drawn, not popped: the elbow is pinned by right/bottom, the short
   arm grows first and the long arm follows, so it reads as a pen stroke rather
   than a glyph appearing. Two borders of a zero-size box, rotated -- no SVG to
   inject into an input that cannot hold one. */
/* display:none until checked. A zero-by-zero box still paints its two 3px
   borders, which left a little dot sitting in every empty box. */
.choice input::after {
  content: '';
  display: none;
  position: absolute; right: .44rem; bottom: .32rem;
  width: 0; height: 0;
  border: solid var(--ink); border-width: 0 3px 3px 0;
  transform: rotate(45deg);
  transform-origin: right bottom;
}
.choice input:checked::after {
  display: block;
  animation: tick 260ms cubic-bezier(.25, .8, .35, 1) forwards;
}

@keyframes tick {
  0%   { width: 0;      height: 0; }
  45%  { width: .34rem; height: 0; }
  100% { width: .34rem; height: .68rem; }
}
@media (prefers-reduced-motion: reduce) {
  /* Still ticked, just already drawn. */
  .choice input:checked::after { animation: none; width: .34rem; height: .68rem; }
}
@media (forced-colors: active) {
  .choice input:checked { background: Highlight; }
  .choice input::after { border-color: HighlightText; }
}

/* Notices carry a left bar AND a word, never colour alone. */
.notice {
  border-radius: var(--radius-sm);
  padding: calc(var(--rule) / 2) .9rem;
  margin: var(--rule) 0;
  font-size: .92rem;
  line-height: var(--rule);
  /* Two rules tall and filled, so it has to sit BETWEEN two lines rather than
     have one drawn 6px above its bottom edge. See --lift. */
  position: relative;
  top: calc(-1 * var(--lift));
}
.notice.error {
  background: color-mix(in srgb, var(--danger) 12%, var(--paper));
  color: var(--ink); border-left: 3px solid var(--danger);
}
.notice.ok {
  background: color-mix(in srgb, var(--tab-1) 30%, var(--paper));
  color: var(--ink); border-left: 3px solid var(--tab-1);
}
/* A caution the reader has to act on, not a failure that already happened.
   Amber rather than --danger: "use a personal address" is an instruction, and
   red would say something has gone wrong when nothing has yet. */
.notice.warn {
  background: color-mix(in srgb, var(--accent-soft) 22%, var(--paper));
  color: var(--ink); border-left: 3px solid var(--accent);
}

/* "Not listed? search the area" school results. A row of tappable matches
   under the school field, each naming the school and where it is. The
   "Already on this install" tag is what makes joining rather than duplicating
   visible to the teacher picking the result. */
.school-search,
#schoolSearch { margin-top: .5rem; }
#schoolSearch [data-role="list"] {
  display: flex; flex-direction: column; gap: .35rem; margin: .4rem 0;
  max-height: 12rem; overflow-y: auto;
}
/* The row carries no box of its own. It used to be a filled, bordered block,
   which put the emphasis on the whole line -- name, town and the "on this
   install" badge alike -- when the only part that identifies a school is its
   NAME. The tape goes behind that and nothing else; the rest of the row is
   supporting text and reads as such. */
/* Block, not flex. As a flex container the row blockified its children, so the
   name stretched the full width and took the tape with it -- a strip the width
   of the dropdown rather than the width of the school. Inline children flow on
   one line and wrap on their own, and the tape hugs the words it is behind. */
.school-result {
  display: block; text-align: left; width: 100%;
  background: none; border: 0; border-radius: var(--radius-sm);
  padding: .3rem .2rem; font: inherit; color: var(--ink); cursor: pointer;
}
.school-result > * + * { margin-left: .6rem; }

/* A torn strip behind the name, the same one .tape draws -- same colour, same
   polygon, same slight rotation. Not the .tape class itself: that starts at
   scaleX(0) and waits for .swiped to animate in, which suits a heading that
   arrives once and is wrong for rows rebuilt on every keystroke. Here it is
   simply there. */
.sr-name {
  font-weight: 600;
  position: relative;
  isolation: isolate;
  display: inline-block;
  transform: rotate(-.6deg);
}
.sr-name::before {
  content: ''; position: absolute; z-index: -1;
  inset: -.06em -.5em;
  background: var(--tape);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .13);
  clip-path: polygon(1.5% 6%, 98% 0%, 100% 94%, 2% 100%);
}
/* Hover and keyboard focus lift the strip rather than repainting the row, so
   the thing that responds is the thing that would be chosen. */
.school-result:hover .sr-name::before,
.school-result:focus-visible .sr-name::before {
  box-shadow: 0 2px 6px rgba(0, 0, 0, .2);
}
.school-result:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The student and parent pickers' own list. The teacher page wraps its results
   in a panel of its own (#schoolSearch); this is the same rows without that
   chrome, sitting directly under the field on the ruled paper. */
.school-list { display: flex; flex-direction: column; gap: .15rem; margin: .1rem 0 .35rem; }
.sr-place { color: var(--ink-stone); font-size: .85rem; }
.sr-already {
  font-size: .78rem; color: var(--tab-1);
  border: 1px solid var(--tab-1); border-radius: var(--radius-sm);
  padding: 0 .35rem; margin-left: auto;
}

/* Getting set up, split by who is doing it.
 *
 * Links, not buttons with a click handler. Each tab IS a URL -- /register/ and
 * /register/code/ -- so it deep-links (a teacher can send a parent straight to
 * theirs), survives a reload, and works with the back button. The router turns
 * the click into a same-document swap, so it is as instant as a scripted tab
 * would be, and without JS it is still a working navigation.
 *
 * Marked with aria-current="page", NOT role="tab"/aria-selected. The ARIA tab
 * pattern promises arrow-key traversal of panels within one page; this is
 * navigation between two, and claiming the wrong pattern is worse for a screen
 * reader than claiming none. The selected tab is hardcoded per view rather than
 * computed -- each view has exactly one right answer, so there is nothing to
 * work out at runtime.
 *
 * No ::before/::after opt-out needed: the tape rule is
 * `main :is(button, .button)`, and these are bare <a>. */
.audience-tabs {
  display: flex;
  gap: .3rem;
  margin: var(--rule) 0 0;
  border-bottom: 1px solid var(--border-warm);
  /* The strip has a fill and a drawn edge, so it is aligned to the box grid
     while the rules are drawn --lift below it: without this the underline
     landed just under a ruled line and read as a doubled line. Moves the paint,
     not the box, so nothing below drifts off the grid. Same treatment as
     .notice, and the reason --lift exists. */
  position: relative;
  top: calc(-1 * var(--lift));
}
.audience-tabs a {
  height: var(--rule);
  line-height: calc(var(--rule) - 2px);
  padding: 0 .9rem;
  /* Overlaps the strip's own bottom rule, so the selected tab can erase the
     segment under itself and join the panel below. */
  margin-bottom: -1px;
  color: var(--ink-stone);
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: 8px 8px 0 0;
  font-size: .95rem;
  white-space: nowrap;
}
.audience-tabs a[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
  background: var(--ivory);
  border-color: var(--border-warm);
  border-bottom-color: var(--ivory);
}
.audience-tabs a:hover:not([aria-current="page"]) { color: var(--ink); background: var(--sand); }
.audience-tabs a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* Two tabs, and the longer label is three words. On a 320px phone they still
   fit side by side at this size; wrapping them into a column would read as two
   links rather than a choice between two. */
@media (max-width: 26rem) {
  .audience-tabs a { padding: 0 .6rem; font-size: .9rem; }
}

/* A field most students should skip. Marked "(optional)" in the label; most
   families already have a contact address on file from the roster, so this is
   the exception rather than a fifth question. */

/* ---- progress spine ---- */

.progress {
  position: sticky; top: var(--nav-h); z-index: 5;
  background: var(--desk);
  color: var(--desk-ink);
  padding: .6rem 1.1rem .5rem;
  font-size: .88rem;
  font-weight: 600;
  line-height: 1.3;
  border-bottom: 1px solid color-mix(in srgb, var(--desk-ink) 14%, transparent);
}
.progress > div { max-width: 48rem; margin: 0 auto; display: flex; justify-content: space-between; gap: 1rem; }
/* Count and Sign out travel together on the right, so the spine stays a
   two-part row at every width instead of the button wrapping under the name. */
.progress-end { display: flex; align-items: baseline; gap: .9rem; flex: 0 0 auto; }
/* Underlined text, not a filled button. Ending the session is not the action
   this page is asking for -- initialing is -- and a solid button here would
   compete with the one inside every section box. Still a real <button>: it
   performs an action rather than going somewhere, and it takes focus and Enter
   the way the keyboard expects. */
.signout-btn {
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit;
  text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer; opacity: .85;
}
.signout-btn:hover { opacity: 1; }
/* An editable value that reads as the value, not as a control. Used for the
   parent address on /admin/codes/, where the cell IS the edit affordance. */
.linky {
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit; text-align: left;
  text-decoration: underline dotted; text-underline-offset: 2px;
  cursor: pointer;
}
.linky:hover { text-decoration-style: solid; }
.linky:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 2px; }
.signout-btn:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 2px; }
.bar { height: 3px; background: color-mix(in srgb, var(--desk-ink) 20%, transparent); border-radius: 2px; margin-top: .4rem; overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--mark-deep); width: 0; transition: width .3s ease-out; }

/* ---- the syllabus as a notebook ----
 *
 * One section per sheet, turned rather than scrolled. This is the one place in
 * the app where a page turn is not decoration: a parent genuinely works through
 * the syllabus a section at a time, initialling each, and the progress spine
 * has been counting those sections all along.
 *
 * The grab target is the STACKED EDGE on the right -- the remaining pages seen
 * side-on. It is always on screen, which is what the corner peel got wrong: that
 * one sat past the bottom of a sheet taller than the viewport, so nobody ever
 * found it. An edge cannot hide.
 */
/* ---- the syllabus, one section per page ----
 *
 * A page is one major section: a heading and everything under it. The paper
 * stays on <main>, so a page is what is written on the sheet rather than a
 * sheet of its own -- which is also what stopped two pages showing through each
 * other, and what stopped `main:has(.book)` stripping the lined background off
 * the whole public side.
 *
 * No turn animation. A page turn built from CSS transforms can only pivot a
 * rigid rectangle, and it read as a flash rather than as paper.
 */
.book { position: relative; }

/* Imported .docx content is rounded onto the grid by snap.js -- an image at its
   original pixel height, a <sup> that grew the line box, a table with borders
   of its own. There is no rule here to do it: the correction depends on a
   rendered height, which only the browser knows, so it is applied as an inline
   margin from JS. Content that already fits is not touched at all, which is
   the normal case. */

/* Same offsets as .on-rules, so the two-rule buttons in here sit BETWEEN two
   lines instead of having one drawn across them. Padding on the bar rather
   than a lift on each button: the bar stays a whole number of rules. */
/*
 * ROW-GAP ON A WRAPPING ROW -- read this before adding `gap` to a flex or grid
 * container that can wrap.
 *
 * `gap` is not only the space BETWEEN items, it is also the space between
 * FLEX LINES. A row that never wraps spends it horizontally and costs nothing;
 * the moment it wraps, the same value is charged vertically as well, and the
 * container stops being a whole number of rules. Everything below it then sits
 * off the ruling -- and it only happens at the narrow widths where the row
 * wraps, which is to say on a phone, which is where most readers are.
 *
 * So: wrapping rows set `column-gap` and `row-gap` separately, and the row-gap
 * is 0. The items are each a whole number of rules, so touching rows still land
 * on the lines -- on ruled paper a chip per line reads correctly anyway.
 */
.book-bar {
  display: flex; align-items: center; column-gap: .6rem; row-gap: 0; flex-wrap: wrap;
  margin: var(--rule) 0 0;
  padding: calc(var(--baseline) + var(--sit)) 0 var(--lift);
}
.book-bar .spacer { flex: 1; }
.book-bar .where { font-size: .95rem; color: var(--ink-stone); line-height: var(--rule); }

/* ---- "initial here", on the page itself ----
 *
 * The tab stuck on the left edge of a contract at the line you have to sign,
 * pointing in at it. The flags down the right edge are navigation -- which
 * sections still want you, and how to get there. This is the other half: once
 * you are on the page, where on it.
 *
 * ::before so it costs no markup and cannot be tabbed to. It is decorative,
 * and a screen reader is already inside a labelled prompt with a text field in
 * it -- announcing "initial here" beside that is noise.
 *
 * The arrow points RIGHT, into the page, which is the opposite of the
 * navigation flags: those point left at the sheet they belong to from outside
 * it, this one points at the line it belongs to from the margin.
 */
.initial-box::before {
  content: 'Initial here';
  position: absolute;
  /* Out through main's 3.2rem left margin and a little past the paper edge, so
     it reads as stuck ON the page rather than floating beside it. */
  left: -4.9rem;
  top: calc(var(--rule) + 5px);
  width: 4.7rem;
  height: 1.28rem;
  clip-path: polygon(0 0, calc(100% - .55rem) 0, 100% 50%, calc(100% - .55rem) 100%, 0 100%);
  background: var(--tab-3);
  /* Fixed dark ink, like the flags: the pastels are accents sitting ON the
     page rather than the page surface, so they do not flip for dark mode. */
  color: #1a1a1a;
  font-size: .58rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  line-height: 1.28rem;
  text-align: center;
  text-indent: -.45rem;
  box-shadow: -2px 2px 4px rgba(0, 0, 0, .18);
  pointer-events: none;
}
/* Done. Green, quieter, and past tense -- the tab stops being an instruction
   and becomes a receipt. */
.initial-box.done::before {
  content: '✓ Initialled';
  background: var(--tab-1);
  opacity: .7;
  box-shadow: none;
}

/* Below this the paper's left margin is gone and the tab would land on the
   text. The prompt is perfectly findable without it. */
@media (max-width: 62rem) {
  .initial-box::before { display: none; }
}

@media print {
  /* An instruction, not part of the record. */
  .initial-box::before { display: none !important; }
}

/* ---- sign-here flags ----
 *
 * The thin arrow stickers a contract comes back with, stuck down the fore-edge
 * at every line that needs a signature -- and, like the real thing, what you
 * grab to get to one. Each flag jumps to its section.
 *
 * One per section that ASKS FOR SOMETHING, not one per section. That is the
 * whole difference from the tab rail this replaces: nine tabs down the edge to
 * say "five of these need you" is nine things to read to find five, and the
 * four that wanted nothing were the loudest thing on the page.
 *
 * Horizontal text, unlike the notebook deck's vertical index tabs. These are
 * thin tongues rather than card dividers, and a rotated label in 20px of height
 * would be one letter wide.
 */
.flags {
  position: sticky;
  top: calc(var(--nav-h) + var(--rule) * 2);
  float: right;
  width: 7.2rem;
  /* Out past the paper's edge, less a little so each flag's square end meets
     the page rather than floating beside it. */
  margin: 0 calc(-7.2rem - 1.6rem + 1.5rem) var(--rule) 1rem;
  display: flex;
  flex-direction: column;
  gap: .3rem;
  z-index: 3;
}
/* The rail is not a row of buttons on the paper; it is the sticky-note markers
   down the edge, with a shape and a colour of their own. It sits inside main,
   so it has to say no to the tape explicitly. */
main .flags button::before, main .flags button::after { content: none; }
/* `:not(.danger)` is not decoration -- it is specificity, the same trick as the
   `:not([disabled])` on the hover rule below.
   The tape treatment is `main :is(button, .button):not(.danger)` at (0,2,1) and
   it sets `background: transparent`. A plain `.flags button` is (0,1,1) and lost
   to it, so every flag NOT yet initialled came out with no colour at all and the
   rail stopped saying "here is what is left" -- it read as a list of grey words.
   The opt-out list further up only cancels the tape's ::before/::after; it never
   touched the background. The signed state kept its green purely by accident:
   `[data-signed="1"]` adds an attribute and reaches (0,2,1) on its own.
   This brings the resting state to the same (0,2,1) and it wins on source order. */
.flags button:not(.danger) {
  /* display:block so text-overflow works -- as a flex item it does not, and a
     long section name is cut mid-word with no ellipsis. */
  display: block;
  width: 100%;
  min-height: 0;
  height: 1.35rem;
  line-height: 1.35rem;
  padding: 0 .5rem 0 0;
  text-indent: 1.15rem;
  text-align: left;
  border: 0;
  border-radius: 0 3px 3px 0;
  /* The point. A notch cut from the left end aims the flag at the page it
     belongs to, the way the sticker's arrow does. */
  clip-path: polygon(0 50%, .62rem 0, 100% 0, 100% 100%, .62rem 100%);
  background: var(--tab-3);
  /* Fixed dark ink: the pastels are accents sitting ON the page, not the page
     surface, so they do not flip for dark mode and a theme-following label
     would go white on rose. */
  color: #1a1a1a;
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: 2px 2px 4px rgba(0, 0, 0, .18);
  opacity: .88;
  transform: translateX(-4px);
  transition: transform .18s ease-out, opacity .18s ease-out;
  cursor: pointer;
}
/* :not([disabled]) is not decoration -- it is specificity. The base
   `button:hover:not([disabled])` rule repaints every button --accent-soft, and
   it outranks a plain `.flags button:hover`, so hovering a flag turned it
   mustard. On a touch screen the state sticks after a tap, which is how it was
   spotted: the flag you had just pressed was the wrong colour thereafter. */
.flags button:hover:not([disabled]) {
  background: var(--tab-3);
  opacity: 1;
  transform: translateX(0);
}
.flags button[data-signed="1"]:hover:not([disabled]) { background: var(--tab-1); opacity: .85; }
/* Already initialled: green, ticked, and quieter. The flag has stopped being a
   job and become a receipt, so it stops competing with the ones still owed. */
.flags button[data-signed="1"] { background: var(--tab-1); opacity: .6; }
/* The section being read. Slid fully out and given its shadow back, so "where
   am I" and "what is left" are the same glance. */
.flags button[aria-current="true"] {
  opacity: 1;
  transform: translateX(2px);
  box-shadow: 3px 3px 6px rgba(0, 0, 0, .26);
}
.flags button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .flags button { transition: none; }
}

/* No margin to put them in on a narrow window, and a flag over the text is
   worse than no flag. They lie down into a strip above the page instead. */
@media (max-width: 62rem) {
  /* row-gap: 0 -- see the note on .book-bar. This rail is the worst place in
     the app to get that wrong: it only wraps on a phone, it is the control a
     parent uses to find the sections still owed, and the drift lands on the
     document they are signing. Each flag is exactly one rule, so the rows
     touch and every one of them sits on a line. */
  .flags {
    position: static; float: none; width: auto; margin: 0 0 var(--rule);
    flex-direction: row; flex-wrap: wrap; column-gap: .3rem; row-gap: 0;
  }
  /* `:not(.danger)` is specificity, not meaning -- the same trick used at the
     base rule above, and needed for the same reason. A plain `.flags button` is
     (0,1,1) and loses to `.flags button:not(.danger)` at (0,2,1), so NONE of
     this block applied: the flags kept the rail's 1.35rem height on a phone.
     That is 22.95px against a 32px rule -- the wrapped rows could never land on
     the ruling however the gap was set -- and it is also the tap target a
     parent aims at to reach a section they still owe. */
  .flags button:not(.danger) {
    width: auto; max-width: 9rem; height: var(--rule); line-height: var(--rule);
    clip-path: none; border-radius: 3px; text-indent: 0; padding: 0 .6rem;
    transform: none;
  }
  .flags button:hover, .flags button[aria-current="true"] { transform: none; }
}

@media print {
  /* A flag is an instruction, not part of the record. */
  .flags { display: none !important; }
}

/* Print is the whole document in order. Every sheet is in the DOM for exactly
   this: what a parent signs is the syllabus entire, not the page they happen to
   be on. */
@media print {
  .book .sheet[hidden] { display: block !important; }
  .book-bar { display: none !important; }
}

/* ---- syllabus blocks ---- */

.block { margin: 0 0 var(--rule); }
.block :is(h1, h2, h3) { margin: var(--rule) 0 0; }
/* A section stands a full ruled line clear of the text above it, so a parent
   reading a long page can see where one policy ends and the next begins. Two
   rules rather than one because the block above already contributes its own
   bottom margin, and the two collapse to whichever is larger. */
.block:not(:first-child) > :is(h1, h2, h3) { margin-top: calc(var(--rule) * 2); }
/* Except where a subheading follows its own heading -- there the gap would read
   as a missing paragraph rather than as a break. */
.block:has(> :is(h1, h2, h3)) + .block > :is(h1, h2, h3) { margin-top: var(--rule); }
.block ul, .block ol { padding-left: 1.3rem; margin: 0 0 var(--rule); }
.block li { margin: 0; line-height: var(--rule); }

/* The scroller app.js wraps every imported table in. Only the table moves
   sideways; the sheet stays put. `max-width: 100%` rather than a width, so a
   narrow table is still just as wide as its content and does not stretch. */
.table-scroll { max-width: 100%; overflow-x: auto; margin: 0 0 var(--rule); }
/* Anything else in teacher-authored content that can be wider than the page.
   A pasted URL or an email address has no break opportunity in it at all, and
   one long enough will push the sheet out on its own. */
.block { overflow-wrap: anywhere; }
.block img { max-width: 100%; height: auto; }

/* Numbered lists get a fixed-width marker column instead of the browser's.
 *
 * A default `list-style-position: outside` marker is right-aligned to the text
 * edge and hangs to the LEFT of it, so every digit gained pushes the number
 * further out: 1.-9. start at one x, 10. onward start ~8px left of it, and a
 * long list walks steadily toward the amber margin rule at main::after. The
 * fourteen course objectives in a real syllabus cross that boundary in the
 * middle of the list, which is exactly where it is most visible.
 *
 * A counter fixes the column, so the numbers right-align to each other and the
 * text edge is the same on item 1 and item 140. Written on both surfaces at
 * once -- .block is the parent's page, .blk .edit is the editor -- because a
 * teacher who lines a list up in one has to see the same thing in the other. */
:is(.block, .blk .edit) ol {
  list-style: none;
  counter-reset: li-num;
  padding-left: 2.6rem;
}
:is(.block, .blk .edit) ol > li { counter-increment: li-num; position: relative; }
/* Taken OUT of the line box, not just pulled left by a negative margin. An
   inline-block marker is baseline-aligned against the text beside it, and its
   own box was tall enough to push the whole line down -- items measured 14.5px
   off the ruling where a plain paragraph sits at 6px, and the half-pixel is the
   giveaway that a line box was being resized rather than moved. Positioned out
   of flow it cannot affect the line at all, so an item rides the rule exactly
   as a paragraph does and a wrapped item keeps its hanging indent. */
:is(.block, .blk .edit) ol > li::before {
  content: counter(li-num) ".";
  position: absolute;
  box-sizing: border-box;
  /* Wide enough for three digits at 17px; a fourth would need more, and no
     syllabus has ever had one. */
  left: -2.6rem; width: 2.6rem; padding-right: .55rem;
  text-align: right;
}

/* The section that asks for initials. Translucent rather than solid, so the
   ruled lines carry on underneath it and the prompt reads as written on the
   same sheet as the policy it is attesting to. */
.initial-box {
  position: relative;
  background: color-mix(in srgb, var(--ivory) 55%, transparent);
  border: 1px solid var(--ring);
  border-left: 4px solid var(--mark-deep);
  border-radius: var(--radius-sm);
  padding: calc(var(--rule) - 1px) 1.1rem;
  margin: var(--rule) 0;
}
.initial-box .prompt { font-weight: 600; line-height: var(--rule); margin: 0 0 var(--rule); }
.initial-box .row { align-items: center; }

/* Initials are handwriting -- the one place a script face belongs. The same
   blank as every other field, just short and centred. */
.initial-box input {
  /* Two rules here, not one: this blank holds 1.7rem handwriting, and it is the
     one field on the site somebody signs. */
  width: 6.5rem; min-width: 0;
  height: calc(var(--rule) * 2);
  margin: 0;
  font-family: var(--font-hand);
  font-size: 1.7rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-align: center;
  text-transform: uppercase;
  padding: 0 .4rem;
}
.initial-box input::placeholder { color: var(--ink-silver); font-size: 1.3rem; }

.initial-box.done { border-left-color: var(--tab-1); background: transparent; }

/* The one sentence a returning family reads before anything else. Two whole
   rules of margin so the sheet below still starts on a ruled line. */
.amended-banner {
  margin: var(--rule) 0;
  padding: calc(var(--rule) / 2 - 1px) 1.1rem;
  line-height: var(--rule);
  border: 1px solid var(--ring);
  border-left: 4px solid var(--mark-deep);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--mark) 22%, transparent);
  color: var(--ink);
}

/* An amended section: signed once, and the words have moved since.
   The left edge is the whole signal -- a different colour on the rule this box
   already had, rather than a badge or a tint, because a parent scrolling for
   the one thing they have to re-read is looking down the margin.
   The line is a WHOLE rule tall and the box keeps its existing padding, so
   marking a section amended does not push the page off the ruled paper. */
.initial-box.updated { border-left-color: var(--mark-deep); }
.updated-flag {
  margin: 0 0 var(--rule);
  line-height: var(--rule);
  font-size: .85rem; font-weight: 600;
  color: var(--ink-olive);
}
/* The marker band, the same one "you are here" uses everywhere else. */
.updated-flag::before {
  content: ''; display: inline-block; vertical-align: top;
  width: .55rem; height: .55rem; margin: calc((var(--rule) - .55rem) / 2) .45rem 0 0;
  border-radius: 50%; background: var(--mark-deep);
}
.stamp { font-size: .85rem; line-height: var(--rule); color: var(--ink-stone); margin: 0; }
/* The initials are handwriting at 1.6rem sitting in a 1rem line. As a
   baseline-aligned inline-block its own baseline sits 3px off the strut's, so
   it grew every stamp line to 32px -- and since each signed section then ran
   2px long, the drift ACCUMULATED down the page: by the fourth section the text
   was 4px off the rules. `vertical-align: top` takes it out of baseline
   alignment, so its 30px box fits the line exactly and the rest of the document
   stays on the grid. */
.stamp b {
  font-family: var(--font-hand);
  font-size: 1.6rem;
  font-weight: 700;
  line-height: var(--rule);
  color: var(--ink);
  letter-spacing: .04em;
  display: inline-block;
  vertical-align: top;
  transform: rotate(-2deg);
  margin: 0 .15rem;
}

.done-banner {
  border: 1px solid var(--ring);
  border-left: 4px solid var(--tab-1);
  background: color-mix(in srgb, var(--ivory) 55%, transparent);
  border-radius: var(--radius-sm);
  padding: calc(var(--rule) - 1px) 1.2rem;
  text-align: center;
}
.done-banner h2 { margin-top: 0; }

/* ---- narrow screens ---- */

@media (max-width: 44rem) {
  header.site nav { flex-wrap: wrap; row-gap: .3rem; padding: .45rem .7rem; }
  /* Wrap onto a second row rather than scroll sideways.
   *
   * The strip scrolls by default, with the scrollbar hidden, which is fine
   * while everything fits and invisible when it does not: at 390px "Read &
   * initial" sat entirely past the right edge with nothing on screen to
   * suggest it was there. Four items is what tipped it over. A parent looking
   * for the link that lets them sign is exactly who cannot afford to be asked
   * to guess that the row swipes. */
  .nav-main {
    order: 3; flex-basis: 100%; justify-content: flex-start;
    flex-wrap: wrap; overflow-x: visible; row-gap: .1rem;
  }
  /* ...unless the links have already become icons, which is the admin nav.
     The rule above exists because five WORDS cannot share a row with the brand
     at 390px. Three marks can -- they come to about a third of the width -- so
     forcing them onto a line of their own just spends a row saying nothing.
     The public nav is still words and still wraps, which is why this is scoped
     by :has() rather than by page. */
  /* Keeps the base `flex: 1 1 auto; justify-content: center`, so the marks sit
     CENTRED in whatever room is left between the brand and the controls --
     which is what the strip does at full width, and what it should keep doing.
     Only `order` and `flex-basis: 100%` are undone, because those are what put
     it on a row of its own.
     This works now only because Sign out became an icon too: at its full text
     width the three bases came to more than the line could hold, the row wrapped
     before any of the centring applied, and the header read as links hard left
     on one row and controls hard right on the next. */
  .nav-main:has(.to-icon) {
    order: 0; flex-basis: auto; margin-left: .4rem;
  }
  .nav-end { margin-left: auto; }
}
@media (max-width: 34rem) {
  /* Padding stays a whole number of rules -- narrowing the sheet must not knock
     the grid out from under it. */
  main { padding: calc(var(--rule) * 2) 1.1rem calc(var(--rule) * 2) 2.4rem; border-radius: 0; margin-top: 0; }
  main::before { left: .45rem; }
  main::after { left: 1.75rem; }
  h1 { font-size: 1.55rem; }
  .brand { font-size: 1.15rem; }
}

/* ---- motion off ----
 *
 * Every band above still ARRIVES -- it just arrives already drawn. Dropping the
 * transform instead of the transition would leave the highlight permanently at
 * scale 0, i.e. the emphasis would silently vanish for the users who asked for
 * less motion.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .mark::before, .tape::before, a::before,
  .nav-main a::before, .nav-end a::before, .nav-end button:not(.theme-toggle)::before { transition: none; }
  button:active:not([disabled]) { transform: none; }
}

/* ---- forced colours ----
 *
 * A Windows high-contrast user gets no backgrounds at all, which would delete
 * every highlight on the page. Give the emphasis back as a border.
 */
@media (forced-colors: active) {
  .mark, .tape { border-bottom: 2px solid; }
  .mark::before, .tape::before, a::before { display: none; }
  .nav-main a[aria-current="page"] { border-color: currentColor; }
  button, .button { border: 1px solid; }
}

/* ---- print: the signed copy ---- */

@media print {
  header.site, .progress, .skip, button, .no-print { display: none !important; }
  body { background: #fff; }
  main {
    max-width: none; margin: 0; padding: 0 .5in;
    background-image: none; box-shadow: none; border-radius: 0;
  }
  main::before, main::after { display: none; }
  .initial-box { break-inside: avoid; border-color: #999; background: #fff; box-shadow: none; }
  .stamp b { color: #000; }
  /* The marker does not survive a printer, so emphasis falls back to the
     underline that was there the whole time. */
  .mark::before, .tape::before, a::before { display: none; }
  .tape { transform: none; }
  a { color: inherit; text-decoration: none; }
}
