/* ---------------------------------------------------------------------------
   Self-hosted webfonts (Besley + Familjen Grotesk).
   Copied from Google Fonts so no visitor request ever leaves Tutora's own
   domain -- see js/vendor/README.md for the reasoning. These are the exact
   @font-face blocks Google served, with the URLs pointed at ../fonts/, so
   unicode-range subsetting and the variable weight axes behave identically.
   --------------------------------------------------------------------------- */
/* latin-ext */
@font-face {
  font-family: 'Besley';
  font-style: italic;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/besley-cb187696.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Besley';
  font-style: italic;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/besley-437da7f1.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Besley';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/besley-1196ee0b.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Besley';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url(../fonts/besley-b0882563.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-8b99df0c.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-396ea7d3.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-2ddfbc6a.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-8e0a0fb3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-cf17c44e.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(../fonts/familjengrotesk-912ffeda.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   Tutora — design system v3: "The schoolbook"
   One serif voice — Besley, a revival of Robert Besley's 1845 London
   Clarendon, the letterform of British schoolbooks and printed exam papers —
   set for headlines and body alike, the way a publisher would.
   Familjen Grotesk appears only in the interface: nav, buttons, small notes.
   Near-white paper, green-black ink, deep bottle green, marking-pen green.
   No caps-lock labels, no mono decorations, no stat bands, no numbered steps.
   ========================================================================== */

:root {
  /* Colour */
  --paper: #FDFCFA;
  --paper-raised: #FFFFFF;
  --ink: #20281F;
  --ink-soft: #4E584D;
  --green-deep: #16352A;
  --green-deep-hover: #1D4536;
  --on-deep: #F5F4EC;
  --on-deep-soft: #A9BFAF;
  --pen: #14603A;
  --pen-bright: #1E7A4A;
  --rule: #DBE1D3;
  --rule-strong: #BAC4AE;
  --error: #A4321F;

  /* Coloured surfaces — used to break up the white */
  --wash: #EEF2E9;          /* soft sage section */
  --wash-deep: #E3EBDD;     /* a touch stronger */
  --wash-warm: #F6F2E9;     /* warm paper, for contrast bands */
  --mist: #F3F6F1;          /* barely-there tint for cards on white */
  --gold: #B9852F;          /* restrained brass accent, used sparingly */
  --gold-soft: #F0E6D2;
  /* ⚠️ TEXT ON --gold-soft, and nowhere else. --gold on --gold-soft measures
     2.63:1 -- barely legible -- and that pairing is every .pill--due badge and
     the .tracker-badge. This is the same hue taken down until it clears 4.5:1
     on that background. Do NOT use it on paper: --gold is right there, and do
     not "simplify" the two into one. */
  --gold-ink: #865D1D;

  /* Field surfaces — crafted, not drab */
  --field-bg: #F7F9F4;
  --field-border: #C4CEB8;

  /* Type */
  --font-serif: "Besley", "Iowan Old Style", Georgia, serif;
  --font-ui: "Familjen Grotesk", "Segoe UI", system-ui, sans-serif;

  /* Rhythm */
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 40px;
  --space-5: 72px;
  --space-6: 120px;

  --container: 1100px;
  --radius: 4px;
  --header-h: 78px;
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 24px);
}

body {
  margin: 0;
  font-family: var(--font-serif);
  font-size: 1.125rem;
  line-height: 1.7;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: inline-block; vertical-align: middle; }

h1, h2, h3, h4 {
  font-family: var(--font-serif);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.005em;
  margin: 0 0 var(--space-2);
  text-wrap: balance;
}

h1 { font-size: clamp(2.7rem, 5.4vw, 4.4rem); }
h2 { font-size: clamp(1.85rem, 3vw, 2.6rem); }
h3 { font-size: 1.3rem; line-height: 1.3; }

p { margin: 0 0 var(--space-2); }
p:last-child { margin-bottom: 0; }

a { color: var(--pen); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--green-deep); }

::selection { background: #D8E8DE; }

/* Ensure the hidden attribute always wins, even on elements with a set display
   (e.g. .link-arrow uses inline-flex, which would otherwise override it). */
[hidden] { display: none !important; }

:focus-visible {
  outline: 2px solid var(--pen-bright);
  outline-offset: 3px;
  border-radius: 2px;
}

.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(20px, 4vw, 40px);
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 8px;
  z-index: 200;
  background: var(--green-deep);
  color: var(--on-deep);
  padding: 10px 18px;
  border-radius: var(--radius);
  font-family: var(--font-ui);
}
.skip-link:focus { left: 8px; }

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

.lead {
  font-size: clamp(1.15rem, 1.7vw, 1.3rem);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 54ch;
}

/* A quiet interface note — sentence case, never shouting */
.note {
  font-family: var(--font-ui);
  font-size: 0.9rem;
  color: var(--ink-soft);
}

/* Footnotes — a real typographic device, set like a book's */
.fn-ref {
  font-family: var(--font-serif);
  color: var(--pen);
  text-decoration: none;
  font-size: 0.72em;
}
.footnote {
  display: flex;
  gap: 8px;
  max-width: 46ch;
  font-size: 0.9rem;
  font-style: italic;
  color: var(--ink-soft);
  padding-top: var(--space-2);
  margin-top: var(--space-4);
  border-top: 1px solid var(--rule);
}
.footnote sup { font-style: normal; color: var(--pen); }

/* --------------------------------------------------------------------------
   Buttons & links
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-family: var(--font-ui);
  font-size: 0.98rem;
  font-weight: 600;
  padding: 14px 28px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease;
}

.btn-primary { background: var(--green-deep); color: var(--on-deep); }
.btn-primary:hover { background: var(--green-deep-hover); color: var(--on-deep); }

.btn-outline { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.btn-outline:hover { border-color: var(--ink); color: var(--ink); }

.btn-on-deep { background: var(--on-deep); color: var(--green-deep); }
.btn-on-deep:hover { background: #FFFFFF; color: var(--green-deep); }
/* Secondary action on a dark band — reads as an alternative to .btn-on-deep
   rather than competing with it. */
.btn-outline-on-deep { background: transparent; color: var(--on-deep); border-color: var(--on-deep-soft); }
.btn-outline-on-deep:hover { border-color: var(--on-deep); color: var(--on-deep); }

.btn-lg { padding: 17px 34px; font-size: 1.02rem; }

/* Used on <a> and, in a few places, on <button> (Download PDF, Remove paper).
   Without the resets a button keeps its browser chrome, so those read as grey
   boxes with a black border rather than as links. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 0.98rem;
  color: var(--pen);
  text-decoration: none;
  background: none;
  border: 0;
  border-bottom: 1px solid currentColor;
  padding: 0 0 2px;
  cursor: pointer;
}
.link-arrow svg { transition: transform 180ms ease; }
.link-arrow:hover svg { transform: translateX(4px); }

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(253, 252, 250, 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--rule);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-h);
}

.logo {
  font-family: var(--font-serif);
  font-size: 1.55rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.logo:hover { color: var(--ink); }
.logo-dot { color: var(--pen-bright); }

.site-nav {
  display: flex;
  align-items: center;
  /* Tightened 24 Aug to make room for an eighth item (About). At the old
     clamp(20px, 3vw, 34px) the seven gaps alone ate 238px at 1280 and the row
     wrapped four labels mid-phrase. Width comes out of the SPACING, not out of
     the labels -- shortening "Book a free consultation" would be the CTA paying
     for the nav. tools/nav-check.mjs is what proves this still fits. */
  gap: clamp(14px, 1.9vw, 24px);
}

.site-nav > a:not(.btn) {
  font-family: var(--font-ui);
  font-size: 0.94rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
}
.site-nav > a:not(.btn):hover { border-bottom-color: var(--rule-strong); }
/* Header buttons only. The global .btn stays at 14px/28px -- every other button
   on the site is fine, and it is only this row that has run out of space. */
.site-nav > .btn { padding: 12px 18px; font-size: 0.93rem; }
.site-nav > a[aria-current="page"] { border-bottom-color: var(--pen-bright); }

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  padding: 10px;
  background: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  cursor: pointer;
  position: relative;                /* for the unread dot below */
}
.nav-toggle span {
  display: block;
  height: 2px;
  background: var(--ink);
  transition: transform 200ms ease, opacity 200ms ease;
}
/* Something behind the hamburger is unread.
   On a phone the nav is inside this button, so a .badge-count on Messages or
   Progress is invisible until the menu is opened -- which is the one moment you
   no longer need telling. A dot on the button itself carries "there is
   something" without trying to fit a number onto a 44px target, and it is the
   same red as the badge it stands for. Set by syncNavToggleDot() in main.js,
   which runs whenever any nav badge changes. */
.nav-toggle.has-unread::after {
  content: "";
  position: absolute;
  top: -3px;
  right: -3px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--error);
  /* Rings the dot in the page background so it reads as a separate object where
     it overlaps the button's border rather than as a bump in the outline. */
  box-shadow: 0 0 0 2px var(--paper);
}

/* 1100, not 940. The inline nav is a flex row with no wrapping, so the moment it
   runs out of room every item shrinks and its label breaks in half -- "How it /
   works", "Log / in". It does not fail gracefully, it just looks broken.
   
   That was ALREADY happening between 941px and roughly 1040px before the
   diagnostic button existed -- the button widened the band rather than creating
   it. Measured with the row forced open: broken to 1040, tight at 1060 (7px of
   clearance), comfortable from 1100 (39px). So the menu takes over below 1100,
   which removes the broken range instead of moving it.
   
   If a nav item is ever added or removed, re-measure. There is no slack here and
   nothing warns you -- the row simply starts wrapping. */
@media (max-width: 1100px) {
  .nav-toggle { display: flex; }

  .site-nav {
    position: absolute;
    top: var(--header-h);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
    padding: var(--space-2) clamp(20px, 4vw, 40px) var(--space-3);
    display: none;
  }
  .site-nav.open { display: flex; }
  .site-nav > a:not(.btn) {
    padding: 14px 0;
    border-bottom: 1px solid var(--rule);
  }
  /* Full-width in the stacked menu, so the desktop tightening above must not
     follow it in -- a menu item is a tap target before it is a label. */
  .site-nav > .btn { margin-top: var(--space-2); padding: 14px 28px; font-size: 0.98rem; }

  .nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  .nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
  .nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero { padding: clamp(64px, 10vh, 128px) 0 clamp(48px, 7vh, 88px); }

/* Ruled paper, with a space cleared for the writing.
   ---------------------------------------------------------------------------
   The hero sits on exercise-book ruling that is strongest at the margins and
   gone by the time it reaches the copy, so the page reads as paper somebody has
   cleared a space on rather than a texture laid evenly under everything.

   THE TWO HALVES DEPEND ON EACH OTHER. The rules are pitched at 0.15, which is
   nearly three times the strength an unmasked version could carry -- an earlier
   flat version had to sit at 0.055 to stop it fighting the headline, and at
   that weight it was barely visible at all. The mask is what buys the darkness
   back: it is all spent at the edges, where there is nothing to compete with.
   Lower the mask and the rules must come down with it, or the h1 loses.

   It has to be a pseudo-element. Masking .hero directly would take the paper
   colour with it and punch a transparent hole through the middle of the
   section. */
.hero { position: relative; }

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* 34px is the body's own line-height at 1.125rem/1.7 -- the rules sit on the
     rhythm the text already uses rather than on a grid of their own, which is
     the whole reason it reads as ruled paper and not as a stripe pattern. */
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0 33px,
    rgba(32, 40, 31, 0.15) 33px 34px);
  background-position: 0 8px;
  /* Transparent mask = no ruling, opaque = full strength. Nothing across the
     middle third, then a ramp out to full at the edges. An ellipse rather than
     a circle because what is being cleared -- a headline beside a panel -- is
     wide and short. */
  /* Two layers, INTERSECTED. The ellipse clears the middle; the vertical
     gradient takes the bottom out so the ruling dissolves into the section
     below instead of running full-weight into the seam and stopping dead.
     Intersect = both must be opaque for ruling to show, so the weaker layer
     wins at any given point.

     The composite is not optional. Its default is `add` (union), which is the
     precise opposite of this: ruling would appear everywhere, headline
     included. Both spellings are set -- the legacy WebKit keyword and the
     standard one, standard last so it wins where both are understood. */
  -webkit-mask-image:
      radial-gradient(ellipse 64% 58% at 50% 50%,
        rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 33%,
        rgba(0, 0, 0, 0.42) 66%, rgba(0, 0, 0, 1) 100%),
      linear-gradient(to bottom,
        rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 55%,
        rgba(0, 0, 0, 0.35) 82%, rgba(0, 0, 0, 0) 100%);
          mask-image:
      radial-gradient(ellipse 64% 58% at 50% 50%,
        rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 33%,
        rgba(0, 0, 0, 0.42) 66%, rgba(0, 0, 0, 1) 100%),
      linear-gradient(to bottom,
        rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 55%,
        rgba(0, 0, 0, 0.35) 82%, rgba(0, 0, 0, 0) 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* The copy and the figure have to sit ABOVE the ruling. An absolutely
   positioned ::before paints in a later layer than in-flow content, so without
   this the rules are drawn OVER the headline rather than behind it.
   Safe to make this a containing block: everything absolute inside the hero
   (.circled's ring, .dg-taster's confetti layer) already resolves against its
   own relatively-positioned parent, not against an ancestor. */
.hero-inner { position: relative; z-index: 1; }

@media (max-width: 960px) {
  /* 960px is where .hero-inner drops to one column, so it is where the shape
     of the thing being cleared changes -- not a round number picked to look
     tidy. Stacked, the hero is tall and narrow, and the desktop ellipse would
     clear everything except two thin bands at the very top and bottom, which
     reads as a rendering fault rather than as ruled paper. Wide and shallow
     keeps ruling above and below the content, which is where a phone has the
     room for it. */
  .hero::before {
    /* Same idea, and the bottom fade starts LATER than on desktop. Stacked,
       the hero is roughly twice as tall, so a fade beginning at 55% would eat
       most of the visible ruling rather than just the seam. */
    -webkit-mask-image:
        radial-gradient(ellipse 92% 42% at 50% 48%,
          rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 30%,
          rgba(0, 0, 0, 0.42) 64%, rgba(0, 0, 0, 1) 100%),
        linear-gradient(to bottom,
          rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 78%,
          rgba(0, 0, 0, 0.35) 91%, rgba(0, 0, 0, 0) 100%);
            mask-image:
        radial-gradient(ellipse 92% 42% at 50% 48%,
          rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 30%,
          rgba(0, 0, 0, 0.42) 64%, rgba(0, 0, 0, 1) 100%),
        linear-gradient(to bottom,
          rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 78%,
          rgba(0, 0, 0, 0.35) 91%, rgba(0, 0, 0, 0) 100%);
    -webkit-mask-composite: source-in;
            mask-composite: intersect;
  }
}

.hero-inner {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(40px, 6vw, 96px);
  align-items: center;
}

.hero h1 { margin-bottom: var(--space-3); }

.hero .lead { margin-bottom: var(--space-4); }

.hero-ctas {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.hero-trust {
  margin: var(--space-3) 0 0;
  font-family: var(--font-ui);
  font-size: 0.92rem;
  color: var(--ink-soft);
}

@media (max-width: 960px) {
  .hero-inner { grid-template-columns: 1fr; }
  .hero-visual { max-width: 460px; }
}

/* The signature: one pen-drawn ring around one word */
.circled {
  position: relative;
  display: inline-block;
  padding: 0 0.08em;
}
.circled svg {
  position: absolute;
  inset: -0.18em -0.22em -0.28em;
  width: calc(100% + 0.44em);
  height: calc(100% + 0.46em);
  overflow: visible;
  pointer-events: none;
}
.circled path {
  fill: none;
  stroke: var(--pen-bright);
  stroke-width: 2.75;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.js .circled path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: draw-ring 800ms cubic-bezier(0.65, 0, 0.35, 1) 500ms forwards;
}
@keyframes draw-ring { to { stroke-dashoffset: 0; } }

/* The hand-drawn ring, reused around a button rather than a word. The hero's
   .circled wraps text, so its padding is set in em of the text it circles; a
   button brings its own padding and needs the ring pushed out further, and the
   button must sit above the SVG or the ring eats the click. */
.circled--btn { padding: 0 0.25em; }
.circled--btn > button { position: relative; z-index: 1; }
.circled--btn svg { inset: -0.5em -0.6em -0.55em; width: calc(100% + 1.2em); height: calc(100% + 1.05em); }
.circled--btn path { stroke-width: 2; }

/* "click me" beside it. Handwritten register, in the same ink as the ring, so
   the two read as one annotation somebody pencilled on rather than two widgets.
   aria-hidden in the markup: it is an instruction to a mouse, and a screen
   reader already has a button that says View. */
.click-me {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--pen-bright);
  white-space: nowrap;
}

/* A*A*A*A, set as a fact rather than as words.
   --------------------------------------------------------------------------
   The grades are the hardest thing on the landing page and were reading as
   ordinary prose. Serif, tracked out slightly, in the accent ink -- the same
   treatment the report card gives a working grade, so the two read as the same
   kind of statement.

   The stars are real <sup>, which is how a grade is written. Raising them with
   font-size alone would leave them sitting on the baseline like multiplication
   signs, which is what A*A*A*A looks like when it is typed rather than set. */
.grades {
  font-family: var(--font-serif);
  font-weight: 700;
  /* Barely any. 0.04em read as four separate grades with gaps between them --
     A* A* A* A rather than a single result. */
  letter-spacing: 0.005em;
  color: var(--pen-bright);
  white-space: nowrap;
}
.grades sup {
  font-size: 0.6em;
  /* Pulled tight on both sides. A superscript star after a capital A leaves a
     gap on each side that the eye reads as a word space, which is what broke
     the string into four pieces. */
  margin-left: -0.06em;
  margin-right: -0.02em;
  top: -0.36em;
  position: relative;
  line-height: 0;
}
/* In running prose it should sit up with the text around it, not shout over
   it -- a shade larger is enough when the colour and the serif are already
   doing the work. */
.why-list .grades { font-size: 1.12em; }
.section-head .lead .grades { font-size: 1.1em; }
/* Bigger than the line it sits in. The trust line is set at 0.92rem, and at
   parity the grades read as small print -- which is the opposite of the job. */
.hero-trust .grades { font-size: 1.3em; }

/* Browser-chrome frame round the "See what a parent sees." screenshot on
   index.html. Faded at the bottom rather than cropped hard, so it reads as
   "there's more, go look" instead of the image just stopping mid-report. */
.report-window {
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--rule);
  box-shadow: 0 1px 2px rgba(32, 40, 31, 0.04), 0 14px 36px -18px rgba(32, 40, 31, 0.14);
  background: var(--paper-raised);
}
.report-window-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  background: var(--wash);
  border-bottom: 1px solid var(--rule);
}
.rw-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--rule-strong); flex-shrink: 0; }
.rw-url {
  margin-left: 8px;
  font-family: var(--font-ui);
  font-size: 0.76rem;
  color: var(--ink-soft);
  background: var(--paper-raised);
  padding: 3px 10px;
  border-radius: 20px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* No max-height/fade here on purpose: the screenshot itself is a curated
   crop (tiles + the "this week at a glance" note, stitched together by
   tools/make-report-preview-shot.mjs, skipping the chart in between) rather
   than the top of a much longer image truncated mid-content. A fade cutting
   into the one sentence doing the actual selling defeated the point of
   picking it -- Dylan, 5 Sep. */
.report-window-shot { position: relative; }
.report-window-shot img { display: block; width: 100%; height: auto; }

/* Session note — a printed record, not a dashboard widget */
.report-card {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(32, 40, 31, 0.04), 0 14px 36px -18px rgba(32, 40, 31, 0.14);
  padding: var(--space-3) var(--space-3) var(--space-2);
}

.report-head {
  font-style: italic;
  color: var(--ink-soft);
  font-size: 0.95rem;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule);
}

.report-subject {
  font-size: 1.35rem;
  font-weight: 600;
  margin: var(--space-2) 0 var(--space-1);
}

.report-list {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
}
.report-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 9px 0;
  border-bottom: 1px dotted var(--rule);
  font-size: 1rem;
}
.report-list li:last-child { border-bottom: 0; }
.report-list .tick { color: var(--pen-bright); flex-shrink: 0; align-self: center; }
.report-list .aside { font-style: italic; font-size: 0.9rem; color: var(--ink-soft); white-space: nowrap; }

.report-foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  border-top: 1px solid var(--rule);
  padding-top: 14px;
  margin-top: var(--space-1);
  font-size: 0.95rem;
  color: var(--ink-soft);
  font-style: italic;
}
.grade-move {
  font-style: normal;
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--pen);
}

/* --------------------------------------------------------------------------
   Sections — one primitive: the ruled line
   -------------------------------------------------------------------------- */

.section { padding: clamp(64px, 9vh, 112px) 0; }
.section + .section { border-top: 1px solid var(--rule); }

.section-head { max-width: 60ch; margin-bottom: var(--space-4); }
.section-head .lead { margin-top: var(--space-2); }

/* --------------------------------------------------------------------------
   Why list — few items, one line each
   -------------------------------------------------------------------------- */

.why-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 820px;
}

.why-list li {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-1) var(--space-4);
  align-items: baseline;
  padding: var(--space-3) 0;
  border-top: 1px solid var(--rule);
}
.why-list li:last-child { border-bottom: 1px solid var(--rule); }

.why-list h3 { margin: 0; font-size: 1.35rem; }
.why-list p { margin: 0; color: var(--ink-soft); font-size: 1.05rem; }

@media (max-width: 700px) {
  .why-list li { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   Contents rows (subjects index)
   -------------------------------------------------------------------------- */

.toc { border-top: 1px solid var(--rule-strong); }

.toc-row {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 6fr) auto;
  align-items: baseline;
  gap: var(--space-3);
  padding: clamp(26px, 3.6vh, 38px) 0;
  border-bottom: 1px solid var(--rule-strong);
  text-decoration: none;
  color: var(--ink);
}

.toc-title {
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  font-weight: 500;
  transition: color 180ms ease;
}
.toc-row:hover .toc-title { color: var(--pen); font-style: italic; }

.toc-detail {
  font-style: italic;
  font-size: 1rem;
  color: var(--ink-soft);
}

.toc-arrow {
  color: var(--ink);
  transition: transform 180ms ease, color 180ms ease;
  align-self: center;
}
.toc-row:hover .toc-arrow { transform: translateX(6px); color: var(--pen); }

@media (max-width: 760px) {
  .toc-row { grid-template-columns: 1fr auto; }
  .toc-detail { grid-column: 1 / -1; grid-row: 2; }
}

/* --------------------------------------------------------------------------
   Steps — written like a book, not numbered like a wizard
   -------------------------------------------------------------------------- */

.steps {
  max-width: 720px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.steps li {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--rule);
}
.steps li:last-child { border-bottom: 1px solid var(--rule); }
.steps .run-in {
  font-style: italic;
  color: var(--pen);
  margin-right: 0.35em;
}
.steps p { margin: 0; color: var(--ink); font-size: 1.08rem; }

/* --------------------------------------------------------------------------
   Quote & proof
   -------------------------------------------------------------------------- */

.quote-lead {
  border-left: 2px solid var(--pen-bright);
  padding-left: clamp(20px, 3vw, 36px);
  margin: 0;
  max-width: 880px;
}
.quote-lead p {
  font-size: clamp(1.5rem, 2.8vw, 2.2rem);
  font-style: italic;
  font-weight: 400;
  line-height: 1.4;
  margin-bottom: var(--space-3);
  text-wrap: balance;
}
.quote-who {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  color: var(--ink-soft);
}
.quote-who strong { color: var(--ink); font-weight: 600; }

.proof-line {
  margin-top: var(--space-4);
  font-style: italic;
  color: var(--ink-soft);
  font-size: 1.02rem;
}

/* --------------------------------------------------------------------------
   Split layouts, check lists, subject index
   -------------------------------------------------------------------------- */

.split {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
}

.split-panel {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-3);
}
.split-panel h3 { margin-bottom: var(--space-2); }

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; }
}

.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.check-list li {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 11px 0;
  border-bottom: 1px dotted var(--rule);
  font-size: 1rem;
}
.check-list li:last-child { border-bottom: 0; }
.check-list svg { color: var(--pen-bright); flex-shrink: 0; transform: translateY(2px); }
/* ⚠️ THE LI IS A FLEX CONTAINER, so any inline markup inside the text SPLITS
   THE LINE. Contiguous text becomes one anonymous flex item, but wrap a word in
   <sup>/<strong>/<em> and you get three items with a 12px gap between them --
   "...specification  *  in and got an A        it" was a real render of a line
   that read correctly in the source.

   Anything richer than plain text must be wrapped in a single <span> so the row
   stays two items: the tick, and everything else. signup.html already does
   this. tools/page-markup-check.mjs fails the build if a new one appears. */

.subject-index {
  columns: 2;
  column-gap: var(--space-4);
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  font-size: 1rem;
  color: var(--ink-soft);
}
.subject-index li {
  padding: 8px 0;
  border-bottom: 1px dotted var(--rule);
  break-inside: avoid;
}

/* Each subject now carries an icon and the boards we hold for it, so a row is
   two lines rather than one.

   The ICON IS DECORATIVE and every one is aria-hidden: the subject name beside
   it already says what it is, and a screen reader announcing "beaker Chemistry"
   is worse than "Chemistry". They exist to make the list scannable, which is
   the one thing a two-column list of seven words is bad at.

   flex on the first line only, so the boards sit under the NAME rather than
   under the icon -- the icon column would otherwise indent them and they would
   read as belonging to it. */
/* flex-start, not center: "Computer Science" wraps to two lines in a phone's
   narrow column, and centring floated its icon down to the middle of the pair
   while every other icon sat on line one. The icon belongs against the first
   line whatever the name does. */
.subj-row { display: flex; align-items: flex-start; gap: 8px; }
.subj-icon { flex: none; color: var(--pen); opacity: 0.85; margin-top: 3px; }
.subj-name { font-weight: 500; color: var(--ink); }
.subj-boards {
  display: block;
  margin-top: 1px;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
}
/* IGCSE, which has one board across four subjects and would be four identical
   lines if it were folded into the list above. */
.subject-note { margin-top: var(--space-2); }

/* Deliberately stays at two columns on a phone. Subject names are short, and
   collapsing to one turns a glanceable list into a long scroll. */

/* A single large spoken line — replaces the stat block */
.big-line {
  font-size: clamp(1.9rem, 4vw, 3.1rem);
  font-weight: 500;
  line-height: 1.2;
  max-width: 22ch;
  margin-bottom: var(--space-3);
  text-wrap: balance;
}
.big-line em { font-style: italic; color: var(--pen); }

/* --------------------------------------------------------------------------
   Pricing
   -------------------------------------------------------------------------- */

.price-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  align-items: stretch;
}

.price-card {
  display: flex;
  flex-direction: column;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-3) var(--space-3);
}

.price-card.featured {
  background: var(--green-deep);
  border-color: var(--green-deep);
  color: var(--on-deep);
}
.price-card.featured .price-blurb,
.price-card.featured .check-list li { color: var(--on-deep-soft); }
.price-card.featured .check-list li { border-bottom-color: rgba(245, 244, 236, 0.15); }
.price-card.featured .check-list svg { color: #7FBF9A; }
.price-card.featured .price-flag { color: #9FD4B4; }

.price-flag {
  font-style: italic;
  font-size: 0.95rem;
  color: var(--ink-soft);
  display: block;
  min-height: 1.5em;
  margin-bottom: 4px;
}

.price-level { font-size: 1.3rem; font-weight: 600; margin-bottom: var(--space-2); }

.price-amount {
  font-size: clamp(2.5rem, 4vw, 3.2rem);
  font-weight: 500;
  line-height: 1;
  margin-bottom: 8px;
}
.price-amount .per { font-size: 1rem; font-style: italic; font-weight: 400; opacity: 0.7; }

.price-blurb {
  color: var(--ink-soft);
  font-size: 1rem;
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}
.price-card.featured .price-blurb { border-bottom-color: rgba(245, 244, 236, 0.15); }

.price-card .check-list { flex: 1; margin-bottom: var(--space-3); }
.price-card .check-list li { font-size: 0.98rem; }

@media (max-width: 960px) {
  /* margin-inline: auto -- same bug as .hero-figure--tracker (see its own
     note): a max-width with no centring sits flush left in a full-width
     container. Invisible on a phone (520px exceeds the viewport, the cap
     never binds); an iPad is exactly the width where it does -- Dylan, 6
     Sep: "on the pricing page, the options aren't centred." */
  .price-grid { grid-template-columns: 1fr; max-width: 520px; margin-inline: auto; }
}


/* --------------------------------------------------------------------------
   FAQ
   -------------------------------------------------------------------------- */

.faq { border-top: 1px solid var(--rule-strong); max-width: 760px; }

.faq details { border-bottom: 1px solid var(--rule-strong); }

.faq summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  cursor: pointer;
  list-style: none;
  font-size: 1.2rem;
  font-weight: 500;
}
.faq summary::-webkit-details-marker { display: none; }

/* The question inside a summary is an <h3> so that the page segments into one
   block per answer instead of one 290-word slab -- extractive systems (and the
   GEO pack's scorer) split on h1-h4 and nothing else, and a <summary> is not a
   heading to any of them.

   It must not LOOK like a heading. Left alone the h3 brought its own 1.3rem and
   its own margins and every FAQ row grew by 10px, which was measured rather than
   noticed. Everything comes from the summary instead. */
.faq summary .faq-q {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  margin: 0;
}
.faq summary svg { flex-shrink: 0; color: var(--pen); transition: transform 200ms ease; }
.faq details[open] summary svg { transform: rotate(45deg); }

.faq-body {
  padding: 0 0 var(--space-3);
  color: var(--ink-soft);
  max-width: 60ch;
}

/* --------------------------------------------------------------------------
   Page hero (subpages)
   -------------------------------------------------------------------------- */

.page-hero {
  padding: clamp(56px, 9vh, 104px) 0 clamp(36px, 5vh, 60px);
  border-bottom: 1px solid var(--rule);
}
.page-hero h1 { font-size: clamp(2.3rem, 4.4vw, 3.6rem); margin-bottom: var(--space-2); }
/* about.html only: Dylan, 5 Sep -- the rule under "The two of us." read as a
   redundant banner this close to the founders section right below it.
   Removing the rule alone left the hero's own bottom padding and the
   founders section's own top padding stacked with nothing to break them up
   visually, which read as a much bigger gap than either was on its own --
   pull both in together rather than just the one side. */
.page-hero--flush { border-bottom: none; padding-bottom: var(--space-4); }
/* The founders grid has its own top rule and its own 40px top padding right
   below (see .founders), so giving the section ANY top padding of its own
   just stacks a second gap under the first -- zero here, let .founders be
   the only thing spacing the photos off the hero. */
.page-hero--flush + .section { padding-top: 0; }

/* --------------------------------------------------------------------------
   Contact
   -------------------------------------------------------------------------- */

.contact-layout {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
  gap: clamp(32px, 4vw, 64px);
  align-items: start;
}

@media (max-width: 960px) {
  .contact-layout { grid-template-columns: 1fr; }
}

.form-card {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: clamp(24px, 3.5vw, 44px);
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2) var(--space-3);
}
@media (max-width: 560px) {
  .form-row { grid-template-columns: 1fr; }
}

.field { margin-bottom: var(--space-2); }

.field label {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 600;
  margin-bottom: 7px;
}
.req { color: var(--pen); }

.field input,
.field select,
.field textarea {
  width: 100%;
  font-family: var(--font-ui);
  font-size: 1rem;
  color: var(--ink);
  background: var(--field-bg);
  border: 1.5px solid var(--field-border);
  border-radius: var(--radius);
  padding: 13px 15px;
  transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}
.field textarea { min-height: 140px; resize: vertical; }

/* Show/hide password. Edge draws its own reveal control on desktop, which is
   why this looked fine on a laptop -- Chrome and every mobile browser draw
   nothing, so on a phone there was no way to check what you had typed.
   Built in JS (see main.js) so it covers every password field, including any
   added later, rather than ten hand-edited inputs. */
.pw-wrap { position: relative; display: block; }
/* Edge draws its own reveal control on desktop. Ours is the one that exists on
   every browser and on a phone, so the native one goes -- two eyes in one field
   is worse than either alone. Firefox and Safari draw nothing here anyway. */
.pw-wrap input::-ms-reveal,
.pw-wrap input::-ms-clear { display: none; width: 0; height: 0; }
.field .pw-wrap input, .pw-wrap input { padding-right: 3.1rem; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer;
  color: var(--ink-soft); padding: 6px; border-radius: 4px;
  line-height: 0;
}
.pw-toggle svg { display: block; }
.pw-toggle:hover { color: var(--pen); }
.pw-toggle:focus-visible { outline: 2px solid var(--pen-bright); outline-offset: 1px; }

.field input::placeholder,
.field textarea::placeholder { color: #9AA792; }

.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--pen-bright); }

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  background: var(--paper-raised);
  border-color: var(--pen);
  box-shadow: 0 0 0 3px rgba(20, 96, 58, 0.16);
}

.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: var(--error); }

.hint {
  font-family: var(--font-ui);
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin: 7px 0 0;
}

.form-status {
  display: none;
  align-items: baseline;
  gap: 10px;
  margin-top: var(--space-2);
  padding: 14px 16px;
  border: 1px solid var(--pen-bright);
  border-radius: var(--radius);
  background: #F0F6F1;
  color: var(--pen);
  font-family: var(--font-ui);
  font-size: 0.95rem;
}
.form-status.visible { display: flex; }
.form-status.error { color: #9b1c1c; border-color: #fca5a5; background: #fef2f2; }
.form-status svg { flex-shrink: 0; transform: translateY(2px); }

.aside-card {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
}
.aside-card h3 { font-size: 1.2rem; margin-bottom: var(--space-2); }

/* Contact page: two ways to reach us, stacked in one card. */
.contact-h { font-size: 1.5rem; margin: 0 0 var(--space-2); }
.contact-h--sep {
  margin-top: var(--space-4); padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}
.contact-cta { margin-top: var(--space-2); }

.contact-line {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px dotted var(--rule);
  font-size: 1rem;
}
.contact-line:last-child { border-bottom: 0; }
.contact-line svg { color: var(--pen); flex-shrink: 0; transform: translateY(3px); }
/* An email address is one long unbreakable word. On a 320px screen it sets the
   minimum width of the whole page, which zooms the document out. "anywhere"
   rather than "break-word" because only the former reduces min-content. */
.contact-line a { overflow-wrap: anywhere; min-width: 0; }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--green-deep);
  color: var(--on-deep);
  margin-top: clamp(56px, 8vh, 100px);
}

/* That margin is page background, which is the right answer when the last
   section is plain paper -- it gives the green band room to arrive. When the
   last section carries its own colour (contact.html ends on a sage wash) the
   same margin renders as a pale stripe wedged between two coloured bands, which
   is what it looked like: a white bar above the footer. The two colours should
   simply meet.

   :has() rather than a per-page class because the footer is a sibling of <main>
   and not of the section, so there is nothing to select from the section side.
   Where it is unsupported the rule is skipped and the stripe comes back, which
   is the behaviour we already had -- it degrades to the status quo, not to
   something broken. */
main:has(> section:last-child.section--wash) + .site-footer,
main:has(> section:last-child.section--deep) + .site-footer,
main:has(> section:last-child.section--warm) + .site-footer { margin-top: 0; }

.footer-cta {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: clamp(48px, 7vh, 88px) 0 clamp(40px, 5vh, 64px);
  border-bottom: 1px solid rgba(245, 244, 236, 0.14);
}
.footer-cta h2 { font-size: clamp(2rem, 4vw, 3.2rem); margin: 0; }

.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) repeat(3, minmax(0, 2fr));
  gap: var(--space-4);
  padding: var(--space-5) 0;
}

.footer-blurb {
  color: var(--on-deep-soft);
  font-size: 1rem;
  max-width: 32ch;
  margin-top: var(--space-2);
}

.footer-col h4 {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--on-deep);
  margin-bottom: var(--space-2);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li { margin-bottom: 10px; }
.footer-col a {
  font-family: var(--font-ui);
  color: var(--on-deep-soft);
  text-decoration: none;
  font-size: 0.98rem;
}
.footer-col a:hover { text-decoration: underline; text-underline-offset: 3px; color: var(--on-deep); }

.site-footer .logo { color: var(--on-deep); }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  border-top: 1px solid rgba(245, 244, 236, 0.14);
  font-family: var(--font-ui);
  font-size: 0.88rem;
  color: var(--on-deep-soft);
}

/* The Companies Act trading disclosure: registered name, number and office.
   Required on the website, not merely somewhere findable, so it goes in the
   footer and therefore onto every page.

   It takes the full row rather than sitting beside the copyright. Put on the
   same line it would push "Made in the UK" to wrap at almost every width, and
   it is a legal notice rather than a peer of those two -- quieter and smaller
   is the honest hierarchy, not a way of hiding it.

   NOT called .footer-legal: that name is already taken, by the grid of policy
   links further down this file. */
.footer-disclosure {
  flex-basis: 100%;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--on-deep-soft);
  opacity: 0.7;
}

@media (max-width: 860px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
/* The three link columns stay side by side on a phone. Stacking them turned
   the footer into about a screen and a half of scrolling to get past twelve
   links; three short columns read in one glance. The brand block above them
   keeps the full width. */
@media (max-width: 560px) {
  .footer-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-3) var(--space-2);
  }
  .footer-grid > :first-child { grid-column: 1 / -1; }
  .footer-col h4 { margin-bottom: var(--space-1); }
  .footer-col a { font-size: 0.9rem; }
}

/* --------------------------------------------------------------------------
   Reveal on scroll
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 550ms ease, transform 550ms cubic-bezier(0.25, 0.6, 0.3, 1);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js [data-reveal] { opacity: 1; transform: none; transition: none; }
  .js .circled path { animation: none; stroke-dashoffset: 0; }
  .js .process-timeline::before { animation: none; width: 75%; }
  .js .pt-tail-svg .pt-tail-solid { animation: none; opacity: 0; }
  .js .pt-tail-svg .pt-tail-dots { animation: none; opacity: 0.4; }
  .js .pt-tail-svg .pt-head { animation: none; opacity: 1; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   v4 additions — colour, photography, tracker, pricing toggle, accounts.
   Layered on top of "The schoolbook" without leaning on notebook clichés:
   coloured surfaces do the heavy lifting, photography is quiet and warm.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Coloured section surfaces — the antidote to all-white
   -------------------------------------------------------------------------- */

.section--wash { background: var(--wash); }
.section--wash + .section--wash { border-top-color: var(--wash-deep); }
.section--warm { background: var(--wash-warm); }

/* Full-bleed deep-green feature band */
.section--deep {
  background: var(--green-deep);
  color: var(--on-deep);
  border-top: 0;
}
.section--deep h1, .section--deep h2, .section--deep h3, .section--deep h4 { color: var(--on-deep); }
.section--deep .lead,
.section--deep p { color: var(--on-deep-soft); }
.section--deep .section + .section { border-top-color: rgba(245, 244, 236, 0.14); }
.section--deep em { color: #9FD4B4; font-style: italic; }

/* When a wash/deep section follows another section, drop the hairline rule —
   the colour change is the divider. */
.section.section--wash,
.section.section--warm,
.section.section--deep { border-top: 0; }

/* On coloured surfaces, white cards read as raised paper */
.section--wash .split-panel,
.section--warm .split-panel { background: var(--paper-raised); }

/* --------------------------------------------------------------------------
   Photography — quiet, warm, always safe if the file fails to load
   The image sits over a brand-coloured base, so a missing image degrades to
   an intentional green panel rather than a broken icon.
   -------------------------------------------------------------------------- */

.photo {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--green-deep);
  background-size: cover;
  background-position: center;
  min-height: 260px;
  box-shadow: 0 1px 2px rgba(32, 40, 31, 0.05), 0 22px 48px -26px rgba(32, 40, 31, 0.35);
}
.photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(190deg, rgba(22, 53, 42, 0) 40%, rgba(18, 40, 31, 0.55) 100%);
  pointer-events: none;
}
.photo--tall { min-height: 420px; }
.photo-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1;
  padding: var(--space-3);
  color: var(--on-deep);
  font-style: italic;
  font-size: 1.02rem;
}

/* Hero: copy on the left, and the thing we actually sell on the right.

   History, because this column has been argued over twice. It was a photo
   above a session-summary card; the photo was deleted outright on 1 Aug, which
   went too far, and came back above 900px on 6 Aug. On 23 Aug both went: the
   column is now the taster question, which is the first thing on the site a
   visitor can actually DO. The summary card moved down to #parent-view,
   where it sat beside copy that explained it until 5 Sep, when that band
   became a preview of the weekly report instead -- see the note above that
   section in index.html. The photo -- an apple on picture books
   next to ABC blocks -- was primary-school stock on a GCSE and A-level site
   and is not missed.

   align-items lives here rather than on .hero-inner for one specific reason:
   answering the taster reveals an explanation, which makes the right column
   taller. Centred, that re-centres the row and jerks the headline upward by
   half the growth while the visitor is reading. Top-aligned, the card grows
   downward into empty page and nothing above it moves. */
.hero-figure {
  display: flex;
  flex-direction: column;
  align-self: start;
}
.hero-inner:has(.dg-taster) { align-items: start; }

/* Phones. The taster is the point of this hero, and on a 390px screen it
   starts below the fold behind three lines of headline, four of lead, two
   buttons and the trust line. This does not fix that -- only reordering the
   copy would -- but it buys back about 50px of the chrome around it, which is
   the difference between a sliver of the card showing and none of it.
   Scoped with :has so no other page's hero moves. */
@media (max-width: 600px) {
  .hero:has(.dg-taster) { padding-top: 44px; }
  .hero-inner:has(.dg-taster) { gap: 28px; }
}

/* --------------------------------------------------------------------------
   Tight bullet list — replaces the over-spread step run-ins
   -------------------------------------------------------------------------- */

.marks {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 680px;
  display: grid;
  gap: 2px;
}
.marks li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  align-items: baseline;
  padding: 13px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 1.05rem;
}
.marks li:first-child { border-top: 1px solid var(--rule); }
.marks .lede {
  font-style: italic;
  color: var(--pen);
  font-weight: 500;
  white-space: nowrap;
}
.section--deep .marks li { border-color: rgba(245, 244, 236, 0.16); }
.section--deep .marks .lede { color: #9FD4B4; }

@media (max-width: 560px) {
  .marks li { grid-template-columns: 1fr; gap: 2px; }
}

/* --------------------------------------------------------------------------
   Process timeline — how-it-works, "One week, start to finish"
   --------------------------------------------------------------------------
   The rail is the items' own border-top, not a pseudo-element on the
   container, so it stays continuous: the columns are separated by padding-right
   rather than by `gap`, which would cut the rule into four pieces.

   On a phone the whole thing rotates. Four columns at 390px gives each step
   about 80px, which is narrower than the word "specification" -- so below 760px
   the rail becomes a vertical spine down the left and the steps stack. Same
   information, a shape that fits the screen. See [[mobile-parity-rule]].
   -------------------------------------------------------------------------- */

.process-timeline {
  list-style: none;
  margin: 36px 0 0;      /* headroom for the tail's rise -- see .pt-tail-svg--desktop */
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  position: relative;
}

.process-timeline > li {
  position: relative;
  min-width: 0;
  padding: var(--space-3) clamp(20px, 3vw, 44px) 0 0;
}

/* THE RAIL AND THE TAIL, drawn as ONE motion. Dylan, after seeing the first
   version live: the rail and the tail were visibly two separate animations
   handed off to each other -- each with its own easing, each starting from
   a dead stop -- so the line's speed visibly jumped at "every week after"
   instead of reading as one continuous ramp up, then down, to the arrow.

   First fix attempt drove both from one animated custom property so there
   would only be a single easing curve to reason about -- reverted. Tested
   against a small isolated page rather than trusted on read, and the
   browser's own reported progress at fixed points in time didn't track the
   specified cubic-bezier at all (20% complete at 1/24th of the duration
   elapsed, on a curve that should still be near 0 there). Animating a
   registered custom property clearly isn't landing the way the spec reads
   in whatever's rendering this, and that is not a foundation to ship a
   client's site on unverified.

   What's here instead needs no browser feature more exotic than the
   stroke-dashoffset trick already used on .circled. The rail runs EASE-IN --
   slow away from day one, arriving at "every week after" at its FASTEST.
   The tail then runs EASE-OUT, starting from that same top speed and
   slowing to a stop at the arrow. Two animations, but both sides of the
   handoff are moving fast, in the same direction, so the join doesn't read
   as a stop-and-restart -- one ramp up, one ramp down, met in the middle at
   speed, the way Dylan described it. The rail sits UNDER var(--rule) at
   rest, for anyone without JS or who asked for no motion, and only becomes
   the green, animated version once .js confirms a script is actually
   running the reveal. */
.process-timeline::before {
  content: "";
  position: absolute;
  top: -1px;                          /* centres the 2px bar on the node's own centreline, y=0 */
  left: 0;
  width: 75%;
  height: 2px;
  background: var(--rule);
}
.js .process-timeline::before {
  width: 0;
  background: var(--pen);
}
.js .process-timeline.is-visible::before {
  animation: pt-rail-draw 1700ms ease-in 150ms forwards;
}
@keyframes pt-rail-draw { to { width: 75%; } }

/* THE TAIL. Past "Every week after" the line keeps going -- up, not along --
   because the first three steps finish and this one does not; it is what
   every week looks like from then on. Fixed pixel size, anchored to the
   fourth node (this item's own top-left corner), so it never has to guess
   the rail's stretched width, and so the arrowhead below can be drawn at a
   fixed, correct angle rather than one that would twist as the rail
   stretches with the viewport.

   TWO overlapping copies of the same path, not one path with an animated
   dash pattern -- tried that twice and both attempts painted something
   wrong. Interpolating stroke-dasharray from a single full-length dash to a
   short repeating one shrinks the visible stroke from ONE end while the
   other stays put, so the arrowhead looked detached mid-transition. Fixing
   that by starting the repeating pattern at "dash, zero-gap" instead created
   its own fault: a zero-length gap on a CURVED stroke with round linecaps
   can render as a faintly beaded, dotted-looking line even while it's
   supposedly still solid, because each round cap along the curve doesn't
   quite line up with the next -- which is likely what actually reads as "a
   random dot" appearing before the real dotted phase even starts. Neither
   fault shows up by reading the CSS; both only showed up on a screenshot
   at the right moment.

   So: .pt-tail-solid is the ONLY path that's ever drawn in (dashoffset
   100 -> 0) and it never carries a dash pattern at all, so there is nothing
   for a browser to render unevenly. Once fully drawn it just fades out.
   .pt-tail-dots sits on the exact same geometry with a FIXED, never-animated
   dotted stroke, starts invisible, and fades in as the solid path fades out
   -- a crossfade between two static renderings, so the only thing animating
   across the whole tail is opacity, which cannot produce a stray mid-shape
   artifact the way an interpolated dash pattern can. */
.pt-tail-svg {
  position: absolute;
  overflow: visible;
  pointer-events: none;
}
.pt-tail-svg--desktop {
  left: 0;
  top: -70px;                         /* -height, so the path's own start point (local y = height) lands exactly on y=0 -- where the rail ends */
  width: 150px;
  height: 70px;
}
.pt-tail-svg--mobile { display: none; }
.pt-tail-svg .pt-tail-solid,
.pt-tail-svg .pt-tail-dots {
  fill: none;
  stroke: var(--pen);
  stroke-width: 2;
  stroke-linecap: round;
}
.pt-tail-svg .pt-tail-dots { stroke-dasharray: 4 6; opacity: 0; }
.pt-tail-svg .pt-head {
  fill: var(--pen);
  opacity: 0;
}
.js .pt-tail-svg .pt-tail-solid {
  stroke-dasharray: 100 100;
  stroke-dashoffset: 100;
}
.js .process-timeline.is-visible .pt-tail-svg .pt-tail-solid {
  animation: pt-tail-draw 700ms ease-out 1850ms forwards,
             pt-tail-solid-out 350ms ease-in 2550ms forwards;
}
.js .process-timeline.is-visible .pt-tail-svg .pt-tail-dots {
  animation: pt-tail-dots-in 350ms ease-out 2550ms forwards,
             pt-tail-dots-settle 2000ms ease-in 2900ms forwards;
}
.js .process-timeline.is-visible .pt-tail-svg .pt-head {
  animation: pt-head-in 200ms ease-out 2550ms forwards;
}
@keyframes pt-tail-draw { to { stroke-dashoffset: 0; } }
@keyframes pt-tail-solid-out { to { opacity: 0; } }
@keyframes pt-tail-dots-in { to { opacity: 1; } }
@keyframes pt-tail-dots-settle { to { opacity: 0.4; } }
@keyframes pt-head-in { to { opacity: 1; } }

/* The node on the rail, centred on the same y=0 line the rail and tail both
   use -- it used to sit 0.5px off that line, invisible on a 1px border,
   obvious once the rail became a thicker animated bar. */
.process-timeline > li::before {
  content: "";
  position: absolute;
  top: -4.5px;
  left: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--pen);
}

.process-timeline .pt-when {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.process-timeline h3 {
  margin: 6px 0 0;
  font-size: 1.15rem;
  line-height: 1.25;
}

.process-timeline p {
  margin: 8px 0 0;
  color: var(--ink-soft);
  font-size: 0.98rem;
}

@media (max-width: 760px) {
  .process-timeline { grid-template-columns: minmax(0, 1fr); }
  .process-timeline::before { display: none; }   /* the horizontal bar has no vertical equivalent -- the spine below is border-left, always full height, no zoom on the phone */

  .process-timeline > li {
    border-top: 0;
    border-left: 1px solid var(--pen);
    padding: 0 0 var(--space-3) var(--space-3);
    margin-left: 4px;
  }
  /* The spine used to STOP at the last node -- "not run past it into
     whitespace" was the note, and it was right while the rail was solid. It
     carries the same meaning as the rail now, so it keeps going past the
     LAST ITEM rather than the last node: the desktop curl reaches sideways
     into space nothing else wants, but on a 390px phone that space is the
     next line of body copy, so the mobile tail anchors to the li's own
     BOTTOM edge (`bottom`, not `top` -- the one measurement that stays
     correct without knowing how tall the stacked text ends up) and keeps
     going straight down into the padding already reserved below it, rather
     than reaching sideways into the column at all. */
  .process-timeline > li:last-child {
    padding-right: 0;
    padding-bottom: var(--space-3);
  }
  .process-timeline > li::before { top: 3px; left: -5px; }

  .pt-tail-svg--desktop { display: none; }
  /* No rail-bar phase here (the spine above is a static border, not
     animated), so there is nothing to hand off from -- the mobile tail
     just starts its own draw right away rather than waiting out the
     desktop rail's 1850ms. Same shift (-1700ms) applied to every delay
     below, in the order each element's own animation list names them. */
  .js .process-timeline.is-visible .pt-tail-svg--mobile .pt-tail-solid {
    animation-delay: 150ms, 850ms;
  }
  .js .process-timeline.is-visible .pt-tail-svg--mobile .pt-tail-dots {
    animation-delay: 850ms, 1200ms;
  }
  .js .process-timeline.is-visible .pt-tail-svg--mobile .pt-head {
    animation-delay: 850ms;
  }
  .pt-tail-svg--mobile {
    display: block;
    left: -5px;
    bottom: -34px;
    width: 20px;
    height: 34px;
  }
}

/* --------------------------------------------------------------------------
   Tracker feature — the product screenshot, drawn in CSS
   -------------------------------------------------------------------------- */

.tracker {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
}
/* minmax(0, 1fr), NOT 1fr. `1fr` is shorthand for minmax(auto, 1fr), and an auto
   minimum is the content's minimum -- so the carousel track, which is five
   slides at 100% each laid out in a row, pushed the column to five times the
   screen width instead of being clipped by its viewport. Measured at a 390px
   window: document scrollWidth 1129. The desktop rule above already uses
   minmax(0, ...) for the same reason; this one was missed.

   Same family as the min-width:auto trap noted in the mobile pass. Any flex or
   grid child that contains something wider than itself needs the floor set. */
@media (max-width: 900px) { .tracker { grid-template-columns: minmax(0, 1fr); } }

.tracker-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-ui);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--gold-ink);
  background: var(--gold-soft);
  padding: 6px 12px;
  border-radius: 100px;
  margin-bottom: var(--space-2);
}
.section--deep .tracker-badge { color: #E8CE9A; background: rgba(233, 206, 154, 0.14); }

.tracker-panel {
  background: var(--paper-raised);
  color: var(--ink);
  border-radius: 10px;
  padding: var(--space-3);
  box-shadow: 0 2px 4px rgba(0,0,0,0.06), 0 30px 60px -30px rgba(0,0,0,0.45);
}
.tracker-panel-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}
.tracker-panel-top .who { font-weight: 600; font-size: 1.1rem; }
.tracker-panel-top .term { font-style: italic; color: var(--ink-soft); font-size: 0.92rem; }

.tracker-topics { margin: var(--space-2) 0 0; }
.tracker-topic { padding: 12px 0; border-bottom: 1px dotted var(--rule); }
.tracker-topic:last-child { border-bottom: 0; }
/* The live topic breakdown drops the rule between rows: once each bar carries
   three colours, a dotted grey line between them is one more horizontal line
   competing with the thing you are meant to read. The static panels on the
   marketing pages keep theirs -- there the rule is the only thing separating
   two rows of near-identical bars. Spacing does the dividing here instead. */
.tracker-topic--split { border-bottom: 0; padding: 14px 0; }
.tracker-topic .row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font-size: 0.98rem; margin-bottom: 7px;
}
.tracker-topic .pct { font-family: var(--font-ui); font-weight: 600; color: var(--pen); font-size: 0.9rem; }
/* Rotating example panels (landing page). The panel keeps its own styling;
   this is only the frame and the controls.

   The slides sit side by side in a track that slides, rather than being shown
   and hidden. Toggling `hidden` was an instant swap with no animation at all,
   which is what read as jittery. */
/* min-width:0 on both, so the carousel can never widen its own container no
   matter what lays it out. The grid rule above is the fix; this is the floor
   that stops the same bug reappearing if the carousel is ever dropped into a
   flex row, a narrower card, or a page whose grid nobody checked. */
.tracker-carousel { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
/* The panels are white cards sitting INSIDE the dark band, and
   `.section--deep p { color: var(--on-deep-soft) }` is meant for paragraphs on
   the dark itself. Left alone it paints every paragraph in these panels -- the
   coverage line, the notes, the lesson summary -- in a colour chosen to sit on
   deep green, which on paper is almost invisible. Headings escaped it only
   because they are spans. */
.section--deep .tracker-panel p { color: var(--ink-soft); }
.section--deep .tracker-panel .summary-quote { color: var(--ink); }
/* The radius matters: the panel inside is flush with this box, and while the
   track is mid-transform Chrome composites the clip without the panel's own
   rounding, so the corners went square for the length of the slide and popped
   back at rest. Rounding the clip itself means there is nothing to lose. */
/* pan-y hands the browser the vertical axis and keeps the horizontal one for
   the swipe handler. Without it the page scroll and the drag fight each other,
   and the usual cure -- preventDefault on touchmove -- makes the whole section
   feel like it is resisting a scroll. */
.tracker-viewport { overflow: hidden; min-width: 0; border-radius: 10px; touch-action: pan-y; }
/* No `will-change: transform` here. It promotes the track to its own
   compositing layer, and for four slides that buys nothing while introducing a
   class of bug where the painted position and the actual transform disagree --
   which is what a headless capture caught it doing. */
.tracker-track {
  display: flex;
  transition: transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
  align-items: stretch;
}
/* Under prefers-reduced-motion it still advances -- it just arrives instead of
   travelling. Vestibular sensitivity is to the movement, not to the change. */
.tracker-track.is-instant { transition: none; }
.tracker-slide { flex: 0 0 100%; min-width: 0; }
/* ---- Density, carousel only -------------------------------------------------
   Every slide is as tall as the tallest, and the tallest is the topic tracker at
   540px, which made the whole band 734px -- taller than the usable height of a
   laptop window once browser chrome is taken off, so the panel could not be seen
   whole while scrolling past it.

   Measured, not guessed: slides come out 540 / 361 / 511 / 358 / 332 naturally,
   so only the topic rows and the chart are worth cutting; trimming the other
   three would change nothing at all.

   Scoped to .tracker-slide on purpose. The portal renders these same components
   inside a scrolling card with no height pressure, and there the roomier
   spacing is right -- it is a page you read, not a panel you glance at. */
.tracker-slide > .tracker-panel { padding: var(--space-2) var(--space-3); }
.tracker-slide .tracker-topic--split { padding: 9px 0; }
.tracker-slide .bar--split { height: 20px; }
.tracker-slide .tracker-coverage { margin-top: var(--space-1); }
.tracker-slide .tracker-key { margin-top: var(--space-1); }
/* Every slide is as tall as the tallest, so the viewport does not jump height
   mid-slide. The cost is dead space at the bottom of the shorter ones, so the
   panel is a column and its closing note is pushed to the foot rather than left
   hanging under the content with a void beneath it. */
.tracker-slide > .tracker-panel { height: 100%; display: flex; flex-direction: column; }
.tracker-slide > .tracker-panel > :last-child { margin-bottom: 0; }
.tracker-slide > .tracker-panel > .note:last-child { margin-top: auto; padding-top: var(--space-2); }
/* Four figures across is a squeeze that wraps to three and one, which reads as
   a mistake. Two by two is the same information, evenly.

   The top margin is not decoration. .tracker-panel-top ends in a rule, and these
   figures are set large, so with no gap the rule sits directly on top of the
   first numeral and reads as underlining it rather than dividing the panel. The
   other panels get their air from the leading of ordinary text; this one has
   none to borrow. */
.tracker-panel .stat-row {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3) var(--space-2);
  margin-top: var(--space-3);
}

/* One line, always. It is a caption, not a paragraph, and wrapping it onto two
   lines on a phone pushed the bars down and made the panel feel taller than it
   is. */
.tracker-coverage {
  margin: var(--space-2) 0 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Legend, since the words came out of the rows. Once per panel instead of once
   per topic is the whole saving. */
.tracker-key {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  margin: var(--space-2) 0 0; padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui); font-size: 0.76rem; color: var(--ink-soft);
}
.tracker-key .k { display: inline-flex; align-items: center; gap: 6px; }
.tracker-key .k::before {
  content: ""; width: 10px; height: 10px; border-radius: 2px; background: var(--ink-soft);
}
.tracker-key .k--s::before { background: #2E7D55; }
.tracker-key .k--a::before { background: #D98A1F; }
.tracker-key .k--w::before { background: #C0402B; }
.tracker-key .k--o::before { background: var(--wash-deep); border: 1px solid var(--rule); }

/* ---- Hero carousel panel 2: "Your child at a glance" as the whole portal --
   Dylan, 5 Sep, third pass. Tried a real screenshot of dashboard.html; it was
   truthful but a real desktop-width screenshot is much wider than tall, and
   forcing it to fill this tall narrow panel meant either a lot of empty
   space or cropping real content off (both tried, neither liked). Back to a
   hand-built mock, this time closer to the real thing: the same nav icons
   the real sidebar uses (copied from the body:has(main[data-portal]) block
   above -- same paths, same viewBox, so it is the same icon, not a redrawn
   one), and content sized to actually fill the panel instead of leaving a
   gap at the bottom. */
.portal-mock {
  flex: 1;
  display: flex;
  min-height: 0;
  /* Bleeds out to the edges of .tracker-panel, cancelling its own padding
     (.tracker-slide > .tracker-panel: var(--space-2) var(--space-3)) so the
     rail can run flush top-to-bottom and reach the panel's own rounded
     corners rather than floating inside them with a margin. */
  margin: calc(-1 * var(--space-2)) calc(-1 * var(--space-3));
  border-radius: 10px;
  overflow: hidden;
}
.portal-mock-rail {
  flex: 0 0 116px;
  background: var(--green-deep);
  padding: 20px 10px;
  display: flex;
  flex-direction: column;
}
.portal-mock-logo {
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--on-deep);
  padding: 0 4px;
  margin-bottom: 24px;
}
.portal-mock-logo .logo-dot { color: #9FD4B4; }
.portal-mock-nav { display: flex; flex-direction: column; gap: 2px; }
.portal-mock-nav span, .portal-mock-logout {
  display: flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--on-deep-soft);
  padding: 8px 8px;
  border-radius: 6px;
}
.portal-mock-nav span.is-active { background: rgba(245, 244, 236, 0.13); color: var(--on-deep); }
.pm-icon {
  width: 15px; height: 15px; flex: none;
  background-color: currentColor;
  -webkit-mask: var(--pm-icon) center / contain no-repeat;
          mask: var(--pm-icon) center / contain no-repeat;
}
/* Same paths as the real body:has(main[data-portal]) .site-nav icons further
   down this file -- copied, not reinvented, so a redrawn icon here can never
   drift from the real one. */
.pm-icon--dashboard { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 10.5 12 3l9 7.5'/%3E%3Cpath d='M5 9.5V21h14V9.5'/%3E%3C/svg%3E"); }
.pm-icon--progress { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 3v18h18'/%3E%3Cpath d='m7 15 4-4 3 3 5-6'/%3E%3C/svg%3E"); }
.pm-icon--lessons { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4.5A1.5 1.5 0 0 1 5.5 3H19v15H5.5A1.5 1.5 0 0 0 4 19.5Z'/%3E%3Cpath d='M4 19.5A1.5 1.5 0 0 1 5.5 21H19'/%3E%3C/svg%3E"); }
.pm-icon--messages { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-4.5 4v-4A1.5 1.5 0 0 1 4 14.5Z'/%3E%3C/svg%3E"); }
.pm-icon--payments { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 3v18h18'/%3E%3Cpath d='M7 15v3M12 10v8M17 6v12'/%3E%3C/svg%3E"); }
.pm-icon--logout { --pm-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 21H5.5A1.5 1.5 0 0 1 4 19.5v-15A1.5 1.5 0 0 1 5.5 3H10'/%3E%3Cpath d='m15.5 8 4 4-4 4M19.5 12H9'/%3E%3C/svg%3E"); }
.portal-mock-logout { margin-top: auto; }
.portal-mock-main {
  flex: 1;
  min-width: 0;
  padding: 12px var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* Dylan, 5 Sep, fifth pass: fitting the next-lesson block and three doorway
   cards back in made the whole panel's OWN natural content height (measured
   with the carousel's stretch briefly disabled) taller than the topic-
   tracker slide -- 574px against its 462px -- and since every slide
   stretches to match the tallest (see "Density, carousel only" above), that
   pushed the whole hero taller. Everything below is compressed to fit back
   under that 462px budget rather than becoming the new tallest slide. */
.portal-mock .tracker-panel-top { padding-bottom: 8px; }
.portal-mock .stat-row { margin-top: 4px; }
.portal-mock .stat .n { font-size: 1.3rem; }
.portal-mock .stat .k { font-size: 0.7rem; margin-top: 1px; }

.portal-mock-next {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
}
.portal-mock-next .cal {
  flex: none;
  width: 30px; height: 30px;
  border-radius: 5px;
  background: var(--green-deep);
  color: var(--on-deep);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 0.58rem;
  font-weight: 700;
  line-height: 1.1;
}
.portal-mock-next strong { display: block; font-family: var(--font-serif); font-size: 0.82rem; font-weight: 600; }
.portal-mock-next .note { font-family: var(--font-ui); font-size: 0.7rem; color: var(--ink-soft); margin: 0; }

.portal-mock-links {
  margin-top: auto;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.portal-mock-links .pm-link-card {
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 5px 8px;
}
.portal-mock-links strong {
  display: block;
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: 0.76rem;
  color: var(--pen);
}
.portal-mock-links p {
  font-family: var(--font-ui);
  font-size: 0.64rem;
  color: var(--ink-soft);
  margin: 0;
  line-height: 1.25;
}
@media (max-width: 480px) {
  .portal-mock-rail { flex-basis: 92px; padding: 16px 7px; }
  .portal-mock-nav span, .portal-mock-logout { font-size: 0.7rem; padding: 7px 5px; gap: 6px; }
  .pm-icon { width: 13px; height: 13px; }
}

/* ---------------------------------------------------------------------------
   REMOVED: .report-sheet / .rep-tiles / .rep-tile / .rep-caveat / .rep-sec /
   .rep-loss -- a hand-written approximation of the weekly report in the site's
   own type.

   It was a good-looking document and it was the wrong one. Nobody is ever sent
   it: the engine emits its own markup with its own stylesheet, and a marketing
   page showing a tidied-up version is advertising something that does not
   exist. The example reports now use the engine's real markup and its real CSS
   (see the block at the end of this file). If you need to style a report,
   you are in the wrong repo.
   --------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   The floating example report — opened by the "View" links on the marketing
   pages. Markup is built by initReportDemos() in js/main.js.
   --------------------------------------------------------------------------
   The sheet inside is the same .report-sheet used on tracker-example.html, so
   there is nothing to restyle here: this is a container, a backdrop and a way
   out. Scrolling belongs to .report-modal-scroll rather than the page, and the
   root gets .report-modal-open so the page behind cannot scroll underneath a
   phone's momentum -- the standard iOS complaint about overlays.
   -------------------------------------------------------------------------- */

.report-modal {
  position: fixed;
  inset: 0;
  z-index: 200;                 /* above the sticky header, which sits at 100 */
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: clamp(12px, 4vh, 48px) clamp(12px, 4vw, 32px);
}

.report-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(26, 32, 28, 0.55);
  backdrop-filter: blur(2px);
}

.report-modal-panel {
  position: relative;
  /* The report sets its own max-width: 840px. The panel is that plus the room
     the document's own horizontal padding needs, so it is never the thing
     deciding how wide a report is. */
  width: min(884px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--paper);
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(26, 32, 28, 0.35);
  animation: reportModalIn 220ms ease-out both;
}

@keyframes reportModalIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .report-modal-panel { animation: none; }
}

.report-modal-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 0;
  min-width: 0;
}

/* No padding above: the report brings its own (.rep sets 22px 20px 52px) and
   two lots would set the document in from its container twice. The only change
   is headroom for the close button, which would otherwise sit on the title. */
.report-modal-scroll .rep { padding-top: 46px; }

.report-modal-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--paper-raised);
  color: var(--ink-soft);
  cursor: pointer;
}
.report-modal-close:hover { color: var(--ink); border-color: var(--rule-strong); }

.report-modal-foot {
  margin: 0 20px var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.report-modal-open,
.report-modal-open body { overflow: hidden; }

@media (max-width: 560px) {
  .report-modal { padding: 0; }
  /* Full-bleed on a phone. A 12px margin round a document this long buys
     nothing and costs a line of text per paragraph. */
  .report-modal-panel { width: 100%; height: 100%; max-height: 100%; border-radius: 0; }
  .report-modal-scroll .rep { padding-top: 52px; }
}

/* The past-paper panel. The chart is inline SVG so it carries no weight and
   inherits the page's own colours; it scales to the panel and needs no rules of
   its own beyond room to breathe. */
/* max-width, because the viewBox is only 440 wide. Inside the carousel panel
   (~500px) it never binds; inside the full-width report sheet the same SVG
   scaled to 2.2x and rendered its 11px axis text at 24px, which read as a
   different, much louder chart. Capping it keeps one chart at one size. */
.paper-chart { display: block; margin: var(--space-2) 0 var(--space-1); overflow: visible; max-width: 460px; }
.paper-move { font-size: 0.95rem; margin: 0 0 var(--space-1); }

/* The written-summary panel. Colour is inherited from .tracker-panel rather
   than set again here -- inside .section--deep the ink tokens are redefined for
   light-on-dark, and naming one explicitly picked up the wrong end of that and
   rendered the quote washed out against its own heading. */
.summary-quote {
  font-family: var(--font-serif); font-size: 1.02rem; line-height: 1.55;
  margin: var(--space-2) 0 var(--space-1);
}
.summary-by { font-family: var(--font-ui); font-size: 0.82rem; color: var(--ink-soft); margin: 0; }
/* Carries .next-lesson too, so the calendar block is the one the dashboard
   already uses; this only adds the rule above it. */
.next-up { margin-top: auto; padding-top: var(--space-3); border-top: 1px solid var(--rule); }
/* Marks the panel as an illustration rather than a real child's record. Quiet
   on purpose -- it has to be legible without competing with the data. */
/* .report-sheet-top was missing from this selector, so the marker rendered as
   the bare word "Example" run into the report's title -- on tracker-example.html
   and in every sheet the modal builds. It is the same pill in both places. */
.tracker-panel-top .eg,
.report-sheet-top .eg {
  font-family: var(--font-ui); font-weight: 600; font-size: 0.68rem;
  letter-spacing: 0.04em; text-transform: none;
  color: var(--ink-soft); border: 1px solid var(--rule);
  border-radius: 100px; padding: 1px 7px; margin-left: 6px; white-space: nowrap;
}
.carousel-controls { display: flex; align-items: center; justify-content: center; gap: var(--space-2); }
.carousel-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: transparent; color: #9FD4B4;
  border: 1px solid rgba(159, 212, 180, 0.4); border-radius: 100px;
  cursor: pointer;
}
.carousel-arrow:hover { background: rgba(159, 212, 180, 0.12); border-color: #9FD4B4; }
.carousel-dots { display: flex; align-items: center; gap: 8px; }
.carousel-dot {
  width: 8px; height: 8px; padding: 0; border-radius: 100px;
  background: rgba(159, 212, 180, 0.35); border: 0; cursor: pointer;
}
.carousel-dot.is-on { background: #9FD4B4; }
/* A dot is 8px wide, which is far below the 24px minimum a finger needs. Grow
   the hit area without growing the mark. */
.carousel-dot::before { content: ""; position: absolute; inset: -8px; }
.carousel-dot { position: relative; }

/* This used to stack the label under the topic name below 640px, because the
   label was a sentence: "20 of 25 covered (80%) - 12 strong (60%), 6 average
   (30%), 2 weak (10%)". The rework cut it to "20 of 25", and the stacking
   outlived the reason for it -- every topic became three lines on a phone (name,
   count, bar) and the panel read as stretched and empty.

   One line now, name on the left and count on the right, as on a laptop. The
   name is the only part allowed to shrink or wrap; the count never does, so the
   right-hand column stays a straight edge to read down. */
@media (max-width: 640px) {
  .tracker-topic--split .row { align-items: flex-start; gap: 10px; }
  .tracker-topic--split .row > span:first-child { min-width: 0; }
  .tracker-topic--split .pct { font-size: 0.82rem; flex: 0 0 auto; white-space: nowrap; }
  /* A little tighter again on a phone: the bars are the content, and the gaps
     between them were sized for a panel three times this wide. */
  .tracker-slide .tracker-topic--split { padding: 7px 0; }
  .tracker-slide > .tracker-panel { padding: var(--space-2); }
}

.bar { height: 7px; background: var(--wash-deep); border-radius: 100px; overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--pen-bright); border-radius: 100px; }

/* A topic bar: filled to how much of the topic has been covered, divided by how
   it went. The track behind it is everything nobody has taught yet. Segments are
   square-ended so they read as one bar rather than three pills; the container's
   overflow rounds the outer ends.

   The selectors below must stay at `.bar--split > .seg--x`. Written as a bare
   `.seg--s` they lose to `.bar > span` above, which is one class plus one
   element, and every segment paints --pen-bright: the bar is the right LENGTH
   and entirely one colour, which is exactly how it shipped on 6 Aug.

   9px rather than 7: three colours in a 7px bar are hard to tell apart, and the
   whole point of the split is that you can read it at a glance. */
/* Thick enough for each band to carry its own figure, which is where the
   strong / average / weak numbers now live. A 9px bar with a sentence above it
   was the busiest thing on the page; this says the same in one object.
   The pill radius goes with it -- at 22px tall it bows the first and last
   bands and makes equal shares look unequal. */
/* overflow is visible, NOT hidden -- a narrow band's figure has to be able to
   escape its own width (see .seg--tight). The outer ends are rounded by the
   first and last segment instead of by clipping the container, which is what
   .bar's overflow:hidden used to do for us. */
.bar--split { display: flex; height: 22px; border-radius: 5px; overflow: visible; }
.bar--split > .seg {
  display: flex; align-items: center; justify-content: center;
  height: 100%; border-radius: 0; min-width: 0; overflow: hidden;
}
.bar--split > .seg:first-child { border-radius: 5px 0 0 5px; }
.bar--split > .seg:last-child { border-radius: 0 5px 5px 0; }
.bar--split > .seg > i {
  font-family: var(--font-ui); font-style: normal; font-weight: 600;
  font-size: 0.7rem; letter-spacing: 0.01em; line-height: 1;
  color: #FFFFFF; white-space: nowrap;
}
.bar--split > .seg--s { background: #2E7D55; }   /* strong  — green  */
.bar--split > .seg--a { background: #D98A1F; }   /* average — orange */
.bar--split > .seg--w { background: #C0402B; }   /* weak    — red    */
.bar--split > .seg--o { background: var(--ink-soft); }   /* covered, no rating on it */
/* White on the orange is about 2.4:1 — legible as decoration, not as text.
   Ink on it is over 7:1 and is the only one of the four that needs it. */
.bar--split > .seg--a > i { color: var(--ink); }

/* A band too narrow to hold its own figure. The ONLY difference from a wide one
   is that the clip comes off, so the number can overhang its neighbours instead
   of being cut mid-character. Everything else -- size, weight, colour -- is
   inherited from the rules above, deliberately: a narrow band styled differently
   (this had a dark chip on it until 16 Aug) reads as a different kind of thing
   rather than the same measurement in a smaller space. Dylan's call.

   No contrast problem to solve here. The white-on-orange case is already
   handled by `.seg--a > i`, which paints ink instead, and that rule still wins
   because nothing below re-declares the colour. */
.bar--split > .seg--tight { overflow: visible; }
.bar--split > .seg--tight > i { position: relative; z-index: 1; }

/* --------------------------------------------------------------------------
   Pricing — billing toggle + animated figures
   -------------------------------------------------------------------------- */

.bill-toggle {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  margin: 0 auto var(--space-4);
  padding: 7px;
  background: var(--wash);
  border: 1px solid var(--rule);
  border-radius: 100px;
  font-family: var(--font-ui);
}
.bill-toggle-wrap { display: flex; justify-content: center; }
.bill-opt {
  border: 0;
  background: transparent;
  font-family: var(--font-ui);
  font-size: 0.96rem;
  font-weight: 600;
  color: var(--ink-soft);
  padding: 9px 20px;
  border-radius: 100px;
  cursor: pointer;
  transition: color 180ms ease, background-color 180ms ease, box-shadow 180ms ease;
  white-space: nowrap;
}
.bill-opt[aria-pressed="true"] {
  color: var(--green-deep);
  background: var(--paper-raised);
  box-shadow: 0 1px 2px rgba(32,40,31,0.06), 0 6px 16px -10px rgba(32,40,31,0.3);
}
.bill-save {
  font-style: italic;
  color: var(--pen);
  font-family: var(--font-serif);
  font-weight: 400;
}

/* A second price on the same card. Was a footnote under the whole grid, which
   put the number furthest from the card it belongs to; Dylan asked for it on
   the band. min-height reserves the line on the two cards that have no
   sub-price, so all three blurbs still start at the same height. */
.price-amount .pack-note {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.86rem;
  font-weight: 500;
  font-style: normal;
  color: var(--pen);
  margin-top: 8px;
  min-height: 1.2em;
}
.price-card.featured .pack-note { color: #9FD4B4; }

.price-flag--pop {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--gold);
  font-style: normal;
  font-family: var(--font-ui);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.price-card.featured .price-flag--pop { color: #E8CE9A; }

/* --------------------------------------------------------------------------
   Auth pages (login / sign-up / tutor apply) — a quiet split screen
   -------------------------------------------------------------------------- */

.auth {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-height: calc(100vh - var(--header-h));
}
@media (max-width: 900px) { .auth { grid-template-columns: 1fr; } }

.auth-aside {
  background: var(--green-deep);
  color: var(--on-deep);
  padding: clamp(40px, 6vw, 80px);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  overflow: hidden;
}
/* ⚠️ --aside-photo must be written as url(../images/x.jpg), NOT url(images/x.jpg)
   and NOT url(/images/x.jpg).

   The pages declare it in a style attribute, but a relative url() inside a
   custom property is resolved against the stylesheet that CONSUMES it -- this
   file, in css/ -- not the page that declared it. So `images/x.jpg` computes to
   css/images/x.jpg, which exists nowhere, and the panel silently renders flat
   green with no error anywhere.

   An absolute /images/x.jpg is right on the deployed site and resolves to
   C:\images\ when the page is opened from disk, which is how this was found.
   ../images/x.jpg is correct in both.

   role-select.html does the same trick with --door-photo, but its rule lives in
   an inline <style>, so the base there IS the document and plain images/x.jpg
   is correct. Check which stylesheet consumes the variable before copying
   either one. */
.auth-aside::before {
  content: "";
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0.18;
  background-image: var(--aside-photo, none);
}
.auth-aside > * { position: relative; z-index: 1; }
.auth-aside h2 { color: var(--on-deep); max-width: 16ch; }
.auth-aside .lead { color: var(--on-deep-soft); }
.auth-aside .logo { color: var(--on-deep); font-size: 1.6rem; }
.auth-aside-points { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.auth-aside-points li {
  display: flex; gap: 12px; align-items: baseline;
  padding: 11px 0; border-bottom: 1px solid rgba(245,244,236,0.14);
  color: var(--on-deep); font-size: 1.02rem;
}
.auth-aside-points svg { color: #9FD4B4; flex-shrink: 0; transform: translateY(2px); }

.auth-main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(40px, 6vw, 80px);
  background: var(--paper);
}
/* The aside used to be display:none below 900px. That removed the only
   reassurance on the page -- "Free until your first lesson", "Only pay for
   sessions you book", "No contracts. Cancel any time." -- at the highest-
   friction step, on the device most parents use. It also removed the logo,
   which lives inside this panel, leaving a phone visitor on a bare password
   form with no way back to the site.
   It is now a compact band above the form rather than a half-screen panel:
   same words, a third of the height, and the form still starts near the top. */
@media (max-width: 900px) {
  .auth-aside {
    padding: var(--space-2) var(--space-3);
    gap: var(--space-1);
    min-height: 0;
  }
  /* The site header carries a logo of its own, so stacked this one is the
     second Tutora wordmark in 200px. Drop it and give the height to the words
     that do work. */
  .auth-aside .logo { display: none; }
  .auth-aside h2 { font-size: 1.16rem; line-height: 1.28; max-width: none; }
  .auth-aside-points { margin-top: 10px; }
  .auth-aside-points li { padding: 5px 0; font-size: 0.9rem; gap: 9px; border-bottom: 0; }
  .auth-aside-points svg { width: 15px; height: 15px; }
  .auth-aside .note { margin-top: 6px; font-size: 0.86rem; }
  /* Stacked, the full-height grid and the centred column fight each other: the
     form row stretches to fill the viewport and pushes the button down. Let the
     content set its own height on a phone. */
  .auth { min-height: 0; }
  .auth-main { padding: var(--space-3); justify-content: flex-start; }
}
.auth-form { width: 100%; max-width: 420px; margin-inline: auto; }
.auth-form h1 { font-size: clamp(2rem, 4vw, 2.8rem); margin-bottom: var(--space-1); }
.auth-form .sub { color: var(--ink-soft); margin-bottom: var(--space-3); }
.auth-alt { margin-top: var(--space-3); font-family: var(--font-ui); font-size: 0.95rem; color: var(--ink-soft); }
.auth-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.remember { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-ui); font-size: 0.92rem; color: var(--ink-soft); }
.remember input { width: 16px; height: 16px; accent-color: var(--pen); }

/* An unticked REQUIRED box (the terms agreement on signup). The red-border rule
   further up only reaches `.field input[aria-invalid]`, and this box is not in a
   .field -- so before this it was flagged for screen readers and invisible to
   everyone else. align-items is start rather than center here because the label
   wraps to two lines once it turns into a block with a ring around it. */
.remember.is-invalid { color: var(--error); align-items: flex-start; }
.remember.is-invalid input {
  accent-color: var(--error);
  outline: 2px solid var(--error);
  outline-offset: 2px;
  flex: 0 0 auto;
}
.remember.is-invalid a { color: var(--error); }
.auth-divider {
  display: flex; align-items: center; gap: 14px;
  margin: var(--space-3) 0;
  font-family: var(--font-ui); font-size: 0.85rem; color: var(--ink-soft);
}

/* Forgot password — a quiet inline reveal under the login row */
.forgot-box {
  margin-top: var(--space-2);
  padding: var(--space-2);
  background: var(--wash);
  border-radius: 8px;
}
.forgot-box label {
  display: block;
  font-family: var(--font-ui); font-size: 0.92rem; font-weight: 600;
  margin-bottom: 8px;
}
.forgot-row { display: flex; gap: var(--space-1); }
.forgot-row input { flex: 1; min-width: 0; }
.forgot-row .btn { flex-shrink: 0; padding: 13px 18px; }
.forgot-box .note { margin: 8px 0 0; }
.auth-divider::before, .auth-divider::after { content: ""; height: 1px; flex: 1; background: var(--rule); }

/* --------------------------------------------------------------------------
   Parent dashboard
   -------------------------------------------------------------------------- */

.dash { background: var(--wash); min-height: calc(100vh - var(--header-h)); }

/* THE CONSULTATION GATE. Dylan, 5 Sep, revised after seeing the first version
   live: it must NOT take over the whole dashboard when only ONE subject
   needs it -- a family with Maths already tutored and a just-added Chemistry
   subject has to be able to switch back to Maths and see it working. So this
   replaces only the CONTENT -- .dash-grid, the cards below the welcome
   message -- for whichever child is currently selected; .dash-head (the
   welcome line, the child switcher, the subject switcher) stays exactly as
   it is, because switching away from the subject that needs booking is
   supposed to work, not be trapped behind this. No close button on the gate
   itself, still deliberate -- "make sure it's very clear they have to book
   one" -- but the switcher IS the escape hatch now, not a missing one.

   Sits in normal document flow as a sibling of .dash-grid (see dashboard.html)
   rather than positioned over it -- simpler than a fixed overlay, and there
   is nothing left for it to need to sit above or beside once it no longer
   spans the whole viewport. js/main.js toggles [hidden] on this and on
   .dash-grid oppositely: exactly one of the two is ever visible for a given
   selected child. */
.consult-gate {
  display: flex;
  justify-content: center;
  padding: clamp(40px, 8vh, 88px) var(--space-3);
}
.consult-gate-panel {
  max-width: 46ch;
  text-align: center;
  background: var(--paper-raised);
  border-radius: 12px;
  padding: clamp(28px, 5vw, 48px);
  box-shadow: 0 2px 4px rgba(0,0,0,0.06), 0 30px 60px -30px rgba(0,0,0,0.45);
}
.consult-gate-panel h2 { margin-bottom: var(--space-2); }
.consult-gate-panel p { color: var(--ink-soft); }
.consult-gate-panel .btn { margin-top: var(--space-3); }

.dash-head {
  background: var(--green-deep);
  color: var(--on-deep);
  padding: clamp(28px, 4vw, 44px) 0;
}
.dash-head h1 { color: var(--on-deep); font-size: clamp(1.8rem, 3.4vw, 2.6rem); margin-bottom: 6px; }
.dash-head .lead { color: var(--on-deep-soft); margin: 0; }
.dash-head-inner { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; }

/* WHICH CHILD sits on its own line, hard right, above the subject row and the
   buttons. Dylan, 6 Sep: "make the child switch in a diff place (maybe top
   right), and then subjects where they are currently."

   They are two different questions and they were sharing one wrapping row, so
   which pill meant "person" and which meant "course" was decided by wherever
   the line happened to break. Giving the child row its own full-width line
   inside .dash-head-actions keeps that from ever depending on the text. */
.dash-head-who { flex-basis: 100%; display: flex; justify-content: flex-end; }
/* Scoped with :has() so the ~20 other pages using .dash-head-actions -- admin,
   tutor, billing -- keep the alignment they already had. Only the three parent
   pages carry a .dash-head-who. */
.dash-head-actions:has(.dash-head-who) { justify-content: flex-end; }

.child-switch { display: inline-flex; gap: 8px; background: rgba(245,244,236,0.1); padding: 6px; border-radius: 100px; }
.child-switch button {
  border: 1px solid transparent; background: transparent; cursor: pointer;
  font-family: var(--font-ui); font-weight: 600; font-size: 0.92rem;
  color: var(--on-deep-soft); padding: 8px 16px; border-radius: 100px;
  max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The one NOT chosen needs an edge of its own. Dylan, 6 Sep: "make the
   unclicked pill have an outline around it, so it is more obvious." Filled
   versus nothing-at-all reads as one button and some text beside it; filled
   versus outlined reads as two buttons, one of them pressed. */
.child-switch button[aria-pressed="false"] { border-color: rgba(245,244,236,0.34); }
.child-switch button[aria-pressed="true"] { background: var(--on-deep); color: var(--green-deep); }

/* The subject row, under the child switcher.

   Deliberately QUIETER than the row above it: outlined rather than filled, and
   a size down. They are two different questions -- which child, then which of
   their courses -- and giving them the same weight makes the header read as one
   long undifferentiated row of pills.

   Same on-deep palette, because .dash-head is still var(--green-deep) here. If
   this header is ever turned to paper, every colour below has to be re-picked:
   see what happened to the portal pages on 2 Sep, where nothing errored and the
   pages photographed fine while 27 elements became unreadable. */
.subject-switch {
  display: inline-flex; gap: 6px; flex-wrap: wrap;
  align-items: center;
}
.subject-switch button {
  cursor: pointer;
  font-family: var(--font-ui); font-weight: 600; font-size: 0.82rem;
  color: var(--on-deep-soft);
  background: transparent;
  border: 1px solid rgba(245,244,236,0.32);
  padding: 5px 12px; border-radius: 100px;
  /* A University-prep row's subject is the family's own sentence. The label is
     already shortened by courseLabel() in js/main.js; this is the backstop for
     a long typed subject, so one course can never stretch the header. */
  max-width: 30ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.subject-switch button[aria-pressed="true"] {
  background: var(--on-deep); color: var(--green-deep);
  border-color: var(--on-deep);
}
/* How many subjects this student takes, on their name button. A bare digit
   would read as a notification count; the parenthetical says it is a total.
   The brackets are written into the markup rather than added here as
   ::before/::after -- pseudo-content is not in the accessibility tree, so as
   CSS the button announced itself as "Amara 2". */
.switch-count {
  /* 0.85, not 0.75. At 0.75 this measured 4.52:1 against the deep green, which
     passes AA by 0.02 -- a margin that any future palette nudge erases without
     anything going visibly wrong. 0.85 measures 5.37:1 and still reads as
     secondary to the name beside it.
     Measured, not eyeballed: the contrast sweep runs under an admin stub and
     never renders the parent switcher, so it cannot see these two rules. */
  font-weight: 500; opacity: 0.85;
}
/* A subject the family has asked for but not yet had the call about. */
.switch-flag {
  font-weight: 500; font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.04em; opacity: 0.8;
  margin-left: 4px;
}

/* No children yet: drop the pill, and use light text — the header is deep green */
.child-switch--empty { background: transparent; padding: 0; }
.child-switch--empty .switch-empty {
  font-family: var(--font-ui); font-size: 0.95rem;
  color: var(--on-deep-soft); margin: 0;
}

.dash-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-3);
  padding: var(--space-4) 0 var(--space-5);
}
/* Grid and flex items default to min-width:auto, which resolves to min-content
   -- so one wide table makes the card refuse to shrink, the layout viewport
   expands past device-width, and the whole page renders zoomed out on a phone.
   These let the scroll containers inside actually do their job. */
.dash-grid > * { min-width: 0; }
.card {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: var(--space-3);
  min-width: 0;
}
.card h3 { font-size: 1.15rem; margin-bottom: var(--space-2); }
.card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-2); }
.card-head h3 { margin: 0; }
.card-head-tools { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }

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

/* ---- Messaging ------------------------------------------------------------ */

/* (a) Full-screen chat app — the dedicated Messages page */
.msg-app { background: var(--wash); }
.msg-app-inner {
  display: flex; flex-direction: column;
  height: calc(100dvh - 150px); min-height: 420px;
  padding-top: var(--space-3); padding-bottom: var(--space-3);
}
.msg-bar { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); flex-wrap: wrap; }
.msg-bar .field { margin: 0; min-width: 260px; }
.msg-bar-title { font-family: var(--font-ui); font-weight: 600; color: var(--ink); }

/* --------------------------------------------------------------------------
   Conversation list — replaced the single <select> on 16 Aug 2026
   -------------------------------------------------------------------------- */

/* Two columns on a desktop, one at a time on a phone (see the breakpoint at the
   end of this block). The list is a fixed 20rem rather than a fraction: it holds
   names and one line of preview, and there is nothing for it to do with more. */
.msg-layout {
  flex: 1 1 auto; min-height: 0;
  display: grid; grid-template-columns: 20rem minmax(0, 1fr);
  gap: var(--space-3);
}
.msg-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; }

.conv-pane {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--paper); border: 1px solid var(--rule); border-radius: 10px;
  overflow: hidden;
}
.conv-pane-head { padding: var(--space-2); border-bottom: 1px solid var(--rule); }
.conv-pane-title {
  margin: 0 0 8px; font-family: var(--font-ui); font-size: 0.78rem;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft);
}
.conv-pane-head .list-search { width: 100%; }

.conv-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.conv-empty { padding: var(--space-2); }

.conv-item {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 10px var(--space-2);
  background: none; border: 0; border-bottom: 1px solid var(--rule);
  font-family: var(--font-ui);
}
.conv-item:hover { background: var(--wash); }
.conv-item.is-open { background: var(--wash-deep); }
/* The open one is marked with a rule down its edge, not just a fill — the fill
   alone is easy to lose against the hover state. */
.conv-item.is-open { box-shadow: inset 3px 0 0 var(--pen); }

.conv-row { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.conv-row + .conv-row { margin-top: 3px; }
.conv-name {
  font-size: 0.92rem; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.conv-when { font-size: 0.74rem; color: var(--ink-soft); flex: 0 0 auto; }
/* "Tutor of Amara" under the name. Its own line so the NAME is never the half
   that gets cut -- which is what happened when the two were one string on a
   phone, and the name is how you tell three tutors apart. */
.conv-sub {
  display: block; margin-top: 1px;
  font-size: 0.74rem; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.conv-preview {
  font-size: 0.82rem; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Unread: the name goes solid, the preview stops being grey. Weight and colour
   carry it, so it does not depend on the badge alone being noticed. */
.conv-item.is-unread .conv-name { font-weight: 800; }
.conv-item.is-unread .conv-preview { color: var(--ink); font-weight: 600; }

/* The red circle. Used twice: on a conversation row, and on the Messages nav
   link. Same object both times on purpose -- they are counting the same thing. */
.badge-count {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px;
  background: var(--error); color: #FFFFFF;
  font-family: var(--font-ui); font-size: 0.68rem; font-weight: 700;
  line-height: 1; font-variant-numeric: tabular-nums;
}
/* On the nav it rides alongside the word rather than inside the text flow. */
.site-nav a .badge-count { margin-left: 6px; vertical-align: middle; }

.msg-back {
  display: none; cursor: pointer;
  background: none; border: 0; padding: 0;
  font-family: var(--font-ui); font-size: 0.88rem; color: var(--pen);
}

/* One pane at a time. Two 10rem columns at 390px would give the thread no room
   to be a thread, so the list IS the page until you pick something. */
@media (max-width: 780px) {
  .msg-layout { grid-template-columns: minmax(0, 1fr); }
  .msg-layout .msg-pane { display: none; }
  .msg-layout.is-viewing .conv-pane { display: none; }
  .msg-layout.is-viewing .msg-pane { display: flex; }
  .msg-back { display: inline-block; }
}

/* Chat bubbles — sent right, received left, in slightly different shades */
.msg-app .msg-thread {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 4px;
  padding: var(--space-3);
  background: var(--paper); border: 1px solid var(--rule); border-radius: 10px;
}
.msg-row { display: flex; flex-direction: column; max-width: 66%; margin-top: var(--space-1); }
.msg-row--mine { align-self: flex-end; align-items: flex-end; }
.msg-row--them { align-self: flex-start; align-items: flex-start; }
.msg-bubble {
  padding: 8px 13px; border-radius: 15px;
  font-family: var(--font-ui); font-size: 0.92rem; line-height: 1.42;
  word-wrap: break-word; overflow-wrap: anywhere;
}
.msg-row--them .msg-bubble { background: var(--wash); color: var(--ink); border-bottom-left-radius: 5px; }
.msg-row--mine .msg-bubble { background: #DCEBE0; color: var(--ink); border-bottom-right-radius: 5px; }
.msg-meta { font-family: var(--font-ui); font-size: 0.72rem; color: var(--ink-soft); margin: 3px 5px 0; }
.msg-empty-state { color: var(--ink-soft); margin: auto; text-align: center; }

/* Attachments inside a bubble */
.msg-attach { display: inline-flex; align-items: center; gap: 7px; margin-top: 5px;
  font-family: var(--font-ui); font-size: 0.86rem; }
.msg-attach img { max-width: 220px; max-height: 220px; border-radius: 8px; display: block; }

/* Composer pinned under the thread */
.msg-compose { display: flex; gap: var(--space-2); align-items: flex-end; margin-top: var(--space-2); }
.msg-compose textarea {
  flex: 1; min-height: 48px; max-height: 160px; resize: none;
  font-family: var(--font-ui); font-size: 1rem; color: var(--ink);
  background: var(--field-bg); border: 1.5px solid var(--field-border);
  border-radius: var(--radius); padding: 13px 15px;
  transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}
.msg-compose textarea::placeholder { color: #9AA792; }
.msg-compose textarea:focus {
  outline: none; background: var(--paper-raised);
  border-color: var(--pen); box-shadow: 0 0 0 3px rgba(20, 96, 58, 0.16);
}
.msg-compose .btn { flex-shrink: 0; }
.msg-attach-btn {
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; padding: 0; border-radius: var(--radius);
  background: transparent; border: 1.5px solid var(--field-border); color: var(--ink-soft); cursor: pointer;
}
.msg-attach-btn:hover { border-color: var(--pen); color: var(--pen); }
.msg-attach-name { font-family: var(--font-ui); font-size: 0.85rem; color: var(--ink-soft); }

/* (b) Correspondence entries — used only by the admin read-only viewer */
.msg-thread {
  max-height: 360px; overflow-y: auto;
  padding: var(--space-1) var(--space-3);
  background: var(--paper); border: 1px solid var(--rule); border-radius: 8px;
}
.msg-entry { padding: var(--space-2) 0; border-bottom: 1px dotted var(--rule); }
.msg-entry:last-child { border-bottom: 0; }
.msg-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); margin-bottom: 5px; }
.msg-from { font-family: var(--font-ui); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink); }
.msg-time { font-family: var(--font-ui); font-size: 0.76rem; color: var(--ink-soft); white-space: nowrap; }
.msg-text { font-family: var(--font-serif); font-size: 1rem; line-height: 1.5; color: var(--ink); word-wrap: break-word; overflow-wrap: anywhere; }
.msg-empty { padding: var(--space-3) 0; }
.admin-thread { margin-top: var(--space-2); max-height: 280px; padding: var(--space-1) var(--space-2); }

/* Two-way switch — same pill idiom as the parent's child switcher, on paper */
.seg { display: inline-flex; background: var(--wash); border-radius: 100px; padding: 3px; }
.seg-opt {
  border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-ui); font-size: 0.88rem; font-weight: 600;
  color: var(--ink-soft); padding: 7px 16px; border-radius: 100px;
}
.seg-opt[aria-pressed="true"] { background: var(--green-deep); color: var(--on-deep); }
.col-4 { grid-column: span 4; }
.col-5 { grid-column: span 5; }
.col-6 { grid-column: span 6; }
.col-7 { grid-column: span 7; }
.col-8 { grid-column: span 8; }
.col-12 { grid-column: span 12; }
/* Every card goes full width below this. This rule existed before but was
   written as [class^="col-"], which matches only when the class attribute
   *starts* with "col-" -- and every card is class="card col-4". So it never
   matched once, and the twelve-column grid stayed twelve columns on a phone:
   a col-4 card was 33% of 390px, about 108px wide. That single typo is most of
   why the portal felt squashed. Targeting the children directly means a new
   card can never miss it. */
@media (max-width: 900px) { .dash-grid > * { grid-column: 1 / -1; } }

.dash-head-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.child-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--space-3); }
/* University prep replaces the subject AND board pickers with one box, so it
   takes both of their columns rather than leaving the second one empty. */
.child-form-grid .ac-uni-only { grid-column: 1 / -1; }
.child-form-grid .ac-uni-only textarea { min-height: 96px; }
@media (max-width: 620px) { .child-form-grid { grid-template-columns: 1fr; } }
.child-form-foot { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-2); }
.child-form-foot .form-status { margin-top: 0; }
.child-form-sub { margin-top: var(--space-2); padding-top: var(--space-1); border-top: 1px solid var(--rule); }

/* Progress-reports list (parent + student dashboards) */
.report-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.report-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: 10px 0; border-bottom: 1px solid var(--rule); flex-wrap: wrap;
}
.report-item:last-child { border-bottom: 0; }
/* Unread, the same idea as an unread conversation: the row takes weight and
   says "New" in words. A coloured dot beside a date tells a parent nothing.
   The number of these on the page is the number on the Progress tab, which is
   the whole point -- a red circle you cannot trace to anything is a puzzle. */
.report-item.is-unread strong { font-weight: 800; }
.report-new {
  display: inline-block; vertical-align: middle; margin-left: 6px;
  padding: 1px 7px; border-radius: 999px;
  background: var(--error); color: #FFFFFF;
  font-family: var(--font-ui); font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.02em; text-transform: uppercase;
}
.report-actions { display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
/* Why a report would not open, said in the row rather than swallowed. */
.report-status { flex-basis: 100%; color: var(--error); margin-top: 4px; }

.tutor-picker {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-top: 10px; padding: 10px 12px; border: 1px solid var(--rule); border-radius: 8px;
  background: var(--wash);
}
.tutor-picker .remember { margin-right: 4px; }

/* Each tutor now carries what they teach under their name, so the label is two
   lines and the checkbox has to sit against the FIRST of them rather than
   floating in the middle of the pair. The row itself goes to flex-start for the
   same reason -- a one-line tutor and a two-line one otherwise centre against
   each other and the checkboxes stop lining up.

   min-width on the name column: a flex item defaults to min-width auto, which
   is min-content, so a long "GCSE, A-Level Further Mathematics" refused to wrap
   and pushed the picker wider than its card. Same trap as the dash-grid one. */
.tutor-picker { align-items: flex-start; }
.tutor-picker .remember.tutor-pick { align-items: flex-start; }
.tutor-picker .remember.tutor-pick input { margin-top: 2px; flex: none; }
.tutor-pick-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tutor-pick-teaches { font-size: 0.8rem; line-height: 1.3; }

.admin-item.is-deleted { opacity: 0.6; }

.admin-item { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-3); padding: var(--space-2) 0; border-top: 1px solid var(--rule); flex-wrap: wrap; }
.admin-item:first-child { border-top: 0; }
.admin-item-main { flex: 1 1 320px; }
.admin-item-main .what { margin-top: 6px; }
.admin-item-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.admin-tutor-select { min-width: 190px; }

.stars-input { display: inline-flex; gap: 2px; }
.stars-input button {
  border: 0; background: transparent; cursor: pointer; padding: 2px;
  /* Was --rule-strong, which measured 1.81:1 on white. An empty star is
     meant to look empty, but it is also the target you have to hit to leave a
     rating -- invisible is not restrained, it is broken. */
  font-size: 1.3rem; line-height: 1; color: var(--ink-soft);
}
.stars-input button:hover, .stars-input button.on { color: var(--gold); }

.snapshot-foot { margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--rule); }
.btn-danger-link {
  border: 0; background: transparent; padding: 0; cursor: pointer;
  font-family: var(--font-ui); font-size: 0.88rem; font-weight: 600;
  color: var(--error); text-decoration: underline; text-underline-offset: 3px;
}
.btn-danger-link:hover { color: #7a1414; }

.stat-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.stat { flex: 1; min-width: 120px; }
.stat .n { font-size: 2rem; font-weight: 600; line-height: 1; color: var(--green-deep); }
.stat .n em { font-style: normal; color: var(--pen); }
.stat .k { font-family: var(--font-ui); font-size: 0.86rem; color: var(--ink-soft); margin-top: 6px; }

.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { padding: 14px 0; border-bottom: 1px dotted var(--rule); }
.timeline li:last-child { border-bottom: 0; }
.timeline .when { font-family: var(--font-ui); font-size: 0.82rem; color: var(--ink-soft); }
.timeline .what { margin: 3px 0 0; }
.timeline .what strong { font-weight: 600; }

.pill {
  display: inline-block;
  font-family: var(--font-ui); font-size: 0.76rem; font-weight: 600;
  padding: 3px 10px; border-radius: 100px;
}
.pill--paid { color: var(--pen); background: #E4F0E7; }
.pill--due { color: var(--gold-ink); background: var(--gold-soft); }
/* Neutral, not alarming: a student without a tutor is a job to do, not a fault. */
.pill--none { color: var(--ink-soft); background: var(--rule); }

/* --- Admin student roster ------------------------------------------------ */
.roster-controls {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  align-items: flex-end; margin-bottom: var(--space-2);
}

/* Segmented filter. Reads as one control, so the group being shown is obvious
   at a glance rather than needing to be remembered. */
.seg { display: inline-flex; border: 1px solid var(--rule-strong); border-radius: 4px; overflow: hidden; }
.seg button {
  font-family: var(--font-ui); font-size: 0.82rem;
  padding: 8px 12px; border: 0; border-left: 1px solid var(--rule);
  background: transparent; color: var(--ink-soft); cursor: pointer;
}
.seg button:first-child { border-left: 0; }
.seg button:hover { background: var(--wash); color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--green-deep); color: #fff; }

/* ONE CARD PER STUDENT, one row per subject inside it.
   The roster used to be a flat list of enrolments, so a student taking three
   subjects drew three rows all headed with the same name -- which reads as
   three people, and hid the fact that each row carries its OWN tutor. The
   nesting is the whole point: the person is the card, and "give them a second
   tutor" is visibly "give this second subject a tutor". */
.roster-student {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--rule-strong);
}
.roster-student:first-child { border-top: 0; padding-top: 0; }
.roster-student-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--space-3); flex-wrap: wrap;
}
.roster-student-name { font-size: 1.05rem; }
.roster-student-head .note { margin: 4px 0 0; }
/* The fold. Same convention as .avail-fold and the consultation rows: a quiet
   pen-coloured summary, and breathing room only once it is open. */
.roster-fold { margin-top: var(--space-2); }
.roster-fold > summary {
  cursor: pointer;
  font-family: var(--font-ui); font-weight: 600; font-size: 0.88rem;
  color: var(--pen);
}
.roster-fold > summary .note { font-weight: 400; }
.roster-fold[open] > summary { margin-bottom: var(--space-1); }

/* Indented and hairline-ruled, so the subjects read as belonging to the name
   above rather than as siblings of it. */
.roster-subjects {
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--rule);
}
.student-add {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3) var(--space-3);
  background: var(--wash);
  border-radius: 6px;
}
.student-add .child-form-grid { margin: var(--space-2) 0; }

.roster-row { padding: var(--space-3) 0; border-top: 1px solid var(--rule); }
.roster-row:first-child { border-top: 0; padding-top: 0; }
.roster-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--space-3); flex-wrap: wrap;
}
.roster-course { margin: 4px 0 0; }
.roster-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex-wrap: wrap; }

/* The facts line. A quiet row of label/value pairs -- scannable down the column
   when several students are open, without competing with the name. */
.roster-facts {
  display: flex; flex-wrap: wrap; gap: 4px var(--space-3);
  margin-top: var(--space-2);
  font-family: var(--font-ui); font-size: 0.82rem; color: var(--ink-soft);
}
.roster-facts b { font-weight: 600; color: var(--ink); font-size: 0.74rem;
  text-transform: uppercase; letter-spacing: 0.04em; margin-right: 4px; }

.roster-row .tutor-picker { margin-top: var(--space-2); }
.roster-row.is-deleted { opacity: 0.6; }
/* A fact that needs attention rather than just reading -- low write-up rates. */
.roster-facts .is-warn, .roster-facts .is-warn b { color: var(--gold); }

@media (max-width: 560px) {
  .roster-head { flex-direction: column; }
  .roster-actions { width: 100%; }
  .seg { width: 100%; }
  .seg button { flex: 1; }
}

.pay-list { list-style: none; margin: 0; padding: 0; }
.pay-list li {
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-2);
  padding: 13px 0; border-bottom: 1px solid var(--rule); font-size: 0.98rem;
}
.pay-list li:last-child { border-bottom: 0; }
.pay-list .amt { font-family: var(--font-ui); font-weight: 600; }

.pay-right { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }

/* Admin dispute + payout rows */
.dispute-row, .payout-row {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: 14px 0; border-bottom: 1px solid var(--rule);
}
.dispute-row:last-child, .payout-row:last-child { border-bottom: 0; }
.dispute-row .body, .payout-row .body { flex: 1; min-width: 240px; }
.dispute-row .body strong, .payout-row .body strong { font-weight: 600; }
.dispute-row .body .note, .payout-row .body .note { margin: 2px 0 0; }
.dispute-row .acts, .payout-row .acts { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- Legal pages -------------------------------------------------------- */
/* Long-form documents people actually have to read: a single measured column,
   generous leading, and nothing competing for attention. Generated by
   tools/build-legal.py from the Markdown in legal/. */
.page-hero--tight { padding-bottom: var(--space-4); }

/* ---- The founders, on the About page ------------------------------------
   Two columns, each person's information running down their own. Deliberately
   NOT a team card grid -- two shadowed cards is the most templated thing an
   about page can contain. The separation is a single vertical hairline between
   the columns, which is the system's one structural primitive: sections are
   divided by rules, not by boxes.

   gap is 0 on purpose. With a gap, a border on the second column hugs its own
   text and sits off-centre in the space; padding either side of the rule
   instead keeps it centred between the two people.                          */
.founders {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.founder { min-width: 0; padding: var(--space-4) 0; }
.founder:first-child { padding-right: clamp(24px, 4vw, 56px); }
.founder + .founder {
  border-left: 1px solid var(--rule);
  padding-left: clamp(24px, 4vw, 56px);
}
.founder h3 { margin: 0; }
.founder .role {
  font-family: var(--font-ui);
  font-size: 0.82rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-soft); margin: 6px 0 var(--space-2);
}
.founder p + p { margin-top: var(--space-2); }
.founder .subjects { color: var(--ink-soft); }

/* The portrait slot. Holds its space at the right shape so the page does not
   reflow when a real photograph replaces it, and reads as intentional rather
   than broken if one never does. Initials in the page's own serif -- no camera
   glyph, no grey rectangle, nothing that looks like a missing asset.

   Capped rather than filling the column: at 1100px each column is over 500px
   wide, and a 4:5 frame that wide is 640px of face before a word of the bio. */
.portrait {
  aspect-ratio: 4 / 5;
  max-width: 240px;
  margin-bottom: var(--space-3);
  background: var(--wash);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
.portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
.portrait .initials {
  font-family: var(--font-serif);
  font-size: 2.6rem; font-weight: 400;
  color: var(--ink-soft); opacity: 0.55;
  letter-spacing: 0.02em;
}
/* The wash band the founders sit on already tints the frame; lift it so the
   portrait still reads as a held space rather than dissolving into it. */
.section--wash .portrait { background: var(--paper); }

@media (max-width: 760px) {
  /* One column, and the hairline turns from vertical to horizontal -- the same
     rule doing the same job in the other axis. */
  .founders { grid-template-columns: minmax(0, 1fr); }
  .founder:first-child { padding-right: 0; }
  .founder + .founder {
    border-left: 0; padding-left: 0;
    border-top: 1px solid var(--rule);
  }
  .portrait { max-width: 160px; }
  .portrait .initials { font-size: 2rem; }
}


/* Legal text carries email addresses, URLs and statute references that no
   browser will hyphenate. On a narrow phone one of those sets the page's
   minimum width and the whole document renders zoomed out. */
.prose { max-width: 74ch; overflow-wrap: anywhere; }

/* The title above that measure, sharing its left edge. Without this the hero
   spans the full 1100px container while the text below runs to 74ch, so the
   heading starts ~157px to the left of the paragraph it introduces.
   The 7 legal pages are the only ones holding a .prose block, hence the :has().
   WARNING: 74ch, NOT the pixel value it happens to compute to. `ch` resolves against
   the element's OWN font: this container renders no text itself, so it inherits
   the same body serif .prose does and the two land on the same pixel. Put 74ch
   on the h1 instead and it would resolve against the display size -- far wider. */
main:has(.prose) .page-hero .container { max-width: 74ch; }

.prose h2 {
  font-size: clamp(1.35rem, 2.4vw, 1.7rem);
  margin: var(--space-5) 0 var(--space-2);
  padding-bottom: 8px; border-bottom: 1px solid var(--rule);
}
.prose h3 { font-size: 1.12rem; margin: var(--space-4) 0 var(--space-1); }
.prose h4 { font-family: var(--font-ui); font-size: 1rem; margin: var(--space-3) 0 6px; }
.prose p, .prose li { line-height: 1.7; }
.prose p { margin: 0 0 var(--space-2); }
.prose ul, .prose ol { margin: 0 0 var(--space-2); padding-left: 1.3em; }
.prose li { margin-bottom: 8px; }
.prose li > ul, .prose li > ol { margin-top: 8px; }
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: var(--space-4) 0; }
.prose a { color: var(--pen); text-underline-offset: 3px; }
.prose code {
  font-size: 0.92em; background: var(--wash);
  padding: 1px 5px; border-radius: 3px;
}
/* Quoted material inside a document -- e.g. the model cancellation wording. */
.prose .callout {
  margin: var(--space-3) 0; padding: var(--space-2) var(--space-3);
  background: var(--wash); border-left: 3px solid var(--pen); border-radius: 0 4px 4px 0;
}
.prose .callout p { margin: 0 0 8px; }
.prose .callout p:last-child { margin-bottom: 0; }
.prose .table-wrap { overflow-x: auto; margin: 0 0 var(--space-3); }
.prose table { border-collapse: collapse; width: 100%; font-size: 0.95rem; }
.prose th, .prose td {
  text-align: left; vertical-align: top;
  padding: 10px 14px 10px 0; border-bottom: 1px solid var(--rule);
}
.prose th { font-family: var(--font-ui); font-weight: 600; }

/* An unfinished clause. Deliberately loud -- this must never slip out unnoticed. */
/* Placeholders are often whole sentences, so they must wrap: an unbreakable
   1500px line expands the layout viewport and renders the entire page zoomed
   out on a phone. Loud enough without nowrap. */
.prose mark.ph, .draft-note mark.ph {
  background: #FBE9C6; color: #6B4A05;
  font-family: var(--font-ui); font-size: 0.86em; font-weight: 600;
  padding: 1px 6px; border-radius: 3px;
  white-space: normal; overflow-wrap: anywhere;
}
.draft-note {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: var(--space-4); padding: var(--space-2) var(--space-3);
  background: #FDF6E7; border: 1px solid #E4C77E; border-radius: 4px;
  font-family: var(--font-ui); font-size: 0.95rem; color: #5C4308;
}
.draft-note mark.ph { white-space: normal; }

/* The auth pages use a full-bleed split layout with no footer, so the legal
   links go under the form instead. */
.auth-legal {
  display: flex; flex-wrap: wrap; gap: 4px var(--space-2);
  margin: var(--space-3) auto 0; max-width: 420px;
  font-family: var(--font-ui); font-size: 0.85rem;
}
.auth-legal a { color: var(--ink-soft); text-decoration: none; }
.auth-legal a:hover, .auth-legal a:focus-visible {
  color: var(--pen); text-decoration: underline; text-underline-offset: 3px;
}
/* The same row, but under a dashboard column rather than a centred auth card,
   so it lines up with the left edge of the cards above it instead of floating
   in the middle of a wide page. Everything else is inherited -- including the
   44px tap targets the media query gives .auth-legal a on phones. */
.auth-legal--inline { max-width: none; margin-left: 0; margin-right: 0; }

/* Legal links, sitting between the footer columns and the copyright line. */
/* A grid, not a wrapped line. Five legal links running on as one paragraph
   read as a ribbon of text you skim past; in columns each one is its own
   item, which is how people actually look for "the cancellation one". */
.footer-legal {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3) 0 0;
  border-top: 1px solid rgba(245,244,236,0.14);
}
.footer-legal a {
  font-family: var(--font-ui); font-size: 0.92rem;
  color: var(--on-deep-soft); text-decoration: none;
}
.footer-legal a:hover, .footer-legal a:focus-visible {
  color: var(--on-deep); text-decoration: underline; text-underline-offset: 3px;
}

/* A card that is a doorway to its own page rather than a workspace: the heading
   is the link, and the whole card lifts slightly so it reads as clickable. */
.card--link .card-head h3 a {
  color: inherit; text-decoration: none;
  display: inline-flex; align-items: baseline; gap: 8px;
}
.card--link .card-head h3 a:hover,
.card--link .card-head h3 a:focus-visible { color: var(--pen); }
.card-link-arrow { transition: transform .18s ease; font-size: 0.9em; }
.card--link .card-head h3 a:hover .card-link-arrow { transform: translateX(3px); }
.card--link .stat .n { font-size: 1.5rem; }

/* "back to the parent page" link above a sub-page title */
.back-link {
  display: inline-block; margin-bottom: 6px;
  font-family: var(--font-ui); font-size: 0.9rem;
  color: var(--on-deep-soft); text-decoration: none;
}
.back-link:hover, .back-link:focus-visible { color: var(--on-deep); text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
  .card-link-arrow { transition: none; }
}

/* A pending two-party time change, shown inside its lesson row. Deliberately
   set apart from the row's own text so the booked time and the proposed time
   are never mistaken for each other. */
.pending-change {
  margin-top: 10px; padding: 10px 12px;
  background: var(--wash); border: 1px solid var(--rule);
  border-left: 3px solid var(--pen); border-radius: 4px;
}
.pending-change p { margin: 0; font-size: 0.95rem; }
.pending-change .note { margin-top: 2px; }
.pending-change .acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* The inline "change the time" form revealed inside a lesson row. */
.resched-form {
  margin-top: 10px; padding: 12px;
  background: var(--wash); border: 1px solid var(--rule); border-radius: 4px;
  display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: flex-end;
}
.resched-form[hidden] { display: none; }
.resched-form .field { flex: 1; min-width: 160px; margin: 0; }
.resched-form .acts { display: flex; gap: 8px; flex-wrap: wrap; }
.resched-form .form-status { flex-basis: 100%; margin: 0; }

/* A pick-one question inside an inline form -- the tutor's "why are you
   cancelling?".

   Deliberately NOT .field. That class is written for text inputs and selects:
   it sets width:100% on the control and display:block on the label, which turns
   a radio into a full-width outlined box with its own label stranded on the
   line underneath. Overriding it here would leave the next radio anywhere else
   in the site with the same problem, so this is its own thing. */
/* Focused programmatically when the form opens, never reachable by Tab, so
   there is no keyboard focus to signal and a ring here would just be a box
   drawn round the whole question. */
.resched-form[tabindex="-1"]:focus { outline: none; }

.choices {
  flex-basis: 100%;
  display: flex; flex-direction: column; gap: 10px;
  margin: 0;
}
.choices label {
  display: flex; align-items: flex-start; gap: 10px;
  /* The whole row is the target, not just the 13px dot. This question decides
     whether a family gets their money back; it should not need careful aim. */
  padding: 3px 0; margin: 0; cursor: pointer;
  font-family: var(--font-ui); font-size: 0.95rem; font-weight: 400;
  line-height: 1.45; color: var(--ink);
}
.choices input[type="radio"] {
  width: auto; flex: none;
  /* Nudged down to sit on the first line's baseline rather than its box top,
     which matters as soon as an answer wraps to two lines on a phone. */
  margin: 4px 0 0; accent-color: var(--ink);
}

/* "Set up payment" nudge on the parent dashboard */
.pay-nudge {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  justify-content: space-between;
  background: var(--wash); border: 1px solid var(--rule); border-radius: 4px;
  padding: var(--space-2); margin-bottom: var(--space-3);
}
.pay-nudge span { flex: 1; min-width: 220px; font-size: 0.95rem; }

.next-lesson {
  display: flex; gap: var(--space-2); align-items: center;
  background: var(--wash); border-radius: 8px; padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
}
.next-lesson .cal {
  flex-shrink: 0; text-align: center; background: var(--green-deep); color: var(--on-deep);
  border-radius: 6px; padding: 8px 12px; font-family: var(--font-ui); line-height: 1.1;
}
/* display:block on the day, as in .schedule .cal -- without it the day and the
   month run together on one line inside the chip ("18 Jul"), which stretches
   the chip and knocks the whole row out of alignment. */
.next-lesson .cal .d { font-size: 1.4rem; font-weight: 700; display: block; }
.next-lesson .cal .m { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; display: block; }

/* Sits under the Join button: explains the wait before the window opens, or
   that the tutor hasn't set a link. Quiet — it's an aside, not an alert. */
.join-hint { font-size: 0.86rem; margin: 0; }

/* Availability editor: a normal week, one column per day. */
#availGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--space-2); }
.avail-day { border: 1px solid var(--rule); border-radius: 6px; padding: var(--space-2); }
.avail-day h4 {
  font-family: var(--font-ui); font-size: 0.82rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft);
  margin: 0 0 10px;
}
.avail-windows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
.avail-window { display: grid; grid-template-columns: 1fr auto 1fr; gap: 6px; align-items: center; }
.avail-window select { padding: 6px 8px; font-size: 0.86rem; }
.avail-window span { font-size: 0.8rem; color: var(--ink-soft); }
.avail-window .btn-danger-link { grid-column: 1 / -1; justify-self: start; font-size: 0.8rem; }
.avail-none { margin: 0; font-size: 0.86rem; }
/* The weekly pattern + days off, folded away behind a summary -- Dylan, 6
   Sep: set-and-forget availability admin that does not need to be seen every
   visit, unlike the consultation-room link right above it. Same shape as
   .rate-add, which folds the rarer "add a rate" action the same way. */
.avail-fold { margin-top: var(--space-3); border-top: 1px solid var(--rule); padding-top: var(--space-2); }
.avail-fold > summary {
  cursor: pointer;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--pen);
}
.avail-fold > summary .note { margin-left: 6px; font-weight: 400; }
.avail-fold[open] > summary { margin-bottom: var(--space-2); }
.avail-sub { margin: var(--space-4) 0 4px; font-size: 1rem; }
.avail-dayoff { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; margin-bottom: var(--space-2); }
.avail-dayoff .field { margin: 0; }
.dayoff-list { list-style: none; margin: 0; padding: 0; }
.dayoff-list li {
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-2);
  padding: 8px 0; border-bottom: 1px solid var(--rule); font-size: 0.92rem;
}
.dayoff-list li:last-child { border-bottom: 0; }

/* One consultation in the admin pipeline: who booked, then the form that
   becomes their child record. */
.consult-row { border: 1px solid var(--rule); border-radius: 6px; padding: var(--space-2); margin-bottom: var(--space-2); }
/* Dylan, 6 Sep: each entry folds closed by default -- a card holding four or
   five of these got long fast, and most visits only need to open the one
   being edited. .consult-row is a <details> now and .consult-head its
   <summary>, so the existing name/pill layout stays exactly as it was; only
   the tag changed. list-style: none drops the native marker in favour of
   the plain chevron below, which needs a fixed side to sit on rather than
   floating wherever the browser puts a ::marker. */
.consult-row > summary { cursor: pointer; list-style: none; }
.consult-row > summary::-webkit-details-marker { display: none; }
.consult-head {
  display: flex; gap: var(--space-2); justify-content: space-between; align-items: flex-start;
  flex-wrap: wrap; margin-bottom: 0;
  padding-right: 26px; position: relative;
}
.consult-head::after {
  content: "";
  position: absolute; right: 2px; top: 4px;
  width: 10px; height: 10px;
  border-right: 2px solid var(--ink-soft); border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg);
  transition: transform 0.12s ease;
}
.consult-row[open] > summary .consult-head::after { transform: rotate(-135deg); top: 8px; }
.consult-head .note { margin: 2px 0 0; }
.consult-body { margin-top: var(--space-2); }
.consult-row .acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: var(--space-2); }

/* Dylan, 6 Sep: "add the ability to add another subject if i want to" -- a
   family can want more than one subject from the same call, but a
   consultation row only ever carries one set of subject/level/board/grade
   columns, and "one call, one child, ever" is a deliberate guard elsewhere
   (see the note above data-cs-convert's handler). So each extra subject is
   its own small self-contained block, visually set apart from the main
   form, with its own fields and its own "Add this subject" button -- it
   inserts a second (third, ...) children row directly and never touches
   the consultation's own columns, which stay subject 1's. */
/* What pressing "Create child" will do, in a sentence. It replaced two
   dropdowns that asked the admin to answer what the form already knew. Styled
   as a statement, not a control -- until it genuinely cannot tell, when
   .is-asking marks it as a question and a picker appears under it. */
.consult-plan {
  margin: var(--space-2) 0 0;
  font-family: var(--font-ui); font-size: 0.88rem; color: var(--ink-soft);
  padding-left: var(--space-2); border-left: 2px solid var(--rule-strong);
}
.consult-plan.is-asking {
  color: var(--ink); font-weight: 600;
  border-left-color: var(--gold);
}

.consult-extra-block {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-1);
  border: 1px dashed var(--rule-strong);
  border-radius: 6px;
  background: var(--mist);
}
.consult-extra-block.is-added { border-style: solid; border-color: var(--rule); background: var(--wash); }
.consult-extra-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-1);
  font-family: var(--font-ui);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.consult-extra-remove {
  background: none;
  border: 0;
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 0.8rem;
  text-decoration: underline;
  padding: 0;
}
.consult-add-subject {
  margin-top: var(--space-2);
  background: none;
  border: 0;
  padding: 0;
  color: var(--pen);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 0.88rem;
  cursor: pointer;
  text-decoration: underline;
}

/* A call whose child has already been created. It stays visible (the notes are
   still worth reading and Save notes still works) but it must not look like a
   form waiting to be submitted a second time. */
/* A button-shaped thing that is a statement of fact, not a control — the
   "Consultation booked" header button once a call is in the diary. It keeps the
   button's shape (it is the same object, in the same place) but drops every
   affordance that promises something happens if you press it. */
.btn--static {
  cursor: default;
  opacity: 0.75;
}
.btn--static:hover, .btn--static:focus { transform: none; box-shadow: none; }

/* "Add a card to finish booking" — the parent did not come to this page on
   purpose, so it needs to read as an instruction, not a footnote. */
.pm-why {
  margin: 0 0 var(--space-2);
  padding: 10px 12px;
  border-left: 3px solid var(--pen);
  background: var(--wash);
  color: var(--ink);
}

/* "This lesson is already paid for." Same shape as .pm-why -- a note that
   explains a rule at the moment it starts applying -- but in the caution gold
   rather than pen green, because this one costs money if it is ignored. Not the
   error red: nothing has gone wrong, and red here would read as a failure. */
.resched-paid {
  margin: 0 0 var(--space-2);
  padding: 10px 12px;
  border-left: 3px solid var(--gold);
  background: var(--wash-warm);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 0.88rem;
  line-height: 1.55;
}

.consult-row.is-converted { border-color: var(--rule-strong); background: var(--wash); }
.consult-row.is-converted .child-form-grid { opacity: 0.6; }

/* The "Other…" free-text box that sits under a consultation dropdown. Hidden
   until Other is chosen -- [hidden] is display:none !important globally, so the
   attribute alone does the work; this is only the spacing when it is shown. */
.consult-row .cs-other { margin-top: 6px; }

/* A fieldset will not shrink below its content's width unless told to, which is
   what pushed the time picker out under the sidebar. */
.slot-fieldset { border: 0; padding: 0; margin: 0 0 var(--space-3); min-width: 0; }
.slot-fieldset legend { font-family: var(--font-ui); font-weight: 600; font-size: 0.95rem; padding: 0; }

/* ---- Shared time grid ------------------------------------------------------
   Used for booking a consultation and for setting a lesson time, so the two
   feel like the same control. Times in a wrapping grid of equal cells; the
   optional band tabs above cut a long day into readable chunks. */
.time-bands {
  display: flex; gap: 0; margin-bottom: var(--space-2);
  border: 1px solid var(--rule); border-radius: 4px; overflow: hidden;
}
.time-band {
  flex: 1; min-width: 0;
  font-family: var(--font-ui); font-size: 0.84rem; padding: 8px 6px;
  background: var(--paper); color: var(--ink-soft);
  border: 0; border-right: 1px solid var(--rule); cursor: pointer;
  transition: background .15s, color .15s;
}
.time-band:last-child { border-right: 0; }
.time-band:hover:not(:disabled) { color: var(--ink); }
.time-band.is-picked { background: var(--wash); color: var(--ink); font-weight: 600; }
/* A band we have nothing free in is shown rather than hidden: "no mornings on a
   Thursday" is worth knowing, and a tab bar that changes shape day to day is
   harder to read than one that doesn't. */
.time-band.is-empty { opacity: 0.42; cursor: default; text-decoration: line-through; }

.time-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(82px, 1fr));
  gap: 6px; min-width: 0;
}
.time-chip {
  font-family: var(--font-ui); font-size: 0.9rem; padding: 9px 4px; text-align: center;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--rule); border-radius: 4px; cursor: pointer;
  transition: border-color .15s, background .15s, color .15s;
}
.time-chip:hover:not(:disabled) { border-color: var(--ink); }
/* Already booked. Shown rather than removed, so the grid keeps its shape and a
   tutor can see at a glance where their day is full rather than wondering why
   the times jump. */
.time-chip.is-taken {
  background: var(--wash); color: var(--ink-soft);
  border-style: dashed; cursor: default; text-decoration: line-through;
  opacity: 0.72;
}
.time-chip.is-picked {
  background: var(--green-deep); border-color: var(--green-deep); color: var(--on-deep); font-weight: 600;
}
/* Long lists scroll inside the control rather than stretching the form. */
.time-scroll { max-height: 208px; overflow-y: auto; min-width: 0; padding-right: 2px; }
.time-none { margin: 0; font-size: 0.9rem; }

/* Lesson time: the grid, then how long for. */
.slot-picker { min-width: 0; }
.slot-dur { display: flex; align-items: center; gap: 8px; margin-top: var(--space-2); }
.slot-dur label { font-size: 0.9rem; color: var(--ink-soft); margin: 0; }
/* Every lesson is an hour, so this states the length rather than offering a
   choice of one. Same size and colour as the label it replaces. */
.slot-dur-fixed { font-size: 0.9rem; color: var(--ink-soft); font-family: var(--font-ui); }
.slot-dur select { flex: 1; min-width: 0; }

.report-note {
  background: var(--mist);
  border-left: 2px solid var(--pen-bright);
  padding: var(--space-2) var(--space-3);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-style: italic;
  color: var(--ink-soft);
}

/* Small helper for the "become a tutor" callout */
.callout {
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-3);
  flex-wrap: wrap;
  background: var(--wash-warm);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: clamp(24px, 3vw, 40px);
}
.callout h3 { margin: 0 0 6px; font-size: 1.5rem; }
.callout p { margin: 0; color: var(--ink-soft); max-width: 46ch; }

/* --------------------------------------------------------------------------
   Tutor portal — student roster, schedule, income, requests, ratings
   -------------------------------------------------------------------------- */

.btn-sm { padding: 9px 16px; font-size: 0.9rem; }
.btn-ghost { background: transparent; color: var(--ink-soft); border-color: var(--rule-strong); }
.btn-ghost:hover { color: var(--ink); border-color: var(--ink); }

.dash-head .who-meta {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-family: var(--font-ui); font-size: 0.95rem; color: var(--on-deep-soft); margin: 6px 0 0;
}
.dash-head .who-meta .stars { color: #E8CE9A; }

/* "Edit" beside the course on the tracker header. Sits on the deep green band,
   so it borrows the header's own soft ink rather than --pen, which is close to
   invisible there. Underlined rather than boxed: it is a quiet correction, not
   an action the page wants you to take. */
.meta-edit {
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-ui); font-size: 0.85rem;
  color: var(--on-deep-soft); text-decoration: underline;
  text-underline-offset: 3px;
  /* The word is only ~22x17px, which is a poor target on a phone. The padding
     buys a ~44px tap area and the equal negative margin takes the space back,
     so it costs nothing in the layout and the text does not move. */
  padding: 13px 10px;
  margin: -13px -10px;
}
.meta-edit:hover, .meta-edit:focus-visible { color: var(--on-deep); }

.stars { color: var(--gold); letter-spacing: 2px; }
.rating-big { font-size: 2.8rem; font-weight: 600; color: var(--green-deep); line-height: 1; }

/* Student roster */
.roster { list-style: none; margin: 0; padding: 0; }
.roster li {
  display: grid;
  grid-template-columns: minmax(0, 2.2fr) minmax(0, 2fr) minmax(0, 1.2fr) auto;
  gap: var(--space-2);
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid var(--rule);
}
.roster li:first-child { border-top: 1px solid var(--rule); }
.roster li:last-child { border-bottom: 0; }
.roster .who { font-weight: 600; }
.roster .who small { display: block; font-family: var(--font-ui); font-weight: 400; font-size: 0.82rem; color: var(--ink-soft); }
.roster .when { font-family: var(--font-ui); font-size: 0.88rem; color: var(--ink-soft); }
.roster .prog { font-family: var(--font-ui); font-size: 0.86rem; font-weight: 600; color: var(--pen); }
.roster .acts { display: flex; gap: 8px; justify-content: flex-end; }
@media (max-width: 640px) {
  .roster li { grid-template-columns: 1fr auto; row-gap: 6px; }
  .roster .when { grid-column: 1 / -1; }
  .roster .acts { grid-column: 1 / -1; justify-content: flex-start; }
}

/* Schedule list */
.schedule { list-style: none; margin: 0; padding: 0; }
.schedule li {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 13px 0; border-bottom: 1px solid var(--rule);
}
.schedule li:last-child { border-bottom: 0; }
.schedule .cal {
  flex-shrink: 0; text-align: center; background: var(--green-deep); color: var(--on-deep);
  border-radius: 6px; padding: 7px 11px; font-family: var(--font-ui); line-height: 1.1; min-width: 52px;
}
.schedule .cal .d { font-size: 1.25rem; font-weight: 700; display: block; }
.schedule .cal .m { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; }
.schedule .body { flex: 1; }
.schedule .body strong { font-weight: 600; }
.schedule .body .note { margin: 2px 0 0; }
.schedule .acts { display: flex; gap: 8px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.schedule-more {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--rule);
}
/* The student's Lessons page is a page about one list, so that list gets to
   breathe -- bigger date chip, more room per row. On the parent's Lessons page
   the same list competes with requests and rescheduling and stays compact. */
.schedule--roomy li { padding: 18px 0; gap: var(--space-3); }
.schedule--roomy .cal { padding: 10px 14px; min-width: 62px; }
.schedule--roomy .cal .d { font-size: 1.55rem; }
.schedule--roomy .body strong { font-size: 1.06rem; }

/* Match requests */
.match-item { padding: 15px 0; border-bottom: 1px solid var(--rule); }
.match-item:first-child { border-top: 1px solid var(--rule); }
.match-item:last-child { border-bottom: 0; }
.match-item .role { font-weight: 600; }
.match-item .detail { font-family: var(--font-ui); font-size: 0.88rem; color: var(--ink-soft); margin: 2px 0 0; }
.match-actions { display: flex; gap: 8px; margin-top: 11px; }

.reviews { list-style: none; margin: 0; padding: 0; }
.reviews li { padding: 13px 0; border-bottom: 1px dotted var(--rule); }
.reviews li:last-child { border-bottom: 0; }
.reviews blockquote { margin: 0; font-style: italic; }
.reviews .cite { font-family: var(--font-ui); font-size: 0.84rem; color: var(--ink-soft); margin-top: 5px; }

/* Availability grid */
.avail { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; margin-top: var(--space-1); }
.avail .day { text-align: center; font-family: var(--font-ui); }
.avail .day .lbl { font-size: 0.74rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.avail .day .slot {
  margin-top: 5px; font-size: 0.78rem; font-weight: 600; padding: 8px 4px; border-radius: 6px;
  background: var(--wash); color: var(--ink-soft);
}
.avail .day .slot.on { background: #E4F0E7; color: var(--pen); }

/* --------------------------------------------------------------------------
   Spec-point progress tracker (tracker.html + dashboard readouts)
   -------------------------------------------------------------------------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* S / A / W rating chips (shared by tracker + parent/student dashboards) */
.rate {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; padding: 2px 8px; border-radius: 999px;
  font-family: var(--font-ui); font-weight: 700; font-size: 0.8rem;
  border: 1px solid transparent; line-height: 1.4;
}
.rate--s { background: #E7F0E9; color: #1B5E3F; border-color: #B7D6C2; }
.rate--a { background: #F6EEDD; color: #8A5A12; border-color: #E4CE9E; }
.rate--w { background: #F6E4E0; color: #8A2B1B; border-color: #E4B7AD; }
.rate--unrated { background: var(--wash); color: var(--ink-soft); border-color: var(--rule); }

/* Coverage stat + rating tallies */
.stat-big { font-family: var(--font-serif); font-size: 2.6rem; line-height: 1; margin: 0; color: var(--ink); }
.rate-tallies { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.rate-tally { font-family: var(--font-ui); font-size: 0.95rem; color: var(--ink-soft); display: inline-flex; align-items: center; gap: 8px; }
.rate-tally b { color: var(--ink); font-size: 1.15rem; }

/* Optional-paper picker. One select for a course that picks one (AQA 7408),
   two stacked for the Further Maths courses. Same gap as .spec-rows so a
   two-select field does not read as one control that has grown. */
#ob-option-selects { display: flex; flex-direction: column; gap: var(--space-2); }

/* Lesson-logging spec rows */
.spec-rows { display: flex; flex-direction: column; gap: var(--space-3); }
.spec-row {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-2); border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--paper);
}
.spec-row-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 92px auto auto;
  align-items: center; gap: var(--space-2);
}
.spec-row-detail {
  display: grid;
  grid-template-columns: minmax(0, 220px) minmax(0, 1fr);
  align-items: start; gap: var(--space-2);
}
.spec-weaknesses { display: flex; flex-direction: column; gap: 6px; }
.spec-weaknesses .spec-weakness[hidden] { display: none; }
.spec-pick { position: relative; min-width: 0; }
.spec-search, .spec-score, .spec-weakness, .spec-comment, .spec-override {
  width: 100%; font-family: var(--font-ui); font-size: 0.98rem; color: var(--ink);
  background: var(--field-bg); border: 1.5px solid var(--field-border);
  border-radius: var(--radius); padding: 11px 13px;
  transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}
.spec-weakness, .spec-override { font-size: 0.9rem; }
.spec-search::placeholder, .spec-comment::placeholder { color: #9AA792; }
.spec-search:hover, .spec-score:hover, .spec-weakness:hover, .spec-comment:hover, .spec-override:hover { border-color: var(--pen-bright); }
.spec-search:focus, .spec-score:focus, .spec-weakness:focus, .spec-comment:focus, .spec-override:focus {
  outline: none; background: var(--paper-raised); border-color: var(--pen);
  box-shadow: 0 0 0 3px rgba(20, 96, 58, 0.16);
}
.spec-score { text-align: center; }
/* Rating control: three S/A/W toggle buttons. A manual override when the row has
   no score; when a score is present it shows the derived rating, read-only. */
.spec-rate { justify-self: center; display: inline-flex; gap: 4px; }
.rate-btn {
  font-family: var(--font-ui); font-weight: 700; font-size: 0.8rem;
  min-width: 28px; padding: 5px 7px; line-height: 1; cursor: pointer;
  background: var(--paper-raised); color: var(--ink-soft);
  border: 1px solid var(--rule); border-radius: var(--radius);
  transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.rate-btn:hover:not(:disabled) { border-color: var(--pen-bright); color: var(--ink); }
.rate-btn.on.rate-btn--s { background: #E7F0E9; color: #1B5E3F; border-color: #B7D6C2; }
.rate-btn.on.rate-btn--a { background: #F6EEDD; color: #8A5A12; border-color: #E4CE9E; }
.rate-btn.on.rate-btn--w { background: #F6E4E0; color: #8A2B1B; border-color: #E4B7AD; }
.spec-rate.is-derived .rate-btn { cursor: default; }
.spec-rate.is-derived .rate-btn:not(.on) { opacity: 0.4; }
.boundary-now {
  font-family: var(--font-ui); font-size: 0.95rem; color: var(--ink);
  background: var(--wash); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 10px 13px; margin: 0 0 var(--space-2);
}
.spec-remove {
  border: 0; background: transparent; cursor: pointer; color: var(--ink-soft);
  font-size: 1.5rem; line-height: 1; padding: 0 6px; border-radius: 6px;
  transition: color 150ms ease, background-color 150ms ease;
}
.spec-remove:hover { color: #8A2B1B; background: #F6E4E0; }

/* Spec-point search dropdown */
.spec-drop {
  position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 4px);
  background: var(--paper-raised); border: 1px solid var(--rule);
  border-radius: var(--radius); box-shadow: 0 12px 28px rgba(22, 53, 42, 0.14);
  max-height: 320px; overflow-y: auto;
}
.spec-opt {
  display: block; width: 100%; text-align: left; border: 0; cursor: pointer;
  background: transparent; padding: 9px 12px; border-bottom: 1px solid var(--rule);
  transition: background-color 120ms ease;
}
.spec-opt:last-child { border-bottom: 0; }
.spec-opt:hover { background: var(--wash); }
.spec-opt-code { display: inline-block; font-family: var(--font-ui); font-weight: 700; font-size: 0.82rem; color: var(--pen); margin-right: 8px; }
.spec-opt-text { font-size: 0.86rem; color: var(--ink-soft); }
.spec-none { padding: 10px 12px; font-family: var(--font-ui); font-size: 0.88rem; color: var(--ink-soft); }

/* Spec-points-assessed table */
.table-scroll { overflow-x: auto; }

/* A long list that has been capped: it scrolls in its own box rather than
   pushing every card below it off the screen. The header stays put so you can
   still tell which column you are reading 200 rows down. */
.table-scroll--capped { max-height: 26rem; overflow-y: auto; }
.table-scroll--capped thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--paper);
  box-shadow: inset 0 -1px 0 var(--rule);
}

/* Search + sort above a long list. */
.list-controls {
  display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.list-search {
  flex: 1 1 16rem; min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--rule-strong); border-radius: 4px;
  font-family: var(--font-ui); font-size: 0.92rem;
  background: var(--paper); color: var(--ink);
}
.list-sort { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-ui); font-size: 0.88rem; color: var(--ink-soft); }
.list-sort select { padding: 7px 8px; border: 1px solid var(--rule-strong); border-radius: 4px; background: var(--paper); color: var(--ink); }
.list-foot { display: flex; gap: var(--space-2); align-items: center; margin-top: var(--space-2); }

@media (max-width: 640px) {
  .list-controls { flex-direction: column; align-items: stretch; }
  .list-sort { justify-content: space-between; }
  .list-sort select { flex: 1 1 auto; margin-left: 8px; }
  /* Shorter on a phone -- 26rem is most of the viewport. */
  .table-scroll--capped { max-height: 18rem; }
}
.spec-table,
.rate-table { width: 100%; border-collapse: collapse; font-family: var(--font-ui); font-size: 0.9rem; }
.spec-table th,
.rate-table th {
  text-align: left; padding: 8px 10px; border-bottom: 1.5px solid var(--rule);
  color: var(--ink-soft); font-weight: 600; font-size: 0.8rem; white-space: nowrap;
}
.spec-table td,
.rate-table td { padding: 9px 10px; border-bottom: 1px solid var(--rule); color: var(--ink); vertical-align: top; }
.spec-table .mono { font-weight: 700; color: var(--pen); white-space: nowrap; }
.spec-table .spec-statement { color: var(--ink-soft); max-width: 360px; }

@media (max-width: 620px) {
  .spec-row-main { grid-template-columns: 1fr auto auto; grid-template-areas: "pick pick pick" "score rate remove"; }
  .spec-pick { grid-area: pick; }
  .spec-score { grid-area: score; }
  .spec-rate { grid-area: rate; }
  .spec-remove { grid-area: remove; }
  .spec-row-detail { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Mobile -- phones and small tablets
   ==========================================================================
   The portal was laid out for a desk: rows read left-to-right as
   subject | detail | actions, and tables carry six or seven columns. None of
   that survives a 390px screen, so on a phone those rows become stacked
   records and the tables become labelled lists.

   Two containment rules elsewhere in this file matter more than anything here,
   because without them the page doesn't merely look cramped -- it renders
   zoomed out. A grid or flex item defaults to min-width:auto (= min-content),
   so a single element that refuses to shrink pushes the layout viewport past
   device-width and the browser scales the whole document down to fit. See the
   min-width:0 on .dash-grid > * / .card, and the wrapping .schedule .acts
   below. Anything new that sits in a row and cannot wrap needs the same care.
   -------------------------------------------------------------------------- */

@media (max-width: 640px) {

  /* --- What a phone doesn't need ------------------------------------------
     Doorway cards (Progress / Lessons / Payments on the parent dashboard,
     Earnings for a tutor, Tutors for an admin) exist to signpost pages that
     are already one tap away in the menu. On a wide screen they fill space
     that would otherwise be empty; on a phone they are three screenfuls of
     signposting between you and your actual information. Every destination is
     in PORTAL_NAV for that role, so nothing becomes unreachable.

     Same reasoning for the Next lesson card on lessons.html: the list directly
     underneath it opens with that exact lesson. It stays on the dashboard,
     where it is the point of the page. */
  .card--link { display: none; }
  .dash[data-portal="parent"] #nextLessonCard { display: none; }

  /* --- Page furniture --------------------------------------------------- */
  .dash-head-actions { width: 100%; }
  .dash-head-actions .btn { flex: 1 1 auto; justify-content: center; }

  /* A card heading and its note share one line on a wide screen. On a phone
     that line isn't wide enough for both, and the heading ends up broken over
     two lines against a note squeezed beside it. Let the note drop. */
  .card-head { flex-wrap: wrap; }
  .card-head h3 { flex: 1 1 100%; }

  /* --- Lesson rows ------------------------------------------------------
     Was: date chip | detail | buttons, all on one line. The buttons had
     flex-shrink:0 and so never wrapped -- with four of them the row demanded
     434px and took the whole page down with it. Now the date chip and the
     detail share the top line and the actions get a full-width row of their
     own, which is also the only way they are comfortably tappable. */
  .schedule li {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: var(--space-2);
    row-gap: 11px;
  }
  .schedule .cal { grid-column: 1; }
  .schedule .body { grid-column: 2; min-width: 0; }
  .schedule .acts {
    grid-column: 1 / -1;
    min-width: 0;
    justify-content: flex-start;
  }
  .schedule .acts .btn { flex: 1 1 auto; }
  .schedule-more { justify-content: center; }

  /* The reschedule form sits inside a row that is already narrow. */
  .resched-form .field { flex-basis: 100%; min-width: 0; }
  .resched-form .acts .btn { flex: 1 1 auto; }

  /* --- Payment history ---------------------------------------------------
     Description above, then the money. The dispute button is ordered last so
     the amount and its status stay together and read first. */
  .pay-list li { flex-direction: column; align-items: stretch; gap: 9px; }
  .pay-right { width: 100%; justify-content: flex-start; }
  .pay-right .btn { order: 3; flex: 1 1 auto; }

  /* --- Admin dispute + payout rows -------------------------------------- */
  .dispute-row .body, .payout-row .body { flex-basis: 100%; min-width: 0; }
  .dispute-row .acts, .payout-row .acts { width: 100%; }
  .dispute-row .acts .btn, .payout-row .acts .btn { flex: 1 1 auto; }

  /* --- Rosters (students and tutors) -------------------------------------
     Stacking .roster-head alone would leave the buttons sitting between the
     name and the course line. Dissolving the head with display:contents lets
     the whole row order properly: who it is, what they study, the facts, then
     the things you can do about it. */
  .roster-row { display: flex; flex-direction: column; gap: var(--space-2); }
  .roster-head { display: contents; }
  /* Everything gets an explicit order. Without the catch-all, anything not
     listed keeps order:0 and sorts ABOVE the name -- which is how the "Asked:"
     and "Declined by" notes ended up introducing a student before you had been
     told who the student was. */
  .roster-row > * { order: 4; }
  .roster-head > div:first-child { order: 1; }
  .roster-course { margin: 4px 0 0; }
  .roster-facts { order: 3; margin: 0; gap: 7px var(--space-2); }
  .roster-row .tutor-picker { order: 5; margin: 0; }
  .roster-actions { order: 6; width: 100%; }
  .roster-actions .btn { flex: 1 1 auto; }
  .roster-controls > .field { flex-basis: 100%; min-width: 0; }
  .tutor-picker { align-items: stretch; }
  .tutor-picker select { width: 100%; }
  .tutor-picker .btn { flex: 1 1 auto; }
  .report-item { align-items: stretch; }
  .report-actions { width: 100%; }

  /* --- Stats -------------------------------------------------------------
     Two across rather than four squeezed onto one line. */
  .stat { flex: 1 1 40%; min-width: 0; }

  /* --- Consultation availability -----------------------------------------
     Seven columns of "17:00-20:00" is unreadable on a phone; a day-per-line
     list says the same thing and is easier to check. */
  .avail { grid-template-columns: 1fr; gap: 0; }
  .avail .day {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-2); text-align: left;
    padding: 9px 0; border-bottom: 1px solid var(--rule);
  }
  .avail .day:last-child { border-bottom: 0; }
  /* A fixed width keeps the times in a column you can read down, instead of
     each chip hugging its own text. */
  .avail .day .slot { margin-top: 0; min-width: 8.5em; text-align: center; }

  /* --- Messaging --------------------------------------------------------- */
  .msg-app-inner { height: calc(100dvh - 118px); }
  .msg-bar .field { min-width: 0; flex-basis: 100%; }
  .msg-row { max-width: 86%; }
  .msg-compose { flex-wrap: wrap; }
  .msg-compose textarea { flex: 1 1 100%; }
  .msg-compose .msg-attach-btn { flex: 0 0 auto; }
  .msg-compose .btn { flex: 1 1 auto; }
  .msg-attach img { max-width: 100%; }

  /* --- Nudges ------------------------------------------------------------ */
  .pay-nudge span { min-width: 0; flex-basis: 100%; }
  .pay-nudge .btn { flex: 1 1 auto; }

  /* Two columns rather than three: at this width three would break
     "Terms and Conditions" across three lines. */
  .footer-legal { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* --- Data tables become labelled records -------------------------------
     Six or seven columns cannot be read side by side on a phone, and a
     horizontal scrollbar hides the columns people actually came for. Each row
     becomes a record with its column heading beside each value; the headings
     come from data-label, set where the rows are built (main.js for the
     tracker tables, tools/build-legal.py for the legal ones). */
  .table-scroll, .prose .table-wrap { overflow-x: visible; }

  .spec-table, .spec-table tbody, .spec-table tr, .spec-table td,
  .rate-table, .rate-table tbody, .rate-table tr, .rate-table td { display: block; }
  .spec-table thead, .rate-table thead { display: none; }
  .spec-table tr,
  .rate-table tr {
    padding: 11px 0;
    border-bottom: 1px solid var(--rule);
  }
  .spec-table tr:last-child, .rate-table tr:last-child { border-bottom: 0; }
  .spec-table td, .rate-table td { border: 0; padding: 2px 0; }
  .spec-table td[data-label],
  .rate-table td[data-label] {
    display: grid;
    grid-template-columns: 7.5em minmax(0, 1fr);
    gap: var(--space-2);
    align-items: baseline;
  }
  .spec-table td[data-label]::before,
  .rate-table td[data-label]::before {
    content: attr(data-label);
    font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--ink-soft);
  }
  /* Pills and other real elements would otherwise stretch the full width of
     the value column and stop reading as chips. Bare text is left alone, so a
     long spec statement still wraps into the whole column. */
  .spec-table td[data-label] > *,
  .rate-table td[data-label] > * { justify-self: start; }
  /* An empty data-label is the actions column: no heading to print, and the
     buttons want the whole width rather than a 7.5em gutter of nothing. */
  .rate-table td[data-label=""] { grid-template-columns: minmax(0, 1fr); padding-top: 8px; }
  .rate-table td[data-label=""]::before { content: none; }
  .spec-table .spec-statement { max-width: none; }
  .spec-table td.row-act { padding-top: 7px; }

  /* Legal tables carry sentences rather than figures, so the heading sits
     above the value and the value keeps the full width to wrap into. */
  .prose table, .prose tbody, .prose tr, .prose td { display: block; }
  .prose thead { display: none; }
  .prose table { font-size: 1rem; }
  .prose tr {
    padding: 12px 0;
    border-bottom: 1px solid var(--rule);
  }
  .prose tr:last-child { border-bottom: 0; }
  .prose td { border: 0; padding: 5px 0; }
  .prose td[data-label]::before {
    content: attr(data-label);
    display: block; margin-bottom: 2px;
    font-family: var(--font-ui); font-size: 0.72rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-soft);
  }
}

/* --------------------------------------------------------------------------
   Touch targets
   Keyed off the pointer rather than the viewport: a small laptop still gets
   the tighter desktop spacing, and a large tablet still gets targets you can
   hit. 44px is the size a fingertip can reliably land on.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .btn-sm { padding: 12px 16px; }
  .seg button { padding: 12px 14px; }
  .link-arrow, .paper-del, .msg-attach { min-height: 44px; align-items: center; }

  /* Only .field input / .field select carry the form styling, so a bare
     control -- the tutor picker's select, for one -- falls back to the
     browser default and lands about 19px tall. */
  button,
  select,
  textarea,
  input:not([type="checkbox"]):not([type="radio"]) { min-height: 44px; }
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }

  /* Two exceptions, because a blanket min-height distorts them rather than
     helping. A star is a glyph in a tight row -- stretching its box leaves the
     stars floating above their own baseline; the honest fix is a bigger star.
     "Remove this child" is a text link that happens to be a <button>. */
  .stars-input button { min-height: 0; font-size: 1.9rem; padding: 3px 5px; }
  .btn-danger-link { min-height: 0; padding: 10px 0; }

  /* The blanket rule above gives the password eye 44px of height but leaves it
     about 32px wide, which is the dimension a thumb actually misses. Widening it
     means the field needs more room, or the last characters typed run underneath
     it -- exactly what you are looking at when you tap it. */
  .pw-toggle { min-width: 44px; }
  .field .pw-wrap input, .pw-wrap input { padding-right: 3.4rem; }

  /* Native date and time controls centre their value against whatever height
     you give them, which reads as text sitting off-centre in the field. */
  input[type="date"], input[type="time"] {
    -webkit-appearance: none; appearance: none;
    display: flex; align-items: center;
  }
  input[type="date"]::-webkit-date-and-time-value,
  input[type="time"]::-webkit-date-and-time-value { text-align: left; margin: 0; }
  input[type="date"]::-webkit-calendar-picker-indicator { margin-left: auto; }

  /* Footer and legal links sit in dense lists, so they gain height from
     padding rather than from margins that would stretch the whole column. */
  .footer-col li { margin-bottom: 2px; }
  .footer-col a { display: inline-block; padding: 9px 0; }
  .footer-legal a { display: inline-block; padding: 10px 0; }
  .auth-legal a { display: inline-flex; align-items: center; min-height: 44px; }
  /* .auth-alt links are deliberately left alone: they sit inside a sentence
     ("New to Tutora? Create an account..."), which WCAG 2.5.8 excludes, and
     padding them to 44px would pull the sentence apart. */

  /* A footnote marker is a 21px target, and it is a superscript -- growing the
     box would push the line apart. An invisible overlay gives the finger
     something to hit without touching the layout. */
  .fn-ref { position: relative; }
  .fn-ref::after {
    content: "";
    position: absolute; left: 50%; top: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
}

/* --------------------------------------------------------------------------
   Finances (admin-finances.html)
   --------------------------------------------------------------------------
   The P&L is a flow, not a stat row. Four figures side by side all look equally
   important, and here they are not: three are subtractions and the fourth is
   the answer. Setting them as steps down a column, with the outgoings marked
   and the total ruled off, means the shape says what the numbers mean before
   anyone reads them.
   -------------------------------------------------------------------------- */

.fin-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-3) 0 var(--space-3);
}
.fin-periods, .fin-groups { display: flex; flex-wrap: wrap; gap: 6px; }
.fin-custom { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-2); }
.fin-custom label { font-family: var(--font-ui); font-size: 0.8rem; color: var(--ink-soft); }
.fin-custom input {
  display: block; margin-top: 3px; padding: 5px 8px;
  font-family: var(--font-ui); font-size: 0.9rem;
  border: 1px solid var(--rule-strong); border-radius: 4px;
  background: var(--paper); color: var(--ink);
}

.pl-flow { list-style: none; margin: 0; padding: 0; max-width: 620px; }
.pl-step {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px var(--space-3);
  padding: 12px 0;
  border-bottom: 1px solid var(--rule);
}
.pl-k { font-family: var(--font-ui); font-size: 0.95rem; }
.pl-v {
  font-family: var(--font-serif);
  font-size: 1.5rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
/* The sub-line runs under the label, in the label's column, so the figures
   stay in one clean right-aligned run. */
.pl-n { grid-column: 1; font-family: var(--font-ui); font-size: 0.76rem; color: var(--ink-soft); }

.pl-step--out .pl-v { color: var(--ink-soft); }
.pl-step--total {
  border-bottom: 0;
  border-top: 2px solid var(--ink);
  margin-top: 4px;
  padding-top: 14px;
}
.pl-step--total .pl-k { font-weight: 600; }
.pl-step--total .pl-v { font-size: 2rem; color: var(--pen-bright); }

.fin-flags { list-style: none; margin: 0; padding: 0; }
.fin-flags li {
  padding: 10px 0;
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
  font-size: 0.92rem;
}
.fin-flags li:first-child { border-top: 0; }
.fin-flags strong { display: inline-block; min-width: 9em; }
.fin-flags span { color: var(--ink-soft); }
.fin-flags .link-arrow { font-size: 0.85rem; margin-left: 6px; }

.fin-controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
}
.fin-tools { display: flex; align-items: center; gap: var(--space-2); }
.fin-tools input[type="search"] {
  padding: 6px 10px;
  min-width: 12rem;
  font-family: var(--font-ui);
  font-size: 0.9rem;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
}

/* Capped and scrollable rather than however long the data happens to be.
   See the long-list rule -- this is fine with six lessons and unreadable with
   six hundred, and the second case is the one worth designing for. */
.fin-scroll { max-height: 26rem; overflow-y: auto; }
.fin-more { margin: var(--space-2) 0 0; }
.fin-profit { font-weight: 600; font-variant-numeric: tabular-nums; }

@media (max-width: 640px) {
  .fin-controls { flex-direction: column; align-items: stretch; }
  .fin-tools { flex-direction: column; align-items: stretch; }
  .fin-tools input[type="search"] { min-width: 0; width: 100%; }
  .fin-scroll { max-height: none; }   /* the phone already scrolls */
  .pl-step--total .pl-v { font-size: 1.6rem; }
  .fin-flags strong { display: block; min-width: 0; }
}

/* --------------------------------------------------------------------------
   Rates & pay (admin-rates.html)
   --------------------------------------------------------------------------
   The tables themselves reuse .spec-table's rules, including its phone
   treatment, so there is one table style in the portal rather than two that
   slowly diverge. What is left here is the money inputs, the explainer, and
   the per-student panel on the roster.
   -------------------------------------------------------------------------- */

.rate-input {
  width: 5.5em;
  padding: 5px 7px;
  font-family: var(--font-ui);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
}
.rate-input:focus-visible { outline: 2px solid var(--pen); outline-offset: 1px; }
/* The spinners cost 20px of a 5.5em box and nobody nudges a price by 1p. */
.rate-input::-webkit-outer-spin-button,
.rate-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.rate-input { -moz-appearance: textfield; appearance: textfield; }

.rate-table td .note { margin: 3px 0 0; font-size: 0.75rem; }
.rate-margin { font-variant-numeric: tabular-nums; font-weight: 600; }

/* Where a number came from, shown next to it. Four layers is not something an
   admin should have to hold in their head while reading a table. */
.rate-src {
  display: inline-block;
  font-size: 0.7rem;
  padding: 1px 6px;
  border-radius: 3px;
  background: var(--wash);
  color: var(--ink-soft);
}
.rate-src--student { background: #F3E3D3; color: #7A4A18; }
.rate-src--tutor   { background: #E2ECE4; color: #1E5638; }
.rate-src--subject { background: #EAE7F2; color: #4A3E6B; }

/* The four-step explainer. Numbered because the order is the whole point. */
.rate-chain {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-3);
}
.rate-chain li { display: flex; gap: 10px; align-items: flex-start; }
.rate-chain .rc-n {
  flex: none;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--green-deep); color: var(--on-deep);
  font-family: var(--font-ui); font-size: 0.75rem; font-weight: 700;
}
.rate-chain p { margin: 2px 0 0; }

/* The four-layer explainer, folded away. It is reference: you read it once and
   then you know, so it should not take a full card at the top of the page on
   every visit. The summary carries the whole rule in one sentence, which is all
   most visits need. */
.rate-explainer { padding: var(--space-3); }
.rate-explainer > summary {
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 0.95rem;
  list-style-position: outside;
}
.rate-explainer > summary .note { margin-left: 6px; }
.rate-explainer[open] > summary { margin-bottom: var(--space-3); }
.rate-explainer .rate-chain { margin-top: var(--space-2); }

/* Adding a rate is the rarer action, so it is folded away behind a summary
   rather than sitting open above the table you came to read. */
.rate-add { margin-top: var(--space-3); border-top: 1px solid var(--rule); padding-top: var(--space-2); }
.rate-add > summary {
  cursor: pointer;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--pen);
}
.rate-add-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin: var(--space-2) 0;
}

/* The per-student override, on a roster row. */
.rate-panel,
.student-panel {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3) var(--space-3);
  background: var(--wash);
  border-radius: 6px;
}
/* "Same student as…" -- which PERSON an enrolment belongs to. Same surface as
   the rate panel because it is the same kind of thing: a rarely-used admin
   correction, folded away until it is wanted. */
.student-panel-row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
.student-panel-row select { flex: 1 1 220px; min-width: 0; }
.rate-panel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
}
.rate-panel-grid label {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.rate-panel-grid input {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 7px 9px;
  font-family: var(--font-ui);
  font-size: 0.92rem;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
}
.rate-panel .note { margin-top: var(--space-2); }

/* ==========================================================================
   THE REPORT ITSELF — lifted verbatim from the report engine
   ==========================================================================
   Everything below is the engine's own stylesheet, copied out of a report it
   actually generated (tutora-report-engine/reports/m.html, 12 Aug 2026) rather
   than written here. The example reports on this site have to BE the report a
   parent receives, not a tasteful approximation of one, and the only way to
   guarantee that is to use the same CSS.

   Three deletions, and nothing else:
     * the @import of Besley and Familjen Grotesk from Google Fonts. This site
       self-hosts both already, and the CSP allows neither a remote stylesheet
       nor a remote font. The same two families load either way.
     * the @page block, which sets A4 margins and running footers for the PDF.
     * the @media print rules, for the same reason.

   Every selector is scoped under .rep, so none of it reaches the site's own
   styles and none of the site's styles reach it. DO NOT hand-edit these rules
   to make something sit better on a web page -- inventing a house style for
   the reports is exactly the mistake this replaced. If the report changes,
   re-copy the block.

   Regenerate the source from the engine repo with:
     python engine/render_monthly.py sample/demo_alex_taylor.xlsx reports/m.html 2026-01-05
   ========================================================================== */
.rep{ /* Tutora "schoolbook" — Besley serif, Familjen Grotesk for UI, paper + bottle green */
  --page:#FDFCFA; --surface:#FFFFFF; --ink:#20281F; --ink2:#4E584D;
  --muted:#6E7A69; --grid:#DBE1D3; --axis:#BAC4AE; --series:#14603A;
  --good:#1E7A4A; --warn:#B9852F; --crit:#A4321F; --border:#DBE1D3; --up:#14603A;
  --wash:#EEF2E9; --wash-warm:#F6F2E9; --green-deep:#16352A; --shadow:none;
  /* categorical palette — muted editorial hues that sit on paper (chart series only,
     distinct from the S/A/W status colours) */
  --cat1:#16352A; --cat2:#B9852F; --cat3:#3F6F53; --cat4:#7A5A34; --cat5:#8C4A3B; --cat6:#5B6E45;
  --font-serif:"Besley","Iowan Old Style",Georgia,serif;
  --font-ui:"Familjen Grotesk","Segoe UI",system-ui,sans-serif;
  font-family:var(--font-serif); color:var(--ink);
  background:var(--page); max-width:840px; margin:0 auto; padding:22px 20px 52px;
  line-height:1.6; font-size:15px; -webkit-font-smoothing:antialiased; }
/* Reports are print-style documents: always the light "paper" theme so the in-browser
   View matches the downloaded PDF even when the viewer's device is in dark mode. */
.rep[data-theme="light"]{ --page:#FDFCFA; --surface:#FFFFFF; --ink:#20281F; --ink2:#4E584D;
  --muted:#6E7A69; --grid:#DBE1D3; --axis:#BAC4AE; --series:#14603A; --good:#1E7A4A; --warn:#B9852F;
  --crit:#A4321F; --border:#DBE1D3; --up:#14603A; --wash:#EEF2E9; --green-deep:#16352A; --shadow:none; }
.rep h1{ font-family:var(--font-serif); font-size:28px; margin:0 0 3px; font-weight:600;
  letter-spacing:-.015em; line-height:1.12; }
.rep h2{ font-family:var(--font-serif); font-size:17px; letter-spacing:-.005em;
  color:var(--green-deep); margin:26px 0 10px; font-weight:600;
  padding-bottom:6px; border-bottom:1px solid var(--border); }
.rep .sub{ color:var(--ink2); font-size:14px; margin:0 0 4px; }
.rep .card{ background:var(--surface); border:1px solid var(--border); border-radius:12px;
  padding:13px 16px; margin:0 0 10px; box-shadow:var(--shadow); }
.rep .tiles{ display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:10px; }
.rep .tile{ background:var(--surface); border:1px solid var(--border); border-radius:12px;
  padding:12px 14px; box-shadow:var(--shadow); }
.rep .tile .k{ font-size:11px; color:var(--muted); text-transform:uppercase;
  letter-spacing:.04em; margin:0 0 4px; }
.rep .tile .v{ font-size:24px; font-weight:700; letter-spacing:-.02em; }
.rep .tile .d{ font-size:12px; margin-top:2px; }
.rep .up{ color:var(--up); font-weight:600; } .rep .dn{ color:var(--crit); font-weight:600; }
.rep .pill{ display:inline-block; padding:3px 10px; border-radius:999px; font-size:12px;
  font-weight:600; }
.rep .pill.good{ background:color-mix(in srgb,var(--good) 16%,transparent); color:var(--good); }
.rep .pill.warn{ background:color-mix(in srgb,var(--warn) 22%,transparent); color:#8a5a00; }
.rep .pill.crit{ background:color-mix(in srgb,var(--crit) 16%,transparent); color:var(--crit); }
.rep ul.clean{ list-style:none; padding:0; margin:0; }
.rep ul.clean li{ padding:9px 0; border-bottom:1px solid var(--border); }
.rep ul.clean li:last-child{ border-bottom:0; }
.rep .row{ display:flex; justify-content:space-between; gap:12px; align-items:baseline; }
.rep .tag{ font-size:11px; font-weight:700; padding:2px 7px; border-radius:6px; color:#fff; }
.rep .tag.S{ background:var(--good); } .rep .tag.A{ background:var(--warn); color:#3a2600; }
.rep .tag.W{ background:var(--crit); }
.rep .muted{ color:var(--muted); font-size:12px; }
.rep .why{ color:var(--ink2); font-size:14px; line-height:1.5; margin-top:3px; }
.rep .whymatters{ color:var(--ink); font-size:14px; margin-top:5px; line-height:1.5; }
.rep .cap{ color:var(--ink); font-size:14px; line-height:1.5; margin-top:11px; }
.rep .cap b{ font-weight:700; }
.rep .foot{ color:var(--muted); font-size:11px; margin-top:28px; border-top:1px solid var(--border);
  padding-top:12px; }
.rep .arrow{ color:var(--muted); }
.rep .grade{ font-size:40px; font-weight:800; letter-spacing:-.03em; line-height:1; }
.rep .traj{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-size:14px; }
.rep .traj b{ font-size:16px; }
.rep svg{ display:block; width:100%; height:auto; }
.rep .pipe{ display:flex; align-items:stretch; gap:8px; flex-wrap:wrap; }
.rep .stage{ flex:1 1 120px; background:var(--surface); border:1px solid var(--border);
  border-radius:10px; padding:11px 12px; position:relative; }
.rep .stage .sn{ font-size:26px; font-weight:800; letter-spacing:-.02em; line-height:1; }
.rep .stage .sl{ font-size:12px; font-weight:700; margin-top:5px; }
.rep .stage .ss{ font-size:11px; color:var(--muted); margin-top:2px; }
.rep .stage .sof{ font-size:11px; color:var(--muted); margin-top:6px; }
.rep .stage.final{ background:color-mix(in srgb,var(--series) 12%,var(--surface));
  border-color:color-mix(in srgb,var(--series) 40%,var(--border)); }
.rep .subgrid{ display:grid; grid-template-columns:1fr 1fr; gap:16px; }
@media (max-width:560px){ .rep .subgrid{ grid-template-columns:1fr; } }
.rep .legend{ display:flex; gap:14px; flex-wrap:wrap; font-size:11px; color:var(--ink2); margin-top:8px; }
.rep .legend i{ display:inline-block; width:10px; height:10px; border-radius:2px; margin-right:5px; vertical-align:-1px; }
.rep .summary{ font-size:14px; line-height:1.5; background:var(--surface); border:1px solid var(--border);
  border-left:4px solid var(--series); border-radius:12px; padding:14px 16px; margin:14px 0 4px;
  box-shadow:var(--shadow); }
.rep .caveat{ font-size:12px; color:var(--muted); margin:8px 2px 0; }
/* "vs last week" comparison strip on the at-a-glance block */
.rep .glancecmp{ display:flex; flex-wrap:wrap; gap:8px; margin:0 0 10px; }
.rep .gchip{ background:var(--surface); border:1px solid var(--border); box-shadow:var(--shadow);
  border-radius:10px; padding:8px 12px; min-width:96px; }
.rep .gchip .gk{ font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
.rep .gchip .gv{ font-size:15px; font-weight:700; letter-spacing:-.01em; margin-top:2px; }
.rep .gchip .gd{ display:block; font-size:11px; margin-top:2px; }
/* whole 'this week at a glance' block wrapped as one glowing, outlined box */
.rep .glancebox{ background:var(--surface); border:1px solid var(--border); border-radius:14px;
  padding:14px 16px 4px; margin:0 0 12px;
  box-shadow:0 0 0 1px color-mix(in srgb,var(--series) 24%,transparent),
             0 0 24px color-mix(in srgb,var(--series) 20%,transparent), var(--shadow); }
.rep .glancebox .glancecmp{ margin:0 0 12px; }
.rep .glancebox .gchip{ background:var(--page); box-shadow:none; }
.rep .glancebox .summary{ margin:0 0 10px; background:transparent; box-shadow:none;
  border:0; border-left:4px solid var(--series); border-radius:0; padding:2px 0 2px 13px; }
/* single 100% stacked bar + key for "where lost marks come from" */
.rep .stackbar{ display:flex; width:100%; height:26px; border-radius:6px; overflow:hidden;
  border:1px solid var(--border); }
.rep .stackseg{ height:100%; min-width:2px; }
.rep .stackkey{ display:flex; flex-wrap:wrap; gap:6px 16px; margin-top:12px; }
.rep .stackkey > span{ font-size:12px; color:var(--ink2); display:flex; align-items:center; gap:6px; }
.rep .stackkey i{ display:inline-block; width:11px; height:11px; border-radius:3px; flex:none; }
/* mastery card: Secure sub-note (exam-tested) */
.rep .msub{ font-size:12px; color:var(--muted); margin:2px 0 2px; padding-left:2px; }
/* mastery card: key under the single 100% stacked mastery bar */
.rep .mkey{ display:flex; flex-direction:column; gap:11px; margin-top:13px; }
.rep .mkey .mrow{ display:flex; gap:10px; align-items:flex-start; }
.rep .mkey .mswatch{ width:12px; height:12px; border-radius:3px; flex:none; margin-top:4px; }
.rep .mkey .mhd{ font-size:14px; font-weight:600; line-height:1.3; }
.rep .mkey .mhd .mcount{ font-weight:800; font-variant-numeric:tabular-nums; }
.rep .mkey .mdesc{ font-size:12px; color:var(--muted); margin-top:1px; }
/* exam-strategy focus/priority block */
.rep .fx{ padding:8px 0; border-bottom:1px solid var(--border); }
.rep .fx:last-child{ border-bottom:0; }
.rep .fx .hd{ display:flex; justify-content:space-between; gap:12px; align-items:baseline; }
.rep .fx .ttl{ font-weight:600; font-size:14px; }
.rep .chip{ font-size:10px; font-weight:600; padding:2px 6px; border-radius:5px; vertical-align:1px;
  margin-left:6px; background:color-mix(in srgb,var(--series) 12%,transparent); color:var(--series); }
.rep .chip.prac{ background:color-mix(in srgb,var(--warn) 18%,transparent); color:#8a5a00; }
.rep .fx .why{ margin-top:5px; }
.rep .meta{ display:flex; flex-wrap:wrap; gap:4px 16px; margin-top:7px; }
.rep .meta > span{ font-size:12px; color:var(--ink2); }
.rep .meta em{ font-style:normal; color:var(--muted); text-transform:uppercase; font-size:9px;
  letter-spacing:.05em; margin-right:5px; }
.rep .meta code{ font-family:ui-monospace,"Cascadia Code",Consolas,monospace; font-size:12px;
  background:color-mix(in srgb,var(--ink) 6%,transparent); padding:1px 5px; border-radius:4px; }
.rep .drill{ font-size:13px; margin-top:5px; color:var(--ink); }
.rep .drill b{ color:var(--series); }
.rep .ev{ font-size:13px; margin-top:4px; color:var(--ink2); }
.rep .ev em{ font-style:normal; color:var(--muted); text-transform:uppercase; font-size:9px;
  letter-spacing:.05em; margin-right:6px; }
.rep .pill.info{ background:color-mix(in srgb,var(--muted) 20%,transparent); color:var(--ink2); }
.rep .ssrow{ display:flex; gap:6px; align-items:center; font-variant-numeric:tabular-nums; }
.rep .dot{ display:inline-block; width:9px; height:9px; border-radius:2px; }
.rep .recbox{ display:flex; gap:10px; align-items:baseline; padding:9px 0; border-bottom:1px solid var(--border); }
.rep .recbox:last-child{ border-bottom:0; }
.rep .recnum{ font-size:13px; font-weight:800; color:var(--series); min-width:20px; }
.rep .cmp{ width:100%; border-collapse:collapse; font-size:13px; }
.rep .cmp th,.rep .cmp td{ text-align:right; padding:7px 8px; border-bottom:1px solid var(--border);
  font-variant-numeric:tabular-nums; }
.rep .cmp th:first-child,.rep .cmp td:first-child{ text-align:left; }
.rep .cmp thead th{ font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; font-weight:700; }
.rep .cmp tbody tr:last-child td{ border-bottom:0; font-weight:700; }
.rep .funnel{ display:flex; flex-direction:column; gap:10px; }
.rep .frow{ display:grid; grid-template-columns:150px 1fr 116px; gap:12px; align-items:center; }
.rep .flabel{ font-size:13px; font-weight:600; line-height:1.25; }
.rep .flabel small{ display:block; font-weight:400; color:var(--muted); font-size:11px; }
.rep .ftrack{ background:color-mix(in srgb,var(--series) 13%,transparent); border-radius:6px; height:24px; }
.rep .ffill{ background:var(--series); height:100%; border-radius:6px; min-width:4px; }
.rep .fval{ font-size:14px; font-weight:700; text-align:right; font-variant-numeric:tabular-nums; }
.rep .fval small{ display:block; font-weight:400; color:var(--muted); font-size:11px; }
.rep .mockchip{ display:flex; align-items:center; gap:12px; margin-top:14px; padding-top:12px;
  border-top:1px solid var(--border); }
.rep .mockchip .g{ font-size:30px; font-weight:800; letter-spacing:-.02em;
  color:var(--series); line-height:1; }
@media (max-width:560px){ .rep .frow{ grid-template-columns:112px 1fr 96px; } }
/* ---- Tutora skin: hairline structure over boxes, 4px radius, no SaaS shadows,
        Familjen Grotesk for UI chrome, and no caps-lock microlabels ---- */
.rep .card,.rep .tile,.rep .summary,.rep .glancebox,.rep .gchip,.rep .stage,
.rep .pill,.rep .tag,.rep .chip{ box-shadow:none; }
.rep .card,.rep .tile,.rep .summary,.rep .glancebox,.rep .gchip,.rep .stage{ border-radius:4px; }
.rep .pill{ border-radius:4px; }
.rep .tag,.rep .chip{ border-radius:3px; }
.rep .glancebox{ border:1px solid var(--border); background:var(--wash);
  border-left:3px solid var(--series); }
.rep .summary{ background:var(--wash); }
.rep .sub,.rep .tile .k,.rep .tile .d,.rep .k,.rep .gchip .gk,.rep .gchip .gv,.rep .gchip .gd,
.rep .pill,.rep .tag,.rep .chip,.rep .muted,.rep .caveat,.rep .foot,.rep .legend,
.rep .meta,.rep .meta em,.rep .ev em,.rep .cmp thead th,.rep .msub,.rep .mdesc,
.rep .stackkey,.rep .flabel small,.rep .fval small,.rep .stage .sl,.rep .stage .ss,
.rep .stage .sof{ font-family:var(--font-ui); }
.rep .tile .k,.rep .gchip .gk,.rep .meta em,.rep .ev em,.rep .cmp thead th{
  text-transform:none; letter-spacing:0; }
.rep .titlerule{ display:block; width:184px; height:8px; margin:1px 0 12px; }

/* --- MONTHLY_CSS: the monthly deep-dive's own additions --- */
.rep .caveat{ font-size:13.5px; color:var(--ink2); line-height:1.5; }
.rep .wkrow{ font-size:14.5px; line-height:1.55; padding:10px 0; border-bottom:1px solid var(--border); }
.rep .wkrow:last-child{ border-bottom:0; }
.rep .wklab{ color:var(--series); font-weight:800; }
/* Weakness-watch labels must NOT read as positive: red = stubborn (2+ months not
   secure), amber = new this month. Scoped modifiers so the green .wklab used by the
   "Where this is heading" section (projected grade / pace) is unaffected. */
.rep .wklab.crit{ color:var(--crit); }
.rep .wklab.warn{ color:#8a5a00; }
.rep .wkname{ color:var(--ink); font-weight:600; }
.rep .wkrow .muted{ font-size:13px; }/* ==========================================================================
   The Maths diagnostic
   --------------------------------------------------------------------------
   A room in the house, not an extension: every colour below is an existing
   token and there is no new typeface. Three aliases are added for the marks,
   named for their role rather than their hue, because "red" is not a thing this
   interface is ever allowed to say out loud.

   The signature is .dg-mark -- eight small squares that record ANSWERED while
   the questions run, then regroup into four rows of two on the results screen
   and carry the outcome. Sample size stays visible instead of being hidden
   behind a percentage. See docs/diagnostic-product-design.md.

   Structure comes from hairlines and space (DESIGN.md section 4). There is
   exactly one tinted surface inside the flow -- the selected answer -- and one
   gesture, the gold stroke under the results headline.
   ========================================================================== */

:root {
  --mark-correct: var(--pen-bright);
  --mark-missed: var(--error);
  --mark-empty: var(--rule);
}

/* --- Shell ---------------------------------------------------------------- */

.dg-wrap {
  padding: var(--space-4) 0 var(--space-5);
  background: var(--paper);
  overflow-x: clip;      /* lets the results bands bleed without a sideways scroll */
}

#diagnostic {
  max-width: 58ch;              /* measure governs, not the grid: this is prose */
  margin-inline: auto;
  padding-inline: clamp(20px, 5vw, 32px);
}

.dg-panel { position: relative; }

.dg-enter { animation: dg-in 160ms ease-out both; }
@keyframes dg-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.dg-live {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* --- Type ----------------------------------------------------------------- */

.dg-h1 {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: clamp(1.75rem, 5vw, 2.5rem);
  line-height: 1.15;
  letter-spacing: -0.005em;
  margin: 0 0 var(--space-2);
  text-wrap: balance;
}
.dg-h1:focus, .dg-h1:focus-visible { outline: none; }

.dg-h2 {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 1.25rem;
  line-height: 1.3;
  margin: 0 0 var(--space-2);
}

.dg-lead {
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 var(--space-3);
}

/* Sentence case, never a pill, never caps -- DESIGN.md section 6. */
.dg-eyebrow {
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin: 0 0 var(--space-1);
}

.dg-note, .dg-meta, .dg-count { font-family: var(--font-ui); }

.dg-honesty {
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--ink-soft);
  border-top: 1px solid var(--rule);
  padding-top: var(--space-2);
  margin: var(--space-3) 0 0;
}

/* Skipped questions. Same quiet register as the caveat above it -- this is a
   fact about the run, not a telling-off -- but it carries no second hairline,
   because two stacked rules read as two separate warnings. */
.dg-honesty--skipped {
  border-top: 0;
  padding-top: 0;
  margin-top: var(--space-1);
}
.dg-honesty--skipped em { font-style: normal; font-weight: 600; color: var(--ink); }

/* --- Notation -------------------------------------------------------------
   Hand-set, no library. The fraction bar is the page's own 1px rule doing
   arithmetic, which is the most subject-native detail in the design. */

.dg-panel var, .dg-taster var { font-style: italic; font-family: var(--font-serif); }

.dg-panel sup, .dg-panel sub, .dg-taster sup, .dg-taster sub {
  font-size: 0.7em;
  line-height: 0;                /* keeps indices off the line rhythm */
}

/* An inline-flex box takes its baseline from its FIRST flex item, so
   `vertical-align` here positions the NUMERATOR's baseline, not the box's
   bottom edge. A negative value therefore drops the whole fraction below the
   line -- which is what a full-size, 2.5em-tall box set at -0.45em did: the
   numerator landed on the text baseline and the denominator hung underneath,
   dragging the line box open with it. The value below is positive and set so
   that the bar lands on the maths axis, roughly mid x-height, where a
   typeset fraction sits. The cells are also set at 0.78em, which is what
   keeps a fraction from forcing the line it sits on taller than its
   neighbours. */
.mq-frac {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  vertical-align: 0.42em;
  text-align: center;
  margin: 0 0.18em;
}
.mq-frac > span {
  display: block;
  font-size: 0.78em;
  line-height: 1.1;
  padding: 0 0.3em;
}
.mq-frac > span:first-child { border-bottom: 1px solid currentColor; padding-bottom: 0.08em; }
.mq-frac > span:last-child  { padding-top: 0.08em; }

/* --- The marks: the signature --------------------------------------------- */

.dg-marks { display: flex; gap: 6px; align-items: flex-end; }

.dg-mark {
  width: 10px;
  height: 10px;
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  background: transparent;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--paper);
}

/* In-progress states are INK, not green.

   Green is the site's affirmative colour and it is what --mark-correct uses on
   the results screen. Wearing it during the set made a filled square read as
   "right", when all it records is "answered" -- a student could watch eight
   green squares appear and believe they had got eight questions right, then be
   told otherwise at the end. Nothing about the shape said so; only the colour
   did, and the colour was lying.

   So green now appears exactly once in this feature, and only after marking,
   where it means correct. Everything before that is ink. */
.dg-mark--done    { background: var(--ink); border-color: var(--ink); }
.dg-mark--todo    { background: transparent; border-color: var(--rule-strong); }
.dg-mark--current {
  background: transparent;
  border-color: var(--ink);
  box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 3px var(--ink);
  transform: translateY(-2px);
}

/* Results states. Never colour alone -- .dg-domain-band carries the word, and
   the missed mark carries a diagonal. */
.dg-mark--correct { background: var(--mark-correct); border-color: var(--mark-correct); }
.dg-mark--missed  { background: transparent; border-color: var(--mark-missed); color: var(--mark-missed); }
.dg-mark--empty   { background: transparent; border-color: var(--mark-empty); }
.dg-mark svg { width: 100%; height: 100%; display: block; }

/* --- Progress -------------------------------------------------------------- */

.dg-progress {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-1);
}
.dg-count { font-size: 0.875rem; color: var(--ink-soft); margin: 0; }

.dg-meta {
  font-size: 0.875rem;
  color: var(--ink-soft);
  margin: 0 0 var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}

/* --- Questions ------------------------------------------------------------- */

.dg-form { margin: 0; }

.dg-fieldset { border: 0; padding: 0; margin: 0 0 var(--space-3); min-width: 0; }

.dg-legend {
  font-family: var(--font-serif);
  font-size: clamp(1.25rem, 3.4vw, 1.4375rem);
  line-height: 1.5;
  color: var(--ink);
  padding: 0;
  margin-bottom: var(--space-3);
  display: block;
  width: 100%;
}
.dg-legend:focus, .dg-legend:focus-visible { outline: none; }

.dg-qnote {
  font-family: var(--font-ui);
  font-size: 0.875rem;
  color: var(--ink-soft);
  margin: -16px 0 var(--space-3);
}

.dg-options { display: flex; flex-direction: column; gap: var(--space-1); }

.dg-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 56px;              /* thumb, not cursor; past the WCAG 2.2 minimum */
  padding: 12px var(--space-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease;
}
.dg-option:hover { border-color: var(--rule-strong); background: var(--mist); }

/* Selected carries three signals, only one of them colour: the radio fills
   (shape), the ground tints, and the border strengthens. */
.dg-option.is-selected {
  border-color: var(--pen-bright);
  background: var(--mist);
}
/* Keyboard only. :focus-within also fires on a mouse click, which drew a second
   green ring on top of the .is-selected border. */
.dg-option:has(.dg-radio:focus-visible) {
  outline: 2px solid var(--pen-bright);
  outline-offset: 2px;
}

/* Real radios, sized up rather than replaced -- StemAdmit drew circles beside
   plain buttons and shipped a group no screen reader can perceive. */
.dg-radio {
  appearance: none;
  -webkit-appearance: none;
  width: 20px; height: 20px;
  margin: 0;
  flex: 0 0 auto;
  border: 1.5px solid var(--rule-strong);
  border-radius: 50%;
  background: var(--paper);
  display: grid;
  place-content: center;
  cursor: pointer;
}
.dg-radio::before {
  content: "";
  width: 10px; height: 10px;
  border-radius: 50%;
  transform: scale(0);
  background: var(--pen-bright);
  transition: transform 120ms ease;
}
.dg-radio:checked { border-color: var(--pen-bright); }
.dg-radio:checked::before { transform: scale(1); }

.dg-option-letter {
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-soft);
  flex: 0 0 auto;
  width: 1ch;
}
.dg-option.is-selected .dg-option-letter { color: var(--pen); }

.dg-option-text {
  font-family: var(--font-serif);
  font-size: 1.0625rem;
  line-height: 1.45;
  color: var(--ink);
}

/* --- Actions --------------------------------------------------------------- */

.dg-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
  margin-top: var(--space-3);
}
.dg-actions--split { justify-content: space-between; }

/* Skip sits with Continue, not opposite Back. Both are ways forward, and putting
   the quiet one next to the loud one makes the choice between them obvious --
   whereas across the row it reads as a second Back. */
.dg-actions-right {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}
.dg-skip { color: var(--ink-soft); }
.dg-skip:hover { color: var(--ink); }

@media (max-width: 480px) {
  /* Continue takes the width it needs; Skip tucks under rather than squeezing
     the two onto one cramped line. */
  .dg-actions--split { flex-wrap: wrap; row-gap: var(--space-2); }
  .dg-actions-right { width: 100%; justify-content: space-between; }
}

.dg-link {
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--pen);
  background: none;
  border: 0;
  padding: 12px 4px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.dg-link:hover { color: var(--green-deep); }

/* A disabled action states its own precondition; the label IS the validation
   message, so it never needs an error to appear underneath. */
.btn.dg-continue:disabled {
  background: transparent;
  color: var(--ink-soft);
  border-color: var(--rule-strong);
  cursor: not-allowed;
}

/* --- Pathway selection ------------------------------------------------------ */

.dg-pathways {
  display: flex;
  flex-direction: column;
  gap: 0;
  border-top: 1px solid var(--rule);
  margin: var(--space-3) 0 0;
}

/* Rows divided by rules, not cards floating on grey -- DESIGN.md section 4. */
.dg-pathway {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 84px;
  padding: var(--space-3) 4px;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  text-align: left;
  cursor: pointer;
  font: inherit;
  transition: background-color 140ms ease, padding-left 140ms ease;
}
.dg-pathway:last-of-type { border-bottom: 0; }
.dg-pathways + .dg-honesty { border-top: 0; margin-top: var(--space-2); }
.dg-pathway:hover { background: var(--mist); padding-left: 12px; }

.dg-pathway-title {
  font-family: var(--font-serif);
  font-size: 1.375rem;
  font-weight: 500;
  color: var(--ink);
  flex: 0 0 auto;
}
.dg-pathway-blurb {
  font-family: var(--font-ui);
  font-size: 0.875rem;
  color: var(--ink-soft);
  flex: 1 1 auto;
}
.dg-pathway-arrow { color: var(--pen); flex: 0 0 auto; display: inline-flex; }

/* --- Optional context ------------------------------------------------------- */

.dg-fieldset--inline { margin-bottom: var(--space-3); }
.dg-legend-small {
  font-family: var(--font-serif);
  font-size: 1.0625rem;
  color: var(--ink);
  padding: 0;
  margin-bottom: var(--space-1);
}
.dg-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); }

/* Not pills: 4px, like everything else. */
.dg-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  color: var(--ink-soft);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}
.dg-chip:hover { border-color: var(--rule-strong); }
.dg-chip.is-selected { border-color: var(--pen-bright); background: var(--mist); color: var(--ink); }
.dg-chip input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.dg-chip:focus-within { outline: 2px solid var(--pen-bright); outline-offset: 2px; }

/* --- Results ---------------------------------------------------------------- */

.dg-h1--result { margin-bottom: var(--space-2); }

/* The one gesture, spent once. Below the descender so it reads as an underline
   and not a strikethrough.
 *
 * Painted as a background rather than as a positioned child. The emphasised
 * phrase is inline and wraps -- on a 390px screen "number and proportion"
 * breaks across two lines -- and an absolutely-positioned element inside an
 * inline box follows only one fragment, so the stroke vanished. A background
 * with box-decoration-break: clone is drawn once per line fragment, which is
 * what a hand-drawn underline should do anyway. */
.dg-stroke {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 12' preserveAspectRatio='none'%3E%3Cpath d='M3 8 C 40 3, 78 10, 118 5 C 150 1, 178 7, 197 4' fill='none' stroke='%23B9852F' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.3em;
  padding-bottom: 0.1em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.dg-block {
  padding-top: var(--space-3);
  margin-top: var(--space-3);
  border-top: 1px solid var(--rule);
}

.dg-domains {
  padding-top: var(--space-3);
  margin-top: var(--space-4);
  border-top: 1px solid var(--rule);
}

.dg-domain {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name marks" "band band";
  gap: 4px var(--space-2);
  align-items: center;
  padding: 14px 0;
  border-bottom: 1px dotted var(--rule-strong);
  animation: dg-row 240ms ease-out both;
  animation-delay: calc(var(--i, 0) * 60ms);
}
@keyframes dg-row {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

.dg-domain-name {
  grid-area: name;
  font-family: var(--font-serif);
  font-size: 1.0625rem;
  color: var(--ink);
}
.dg-domain-marks { grid-area: marks; display: flex; gap: 6px; }
.dg-domain-marks .dg-mark { width: 15px; height: 15px; border-radius: 3px; }
.dg-domain-band {
  grid-area: band;
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  color: var(--ink-soft);
}
.dg-domain-band--confident { color: var(--pen); }
.dg-domain-count { color: var(--ink-soft); }

.dg-list { margin: 0; padding-left: 1.1em; }
.dg-list li { margin-bottom: var(--space-1); line-height: 1.6; }
.dg-list strong { font-weight: 600; }

.dg-recommend p { margin: 0; }

/* --- Review ------------------------------------------------------------------ */

/* --- The email step ---------------------------------------------------------
   Between the last question and the results. Kept deliberately plain: it is a
   gate, and dressing a gate up does not make it less of one. The honest note
   under the field does more for trust than any styling would. */
.dg-emailform { margin-top: var(--space-3); }
.dg-emailform .field { max-width: 26rem; }

.dg-emailnote {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 40rem;
  margin: var(--space-2) 0 0;
}

.dg-emailstatus {
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  color: var(--ink-soft);
  margin: var(--space-2) 0 0;
}
.dg-emailstatus.is-error { color: var(--error); }

/* The honeypot. Off-screen rather than display:none -- a bot that reads computed
   styles skips hidden fields, and the point is that it should fill this one in.
   aria-hidden and tabindex keep it away from anyone real. */
.dg-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Did the report actually send. Green for yes, but never green alone -- the
   sentence says which it is. */
.dg-sent {
  font-size: 0.9375rem;
  line-height: 1.6;
  margin: var(--space-2) 0 0;
  padding: 10px var(--space-2);
  border-left: 3px solid var(--pen-bright);
  background: var(--mist);
  color: var(--ink);
}
.dg-sent--failed {
  border-left-color: var(--error);
  background: var(--wash-warm);
}
.dg-sent a { color: inherit; }

/* --- The worked-through questions ------------------------------------------
   Eight of these run down the page, and the missed ones open by default with a
   prompt, a misconception and an explanation each. Separated only by hairlines
   they ran together into a single column of text with no way in.

   So each question becomes a card, and the two states are weighted DIFFERENTLY
   on purpose: a question you missed is the reason you are reading this page, so
   it sits raised on white with a red edge, while one you got right recedes into
   a flat tint. Emphasis follows what is worth your attention rather than being
   spread evenly across all eight.

   This is the one place in the feature that departs from the hairlines-and-space
   rule in DESIGN.md §4. It earns it: eight repeating blocks of mixed content are
   exactly the case that rule does not cover, and the alternative was a wall. */
.dg-review {
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--mist);
  margin-bottom: var(--space-2);
  overflow: hidden;                 /* keeps the head's corners inside the card */
  transition: background 0.15s ease, border-color 0.15s ease;
}
.dg-review:last-child { margin-bottom: 0; }

/* Missed: raised, and edged in the same red the mark uses. Never colour alone --
   the head still carries the word "Missed" and the mark still carries a cross. */
.dg-review--missed {
  background: var(--paper-raised);
  border-color: var(--rule-strong);
  border-left: 3px solid var(--mark-missed);
}

/* Skipped. Quieter than missed and quieter than correct -- it is the absence of
   a result rather than a result, so it should not compete with either. Dashed,
   because a dashed edge reads as "nothing here" in a way a solid one does not. */
.dg-review--skipped {
  background: transparent;
  border-style: dashed;
}
.dg-review--skipped .dg-review-n,
.dg-review--skipped .dg-review-skill { color: var(--ink-soft); }

.dg-review-head {
  display: flex;
  /* flex-start, not center: a long skill wraps to two lines, and centring left
     the mark and "Question 4" floating halfway down beside it. Top-aligned it
     reads as a deliberate row with a wrapped label. */
  align-items: flex-start;
  gap: var(--space-1);
  padding: 14px var(--space-2);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  line-height: 1.5;
}
/* Nudge the square onto the first line's optical centre rather than its top. */
.dg-review-head .dg-review-mark { margin-top: 5px; }
.dg-review-head::-webkit-details-marker { display: none; }
.dg-review-head:focus-visible { outline: 2px solid var(--pen-bright); outline-offset: -2px; }
.dg-review[open] .dg-review-head { border-bottom: 1px solid var(--rule); }

.dg-review-mark { width: 12px; height: 12px; flex: 0 0 auto; }
.dg-review-n {
  font-weight: 600;
  color: var(--ink);
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;   /* "Question 1".."Question 8" stay aligned */
}
.dg-review-skill { color: var(--ink-soft); flex: 1 1 auto; }
.dg-review-state {
  flex: 0 0 auto;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  padding: 2px 10px;
  background: var(--paper);
}
.dg-review--missed .dg-review-state {
  color: var(--mark-missed);
  border-color: var(--mark-missed);
  background: transparent;
}

.dg-review-body { padding: var(--space-2); }

/* The question itself, restated. Set apart so the eye can tell the QUESTION from
   the commentary about it without reading either first. No left rule any more --
   the card already frames this, and two frames inside each other was part of the
   clutter. */
.dg-review-prompt {
  font-family: var(--font-serif);
  font-size: 1.0625rem;
  color: var(--ink);
  margin: 0 0 var(--space-2);
  padding: var(--space-2);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.dg-review--missed .dg-review-prompt { background: var(--mist); }

/* The misconception is the single most useful line on the page -- it is the
   difference between "you got that wrong" and "here is what you did". Given warm
   paper so it reads as a note in the margin rather than more body copy. */
.dg-review-mis {
  color: var(--ink);
  margin: 0 0 var(--space-2);
  padding: var(--space-2);
  background: var(--wash-warm);
  border-radius: 8px;
}
.dg-review-mis strong {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 4px;
}
.dg-review-body > p:last-child { margin-bottom: 0; }

@media (max-width: 560px) {
  /* The head wraps to two lines rather than crushing the skill into a sliver:
     number and state on one, the skill underneath with room to be read. */
  .dg-review-head { flex-wrap: wrap; row-gap: 6px; }
  .dg-review-skill { flex: 1 0 100%; order: 3; }
  .dg-review-state { margin-left: auto; }
}

/* --- Tutora, and the call to action -------------------------------------------
   Colour arrives as a wash, not a card floating on white. */

/* Both of these run edge to edge. DESIGN.md §4: "Colour arrives as full-bleed
   washes, not as coloured cards floating on white." They were built as rounded
   cards inside the reading column, which is exactly the pattern all three
   reference sites use and exactly the one the brand guide rejects.

   The bleed is the standard percentage/vw pair -- the percentages resolve
   against #diagnostic's width, so the margin pulls each edge out to the
   viewport and the padding puts the text back on the measure. .dg-wrap carries
   overflow-x: clip so a scrollbar can never turn 100vw into a sideways scroll. */
.dg-tutora,
.dg-cta {
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
  border-radius: 0;
}

.dg-tutora {
  background: var(--wash);
  border-top: 0;
  padding-block: var(--space-4);
  margin-top: var(--space-5);
}
.dg-tutora p:last-child { margin-bottom: 0; }

.dg-cta {
  background: var(--green-deep);
  color: var(--on-deep);
  padding-block: var(--space-4);
  margin-top: 0;
  text-align: left;
}
.dg-cta .dg-h2 { color: var(--on-deep); }
.dg-cta p { color: var(--on-deep-soft); margin-bottom: var(--space-3); }

/* Another go. Moved out from under the consultation CTA -- where a reader has
   already stopped -- to the foot of the report itself, and given a border and
   real buttons so it reads as an offer rather than an afterthought. */
.dg-restart {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  background: var(--mist);
}

.dg-restart-lead {
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--space-2);
}

.dg-restart-acts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

/* The confirm drops in underneath rather than beside, so the question and its
   two answers stay on one reading line. */
.dg-restart .dg-confirm {
  flex-basis: 100%;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule-strong);
}
.dg-restart .dg-confirm p { margin: 0 0 var(--space-2); }

@media (max-width: 480px) {
  /* Full-width stacked buttons rather than two half-width ones with wrapped
     labels -- the same rule the mobile nav follows. */
  .dg-restart-acts { flex-direction: column; align-items: stretch; }
  .dg-restart-acts .btn { width: 100%; }
}
.dg-confirm {
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  padding: var(--space-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--mist);
}
.dg-confirm p { margin: 0; flex: 1 1 auto; font-size: 0.9375rem; }

.dg-failure { padding: var(--space-4) 0; }

/* The site footer opens with "Ready when you are." and a consultation button.
   On every other page that is the right closing note; here it lands directly
   under question three, competing with the diagnostic's own Continue and
   pre-empting the promise that nothing is asked for until the results are in.
   The footer's navigation, legal links and company details all stay. */
.dg-page .footer-cta { display: none; }

/* --- The landing-page taster --------------------------------------------------
   A real question, answerable in place, in the hero. It demonstrates the format
   instead of describing it.

   It IS marked, immediately, which is the opposite of what happens inside the
   diagnostic itself -- see docs/diagnostic-product-design.md 3 for why the run
   holds its marking to the end, and why one standalone question outside a run
   is a different case. Changed 23 Aug 2026.

   .dg-entry-inner is the #parent-view band further down the page (renamed
   from #try-it on 5 Sep, when it stopped being about the diagnostic at all):
   copy about the weekly report on the left, a preview of a real one on the
   right that opens in the site's own report-modal. */

.dg-entry-inner {
  display: grid;
  gap: var(--space-4);
  align-items: start;
}
/* The summary card is a good deal shorter than the copy beside it, and pinned
   to the top of the row it left a wedge of empty page under it. Centred, the
   two columns read as a pair. */
@media (min-width: 860px) {
  .dg-entry-inner .report-card { align-self: center; }
}

.dg-taster {
  position: relative;                /* the confetti layer pins to this */
  border: 1px solid var(--rule);
  border-top: 3px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper-raised);
  padding: var(--space-3);
  transition: border-color 200ms ease;
}

/* In the hero it is the figure, not a footnote: raised off the page with the
   same shadow the photo used to carry, so the column still has weight. */
.dg-taster--hero {
  box-shadow: 0 1px 2px rgba(32, 40, 31, 0.05), 0 22px 48px -26px rgba(32, 40, 31, 0.35);
}

.dg-taster-meta {
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin: 0 0 var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}
.dg-taster .dg-legend { font-size: 1.1875rem; margin-bottom: var(--space-2); }
.dg-taster .dg-option { min-height: 48px; }

/* Keyboard only, and only while the group has keyboard focus. Arrow keys move
   the selection here without committing it -- see js/taster.js -- so a sighted
   keyboard user needs telling how to commit, and nobody else needs the clutter.
   Screen readers get the same sentence from the legend instead, which is why
   this carries aria-hidden. */
.dg-taster-hint {
  margin: 10px 0 0;
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  color: var(--ink-soft);
  opacity: 0;
  transition: opacity 120ms ease;
}
.dg-options:has(.dg-radio:focus-visible) + .dg-taster-hint { opacity: 1; }
.dg-taster.is-answered .dg-taster-hint { display: none; }
/* "Not sure" on a question. A real bordered control, because as a text link
   beside a filled Continue button nobody saw it -- but deliberately lighter
   than Continue, because answering is still the point. */
.dg-skip.btn-quiet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 var(--space-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}
.dg-skip.btn-quiet:hover {
  border-color: var(--ink-soft);
  background: var(--mist);
  color: var(--ink);
}
.dg-skip.btn-quiet:focus-visible {
  outline: 2px solid var(--pen-bright);
  outline-offset: 2px;
}

.dg-taster-hint kbd {
  font-family: var(--font-ui);
  font-size: 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  padding: 1px 5px;
  background: var(--field-bg);
}

/* --- Marked ------------------------------------------------------------------
   The diagnostic proper never marks mid-run. This one question does, because it
   is not in a run -- see js/taster.js. Three signals, only one of them colour:
   the ground tints, the border changes, and a tick or a cross appears at the end
   of the row. The top edge of the whole card takes the verdict colour so the
   answer is legible from across the room. */

.dg-option-mark {
  margin-left: auto;
  flex: 0 0 auto;
  width: 18px; height: 18px;
  display: none;
}
.dg-option-mark svg { width: 100%; height: 100%; display: block; }
.dg-option.is-correct .dg-option-mark,
.dg-option.is-wrong .dg-option-mark { display: block; }

.dg-option.is-correct {
  border-color: var(--mark-correct);
  background: #EDF5F0;
  color: var(--pen);
}
.dg-option.is-correct .dg-option-letter,
.dg-option.is-correct .dg-option-mark { color: var(--mark-correct); }

.dg-option.is-wrong {
  border-color: var(--mark-missed);
  background: #FAF0ED;
}
.dg-option.is-wrong .dg-option-letter,
.dg-option.is-wrong .dg-option-mark { color: var(--mark-missed); }

/* The radio itself. It fills green on :checked, which is right everywhere else
   on the site and exactly wrong here -- a green dot sitting inside a row edged
   in red, next to a cross, is the one mixed signal this card can give. */
.dg-option.is-wrong .dg-radio { border-color: var(--mark-missed); }
.dg-option.is-wrong .dg-radio::before { background: var(--mark-missed); }

/* Answered: the radios are disabled, so say so with the cursor rather than by
   greying the text out -- the options still have to be readable, they are what
   the explanation refers to. */
.dg-taster.is-answered .dg-option { cursor: default; }
.dg-taster.is-answered .dg-option:hover {
  border-color: var(--rule);
  background: var(--paper);
}
.dg-taster.is-answered .dg-option.is-correct:hover {
  border-color: var(--mark-correct);
  background: #EDF5F0;
}
.dg-taster.is-answered .dg-option.is-wrong:hover {
  border-color: var(--mark-missed);
  background: #FAF0ED;
}
.dg-taster.is-answered .dg-radio:disabled { cursor: default; opacity: 1; }

.dg-taster--correct { border-top-color: var(--mark-correct); }
.dg-taster--wrong { border-top-color: var(--mark-missed); }

/* --- The reply ---------------------------------------------------------------- */

.dg-taster-reply {
  margin: var(--space-2) 0 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}
.dg-taster-reply[hidden] { display: none; }

.dg-taster-verdict {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 var(--space-1);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 1.0625rem;
}
.dg-taster-verdict-mark { width: 17px; height: 17px; flex: 0 0 auto; }
.dg-taster-verdict-mark svg { width: 100%; height: 100%; display: block; }
.dg-taster--correct .dg-taster-verdict { color: var(--mark-correct); }
.dg-taster--wrong .dg-taster-verdict { color: var(--mark-missed); }

/* What the chosen answer suggests was done. Set apart from the explanation and
   quoted off a rule, because it is about the visitor and the explanation is
   about the maths -- and because it must not read as a telling-off. */
.dg-taster-because {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-2);
  border-left: 2px solid var(--mark-missed);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--ink);
}

.dg-taster-why {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* Belt and braces against automatic fractions. The explanation is full of
   ratios -- 9/4, 3/2, 27/8 -- and a font that turns those into stacked
   sub/superscript pairs makes the one paragraph that has to be readable the
   least readable thing on the card. */
.dg-taster-reply { font-variant-numeric: normal; }
.dg-taster-reply sup { font-variant-numeric: normal; }

.dg-taster-skill {
  margin: var(--space-2) 0 0;
  padding-top: 14px;
  border-top: 1px dotted var(--rule);
  font-family: var(--font-ui);
  font-size: 0.875rem;
  color: var(--ink-soft);
}

/* No rule of its own. Unanswered there is one line above the buttons -- the
   options' own bottom edge -- and answered the skill line already draws a
   dotted one 24px higher; a second solid rule under it stacked two horizontal
   lines in the same breath and pushed the buttons off a 900px screen. */
.dg-taster-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}
.dg-taster.is-answered .dg-taster-foot {
  margin-top: var(--space-2);
  padding-top: 0;
  border-top: 0;
}

/* --- Confetti ------------------------------------------------------------------
   Thrown from the option that was right. Clipped to the card, removed when it
   lands, and never drawn at all for prefers-reduced-motion -- js/taster.js
   returns before building it, so this is belt and braces for anyone who flips
   the setting mid-flight. */

.dg-confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
  border-radius: var(--radius);
}
.dg-confetti-bit {
  position: absolute;
  display: block;
  border-radius: 1px;
  will-change: transform, opacity;
}
.dg-confetti-bit.is-round { border-radius: 50%; }

@media (prefers-reduced-motion: reduce) {
  .dg-confetti { display: none; }
  .dg-taster { transition: none; }
}

.dg-entry-ctas { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }

@media (min-width: 860px) {
  .dg-entry-inner { grid-template-columns: 1fr 1fr; gap: var(--space-5); }
}

/* --- Motion ------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .dg-enter, .dg-domain { animation: none; }
  .dg-option, .dg-radio::before, .dg-pathway, .dg-chip { transition: none; }
  .dg-pathway:hover { padding-left: 4px; }
}

/* --- Narrow phones -------------------------------------------------------------
   The rule from tools/README.md: one item that refuses to shrink pushes the
   layout viewport past device-width and the browser scales the whole page down.
   Everything here is allowed to shrink. */

@media (max-width: 400px) {
  .dg-pathway { flex-wrap: wrap; min-height: 0; padding: var(--space-2) 4px; }
  .dg-pathway-title { flex: 1 0 100%; }
  .dg-pathway-blurb { flex: 1 1 auto; }
  .dg-option { padding: 10px 12px; gap: 10px; }
  .dg-review-head { flex-wrap: wrap; }
  .dg-review-skill { flex: 1 0 100%; order: 3; padding-left: 20px; }
}

/* A consultation whose family has not signed up yet. The Create child button is
   disabled and the reason used to live only in a title attribute, so pressing
   it looked like a broken button. */
.consult-waiting {
  display: block;
  flex: 1 1 100%;
  margin-top: var(--space-1);
  padding: 10px var(--space-2);
  border-left: 2px solid var(--gold);
  background: var(--gold-soft);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--ink);
}

/* Message your tutor. Sits under the tutor's details on both dashboards, with
   the icon and label on one line however long the tutor's first name is. */
.tutor-msg-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--space-2);
}
.tutor-msg-btn svg { flex: 0 0 auto; }

/* The consultation brief, on the tracker's setup form. Quiet paper card rather
   than a coloured alert: it is context the tutor reads once, not something that
   needs acting on. */
.ob-brief {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--mist);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--pen-bright);
  border-radius: var(--radius);
}
.ob-brief-head {
  margin: 0 0 8px;
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--pen);
}
.ob-brief-row {
  margin: 0 0 8px;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--ink);
}
.ob-brief-row:last-child { margin-bottom: 0; }
.ob-brief-row span {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  color: var(--ink-soft);
}

/* Written feedback, under the stars. Appears once a rating is given -- see
   js/main.js. Separated from the stars by a rule so it reads as a second,
   optional step rather than part of the tap. */
.rate-note {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}
.rate-note label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-ui);
  font-size: 0.875rem;
  font-weight: 600;
}
.rate-note textarea {
  width: 100%;
  min-height: 72px;
  padding: 10px 12px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  background: var(--field-bg);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--ink);
  resize: vertical;
}
.rate-note textarea:focus-visible {
  outline: 2px solid var(--pen-bright);
  outline-offset: 1px;
}
.rate-note textarea:disabled { opacity: 0.7; }
.rate-note-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

/* ---- One live bar on the overview doorway cards ------------------------- */
.latest-topic {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}
.latest-topic-head {
  margin: 0 0 6px;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.latest-topic .tracker-topic { margin-bottom: 0; }
.latest-topic-link { margin: var(--space-2) 0 0; }

/* ---- Search, sort and scroll cap on the full tracker -------------------- */
.trk-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
}
.trk-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 180px;
  min-width: 0;                 /* or the flex item refuses to shrink and the
                                   card pushes the page sideways on a phone */
}
.trk-search svg {
  position: absolute;
  left: 10px;
  color: var(--ink-soft);
  pointer-events: none;
}
.trk-search input {
  width: 100%;
  min-width: 0;
  padding: 9px 12px 9px 31px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  background: var(--field-bg);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  color: var(--ink);
}
.trk-sort { flex: 0 1 auto; min-width: 0; }
.trk-sort select {
  width: 100%;
  min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  background: var(--field-bg);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  color: var(--ink);
}
.trk-search input:focus-visible,
.trk-sort select:focus-visible {
  outline: 2px solid var(--pen-bright);
  outline-offset: 1px;
}

/* The cap. A Further Maths course would otherwise run the card down the page.
   .list-scroll is the same thing for any admin list that only ever grows --
   Cancelled after payment is the first. Shared rather than copied: two
   near-identical scroll treatments drift, and the fade is the only thing
   telling anyone there is more below. */
.trk-scroll, .list-scroll {
  position: relative;
  max-height: 460px;
  overflow-y: auto;
  padding-right: 4px;
  overscroll-behavior: contain;   /* stop a flick inside the list scrolling the
                                     whole page once it hits the end */
}
/* The only thing saying "there is more below". Removed at the end of the list
   by syncFade(), or it becomes decoration and stops meaning anything.
   position:sticky rather than a fixed overlay so it rides the scroll without
   needing a wrapper that would clip the bars' own labels. */
.trk-scroll.has-more::after, .list-scroll.has-more::after {
  content: "";
  position: sticky;
  display: block;
  bottom: 0;
  height: 24px;
  margin-top: -24px;
  pointer-events: none;
  /* Short on purpose. At 36px it swallowed a whole topic row, which read as
     disabled content rather than as "there is more below". */
  background: linear-gradient(to bottom, rgba(253, 252, 250, 0), var(--paper-raised));
}
/* Shorter than the tracker's: these rows are two lines, not a bar plus labels. */
.list-scroll { max-height: 330px; }
/* The search sits above the scroller, not inside it, so it stays put. */
.cp-search { margin-bottom: var(--space-2); }

.trk-count {
  margin: var(--space-2) 0 0;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 560px) {
  .trk-controls { flex-direction: column; }
  .trk-search, .trk-sort { flex: 1 1 auto; width: 100%; }
  .trk-scroll, .list-scroll { max-height: 380px; }
}

/* The weekly note inside "Log a lesson". Its state line, in three shapes --
   already written, another lesson later this week, or nothing to say. */
.wk-state {
  margin: 0 0 8px;
  padding: 8px 12px;
  border-radius: var(--radius);
  border-left: 2px solid var(--rule-strong);
  background: var(--mist);
  font-size: 0.875rem;
  line-height: 1.5;
}
.wk-state--done {
  border-left-color: var(--pen-bright);
  background: #EDF5F0;
  color: var(--ink);
}
.wk-state--later {
  border-left-color: var(--gold);
  background: var(--gold-soft);
  color: var(--ink);
}
#sl-week-note {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  background: var(--field-bg);
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--ink);
  resize: vertical;
}
#sl-week-note:read-only {
  background: var(--mist);
  color: var(--ink-soft);
  cursor: default;
}
#sl-week-note:focus-visible {
  outline: 2px solid var(--pen-bright);
  outline-offset: 1px;
}

/* --------------------------------------------------------------------------
   Cookie consent banner
   Only ever shown on public marketing pages -- see js/consent.js.

   The two buttons are deliberately the same size and the same weight. The ICO
   has enforced against greyed-out reject buttons and against hiding "no"
   behind a preferences menu, and beyond the law it is simply the difference
   between asking a question and pretending to.
   -------------------------------------------------------------------------- */

.consent {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 200;
  background: var(--paper-raised);
  border-top: 1px solid var(--rule);
  box-shadow: 0 -8px 32px -16px rgba(32, 40, 31, 0.28);
}
.consent[hidden] { display: none; }

.consent-inner {
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.consent-copy { flex: 1 1 340px; min-width: 0; }

.consent-title {
  margin: 0 0 4px;
  font-family: var(--font-ui);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
}
.consent-title:focus-visible { outline: none; }   /* focused for screen readers, not decoration */

.consent-text {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--ink-soft);
}
.consent-text a { color: var(--pen); }

.consent-actions {
  display: flex;
  gap: var(--space-2);
  flex: 0 0 auto;
}
/* Same width, so neither reads as the default. */
.consent-btn { min-width: 120px; justify-content: center; }

@media (max-width: 620px) {
  .consent-inner { padding: var(--space-2); gap: var(--space-2); }
  .consent-actions { flex: 1 1 100%; }
  .consent-btn { flex: 1 1 0; min-width: 0; }
}

/* Keep the banner from sitting on top of the last line of the page. */
body.has-consent-banner { padding-bottom: 160px; }
@media (min-width: 620px) { body.has-consent-banner { padding-bottom: 110px; } }

/* A <button> that has to sit among the footer's anchors without looking like a
   button. Withdrawal has to be as easy as consent, so it lives in the footer of
   every page. */
.link-plain {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.link-plain:hover { color: var(--on-deep); }

/* ── Figures ─────────────────────────────────────────────────────────────
   Generated inline SVG from js/diagnostic-figures.js. No colour is pinned
   here — every stroke and fill reads a site token (--pen, --rule, --ink),
   so a figure is drawn in the brand's palette rather than in a second one
   invented for graphs, and it follows any later change to those tokens.

   The site is light-only today: css/styles.css defines no
   prefers-color-scheme block. Using tokens is therefore not a dark-mode
   claim, it is what makes a dark mode possible later without revisiting
   this file. The one thing genuinely fixed here is the aspect ratio, which
   the viewBox carries. */

.dg-figure {
  margin: var(--space-3) 0;
  padding: var(--space-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
}

/* The SVG shrinks with its container rather than forcing a minimum width.
   A figure that refuses to shrink is exactly the item that pushes the layout
   viewport past device-width and makes the browser scale the whole page
   down -- the failure tools/README.md warns about. */
.dg-fig {
  display: block;
  width: 100%;
  height: auto;
  max-width: 480px;
  margin: 0 auto;
  color: var(--ink);
  overflow: visible;
}

.dg-fig-grid   { stroke: var(--rule); stroke-width: 1; }
.dg-fig-axis   { stroke: var(--rule-strong); stroke-width: 1.5; }
.dg-fig-curve  { fill: none; stroke: var(--pen); stroke-width: 2.5;
                 stroke-linejoin: round; stroke-linecap: round; }
.dg-fig-line   { stroke: var(--pen-bright); stroke-width: 2; }
.dg-fig-line.is-dashed { stroke-dasharray: 6 4; }
.dg-fig-arrow  { stroke: var(--pen-bright); stroke-width: 2; }
.dg-fig-point  { fill: var(--pen-bright); stroke: var(--paper); stroke-width: 2; }
.dg-fig-point.is-hollow { fill: var(--paper); stroke: var(--pen-bright); }
.dg-fig-shade  { fill: var(--pen-bright); opacity: 0.14; stroke: none; }
.dg-fig-pulley { fill: none; stroke: var(--ink-soft); stroke-width: 2.5; }
.dg-fig-body   { fill: var(--mist); stroke: var(--ink-soft); stroke-width: 2; }

/* Figure text is interface chrome, not prose, so it takes the UI face. The
   sizes are in viewBox units, NOT screen pixels, and that distinction is the
   whole reason they look large here: the 480-unit box renders about 280px
   wide inside a 320px phone, so everything in it is scaled by roughly 0.6.
   13 units — a sensible-looking number — arrives on the phone at under 8px
   and cannot be read. These sizes are chosen backwards from the narrow case:
   18 units lands near 11px at 320px wide, and full size on a desktop. */
.dg-fig-label,
.dg-fig-note,
.dg-fig-tick {
  font-family: var(--font-ui);
  fill: var(--ink-soft);
  font-size: 18px;
}
.dg-fig-tick { font-size: 16px; }
.dg-fig-note { fill: var(--ink); font-weight: 600; }

@media (max-width: 400px) {
  .dg-figure { padding: var(--space-1); }
}

/* ── The admin calls agenda ───────────────────────────────────────────────
   admin-calls.html. Deliberately NOT the .consult-row treatment on the admin
   overview: that row is a form you fill in, so a box around it is right. This
   is a list you scan, so it takes the schoolbook's other primitive -- a dotted
   rule between rows and nothing else. Boxes here would make an agenda of six
   calls look like six unfinished tasks. */

.call-day {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: var(--space-3) 0 var(--space-1);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--rule);
}
.call-day:first-child { margin-top: 0; }

.call-row {
  display: grid;
  /* The time column is fixed so every start time lines up down the page --
     that vertical edge is what makes a list of times readable at a glance. */
  /* Wide enough for "11:30-12:00pm" on one line. At 5.5rem every single row
     wrapped its time onto two lines, which doubled the vertical rhythm of the
     list for no information. */
  grid-template-columns: 7.5rem 1fr auto;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-2) 0;
  border-bottom: 1px dotted var(--rule);
}
.call-row:last-child { border-bottom: 0; }

.call-time {
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
}
.call-who p.note { margin: 2px 0 0; }
.call-enquiry { max-width: 52ch; }

.call-act { justify-self: end; align-self: center; text-align: right; }
.call-act .note { display: block; max-width: 24ch; }

.call-row--past { opacity: 0.72; }
/* The status select is a live control, not part of the historical record --
   Dylan, 6 Sep, wanted past calls editable again, and a control dimmed to
   72% reads as disabled. Full opacity for it alone; the row around it stays
   dimmed as the "this already happened" signal. */
.call-row--past .call-status-select {
  opacity: 1;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  padding: 4px 8px;
  border-radius: 6px;
  border: 1px solid var(--rule-strong);
  background: var(--paper-raised);
  color: var(--ink);
}

/* A search box that reads as one thing, not as a labelled form field in the
   middle of a card. */
.field--search { margin: 0 0 var(--space-2); }
.field--search input {
  width: 100%;
  font-family: var(--font-ui);
  font-size: 0.95rem;
}

/* ── Phone ─────────────────────────────────────────────────────────────
   The three columns become two rows. The time keeps its own line with the
   action beside it, and the name and contact details run full width beneath --
   squeezing a 5.5rem time column, a name, an email and a button into 390px
   gives four unreadable columns instead of one readable block. */
@media (max-width: 640px) {
  .call-row {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "time act"
      "who  who";
    row-gap: 6px;
    align-items: center;
  }
  .call-time { grid-area: time; }
  .call-act  { grid-area: act; }
  .call-who  { grid-area: who; }
  .call-act .note { max-width: none; text-align: left; }
  .call-enquiry { max-width: none; }
}

/* ── Signup: who is this account for ──────────────────────────────────────
   signup.html. Deliberately NOT the .choices treatment used by the tutor's
   cancel form: that one is a bare list of two words, and this one carries a
   line of explanation under each option that decides whether somebody picks
   the right thing. A parent skim-reading needs to know at a glance that "I'm
   the student" is not for them. */

.signup-who {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-2);
}
.signup-who legend {
  font-family: var(--font-ui);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  padding: 0;
  margin-bottom: 10px;
}
.signup-who .choice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  margin-bottom: 8px;
  /* A hairline box here rather than the usual bare rule: these two are a
     mutually exclusive pick, and the border is what makes the selected one
     read as chosen rather than merely nearest the cursor. */
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  cursor: pointer;
  font-family: var(--font-ui);
  line-height: 1.45;
}
.signup-who .choice:hover { border-color: var(--ink-soft); }
.signup-who .choice:has(input:checked) {
  border-color: var(--pen);
  background: var(--wash);
}
.signup-who .choice input { margin-top: 3px; flex: 0 0 auto; }
.signup-who .choice > span { display: block; }
.signup-who .choice strong { display: block; font-size: 0.98rem; }
.signup-who .choice .note { display: block; margin: 3px 0 0; }

/* The age confirmation. A wash rather than a plain checkbox because it is the
   one thing on this form with a consequence beyond the form -- it is what the
   terms rely on -- and it should not read as another optional preference
   sitting between the password and the submit button. */
.age-gate {
  padding: 12px 14px;
  margin: 0 0 var(--space-2);
  background: var(--wash-warm);
  border-radius: var(--radius);
}
.age-gate .remember { margin: 0; font-weight: 500; }
.age-gate .hint { margin: 8px 0 0; }

/* ── Linked accounts ──────────────────────────────────────────────────────
   dashboard.html, the card at the foot. A student aged 16+ holds their own
   account; this is where they hand their parent sight of it, and where a
   parent asks for it. See migration-account-links.sql.

   No boxes. This is a list of relationships you read once and then leave
   alone -- two or three rows at the very most -- so it takes the schoolbook's
   list treatment: a dotted rule between rows and the address doing the work.
   A card per person here would make three people look like three tasks. */

.link-head {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: var(--space-3) 0 0;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--rule);
}

.link-list { list-style: none; margin: 0 0 var(--space-2); padding: 0; }

.link-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-2) 0;
  border-bottom: 1px dotted var(--rule);
}
.link-row:last-child { border-bottom: 0; }

.link-who { min-width: 0; }
/* The email is the identity here -- it is what was typed and what the other
   person has to recognise -- so it gets the ink and stays whole. */
.link-who strong { display: block; font-weight: 500; overflow-wrap: anywhere; }
.link-who .note { margin: 2px 0 0; }

.link-act { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }

/* One email field, not a grid, so it must not stretch the full 1100px the way
   a two-column form's halves do. An input four times longer than anything you
   would type into it reads as a paste area. */
#linkInviteForm .field { max-width: 30rem; }

/* At 390px the address and the buttons stop competing for one line: the row
   becomes two, address first. Squeezing an email and two buttons side by side
   truncates the one thing on the row that has to be read exactly. */
@media (max-width: 560px) {
  .link-row { flex-direction: column; align-items: stretch; }
  .link-act { justify-content: flex-start; }
}

/* ── Read-only: looking at a linked student's account ─────────────────────
   A parent who has accepted an account link sees that student's learner in
   their own switcher. The database gives them SELECT and nothing else, so
   every control that writes has to come off the page -- a Cancel button that
   is offered and then refused is worse than no button.

   CSS rather than JS-set .hidden, deliberately. These controls are re-rendered
   and re-shown by their own renderers (removeChildBtn is un-hidden at the very
   end of loadChildData; the cancel buttons live inside rows rebuilt on every
   refresh), so anything set once here would be quietly undone by code that has
   no idea this mode exists. An author display rule outlives all of it, and
   beats the `hidden` attribute's UA rule in both directions. */

html.is-observer [data-observer-hide],
/* Per-row controls, which are markup built in js/main.js rather than in the
   page, so they are matched by what they DO rather than by a class. */
html.is-observer [data-session-confirm],
html.is-observer [data-session-decline],
html.is-observer [data-cancel-session],
html.is-observer [data-cancel-series],
html.is-observer [data-cancel-lesson],
html.is-observer [data-cancel-lesson-series],
html.is-observer [data-cancel-form],
html.is-observer [data-dispute-payment],
/* The reschedule controls. These were MISSED on the first pass and shipped
   live: an observer looking at a lesson with a tutor-proposed time change got
   working Approve / Decline / Change time / Withdraw buttons. The RPCs refused
   them, so nothing landed -- which is exactly the "offered and then refused"
   failure the comment above says must not happen.

   Worth being honest about why: a list of attributes FAILS OPEN. Every write
   control added after this rule is written is visible to an observer until
   somebody remembers to come back here. The list is kept because it is what
   makes the page look right, but it is no longer what makes the page SAFE --
   see canWriteCurrentChild() in js/main.js, which refuses at the point of
   action and fails closed. */
html.is-observer [data-resched-approve],
html.is-observer [data-resched-decline],
html.is-observer [data-resched-withdraw],
html.is-observer [data-resched-open],
html.is-observer [data-resched-cancel],
html.is-observer [data-resched-form] {
  display: none;
}

/* The Payments link in the nav, while observing.
   Matched by HREF rather than by an attribute: this link exists twice over --
   written into the markup of every parent-portal page, and injected again by
   renderPortalNav() for pages that shipped without it. Tagging both by hand is
   two places to forget; the href is the one thing they share.

   Note this is about not OFFERING it. billing.html is scoped to whoever is
   looking, so a linked adult opening it directly sees their own card and their
   own charges, never the student's -- there is no grant that would let them.
   It comes back the moment they select one of their own children. */
html.is-observer .site-nav a[href="billing.html"] { display: none; }

/* The explanation. A wash, not a coloured box: it is a standing condition of
   the page you are on, not a warning about something that went wrong. */
.observer-note {
  margin: 0;
  padding: 12px 16px;
  background: var(--wash-warm);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font-family: var(--font-ui);
  font-size: 0.94rem;
  line-height: 1.5;
}

/* The human check on the login and sign-up forms. Only this one gets the
   spacing: the consultation form's widget sits between a textarea and its
   button and already has the field margin above it, so a shared rule would
   move a page that has been live and verified since 24 Aug.

   The widget draws nothing at all when no site key is configured, and an
   element with no content takes no height -- so this margin costs nothing in
   that state rather than leaving a 24px gap where a box would have been. */
#auth-turnstile:not(:empty) { margin-top: var(--space-3); }

/* ── The tracker in the hero, the taster on the deep band ─────────────────
   26 Aug. The page led with a maths question and followed it with a section
   about a diagnostic, which read as a past-paper service rather than a
   tutoring one. The two swapped: the first thing on the page is now the
   product working, and the question sits further down where it reads as a
   demonstration of what the section above it just described.

   Both cards move between a light background and a dark one, so both need
   their surroundings re-tuned. Nothing about the panels themselves changes --
   they are still the portal's own components. */

/* The hero's figure column is the narrower half (5fr against 7fr), which was
   right for a four-option question and is tight for a carousel panel carrying
   a table of topic bars. Even them up when the figure is the tracker. */
/* INSIDE the breakpoint, not above it. This rule lives at the end of the file,
   after `@media (max-width: 960px) { .hero-inner { grid-template-columns: 1fr } }`
   -- so unscoped it won that fight and the hero stayed two columns on a phone.
   At 390px that put the h1 and a table of topic bars side by side, each about
   170px wide: "Geometry and measures" broke onto three lines and its bar
   segments read 20% / 16% / 2% in boxes too narrow for the digits. */
@media (min-width: 961px) {
  .hero-inner:has(.hero-figure--tracker) {
    grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
    align-items: center;
  }
}

/* On a phone the figure follows the copy, and the label sits with it rather
   than floating off to one side above a full-width card. */
@media (max-width: 960px) {
  /* minmax(0, 1fr), NOT the 1fr the stacking rule above uses. Exactly the trap
     already documented on .tracker: `1fr` is minmax(auto, 1fr), an auto minimum
     is the content's minimum, and the carousel track is five slides at 100%
     each laid out in a row -- so the column grew to five screens wide and took
     the h1 and the lead off the right-hand edge with it. The tracker section
     hit this once already; moving the carousel into the hero brought it along.

     Any grid or flex child holding something wider than itself needs the floor
     set. Same family as the min-width:auto trap from the mobile pass. */
  .hero-inner:has(.hero-figure--tracker) { grid-template-columns: minmax(0, 1fr); }
  /* margin-inline: auto -- Dylan, 6 Sep, iPad: the grid cell is full width once
     stacked to one column, and a max-width with no centring sits flush at the
     cell's start edge (left) with the rest of the width sitting empty beside
     it. Invisible on a phone, where 520px exceeds the viewport anyway and the
     cap never binds; a tablet is exactly the width where it does. */
  /* ⚠️ width: 100% IS LOad-BEARING, AND ITS ABSENCE WAS A LIVE BUG.
     `margin-inline: auto` on a GRID ITEM switches it out of stretch sizing:
     the spec says an auto inline margin means the item is sized to fit-content
     and the margins absorb what is left. So this box stopped filling its track
     and took its max-content width instead -- 520px -- inside a 350px cell on
     a 390px phone. .hero overflows hidden, so it did not scroll, it CLIPPED:
     Dylan's screenshot has the topic bars sliced off mid-percentage.

     The note below was written when the auto margin went in and says the cap
     "never binds" on a phone. True of the CAP; the auto margin was doing
     something else entirely.

     width: 100% restores stretch, and centring still works above 520: the
     width resolves to the track, max-width trims it to 520, and the auto
     margins share the remainder. Correct at every size, which is what the
     bare max-width was reaching for. */
  .hero-figure--tracker {
    width: 100%;
    max-width: 520px;
    min-width: 0;
    margin-inline: auto;
  }
  .hero-figure-foot { flex-direction: column; gap: 6px; }
}

.hero-figure-foot {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: var(--space-2) 0 0;
}
.hero-figure-foot .note { margin: 0; }

/* The panel shadow was built to lift a white card off deep green: 60px of
   blur at 45% black. On paper that reads as a smudge rather than a lift, so
   it comes down to the hero's own card treatment and picks up a hairline,
   which is what actually separates paper from paper. */
.hero-figure--tracker .tracker-panel {
  border: 1px solid var(--rule);
  box-shadow: 0 1px 2px rgba(32, 40, 31, 0.05), 0 22px 48px -26px rgba(32, 40, 31, 0.35);
}

/* The arrows and dots were mint -- a colour picked to sit on deep green. On
   paper they were very nearly invisible, which is the kind of thing that reads
   as "the carousel is broken" rather than "I cannot see the control". */
.hero-figure--tracker .carousel-arrow {
  color: var(--pen);
  border-color: var(--rule-strong);
}
.hero-figure--tracker .carousel-arrow:hover {
  background: var(--wash);
  border-color: var(--pen);
}
.hero-figure--tracker .carousel-dot { background: var(--rule-strong); }
.hero-figure--tracker .carousel-dot.is-on { background: var(--pen); }

/* The taster, now on the deep band. Same trap the tracker panels hit: the card
   is white, but `.section--deep p` paints every paragraph inside it in a colour
   chosen for deep green, which on paper is almost invisible. */
.section--deep .dg-taster p,
.section--deep .dg-taster .dg-taster-meta { color: var(--ink-soft); }
.section--deep .dg-taster .dg-legend,
.section--deep .dg-taster .dg-option-text { color: var(--ink); }

.dg-taster--deep {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06), 0 30px 60px -30px rgba(0, 0, 0, 0.45);
}


.rail-toggle { display: none; }

/* ===========================================================================
   PORTAL SIDEBAR
   ===========================================================================
   The signed-in pages get a fixed green rail down the left instead of the
   marketing site's top bar. Everything below is presentation: no page markup
   changes, no JS changes, nothing that loads data or touches a form.

   ⭐ HOW IT ATTACHES, AND WHY THERE IS NO HTML TO EDIT. Every one of the 17
   portal pages already carries `<main data-portal="...">` and an identical
   `<header class="site-header">` shell whose links are generated by
   renderPortalNav(). So `body:has(main[data-portal])` is a reliable "this is a
   signed-in page" hook that already exists on all of them. :has() is used a
   dozen times elsewhere in this file; it is not a new dependency.

   ⭐ DESKTOP ONLY, DELIBERATELY. It lives inside min-width: 1101px, which is
   exactly where the existing mobile drawer takes over (the max-width: 1100px
   block above turns .site-nav into the stacked menu behind .nav-toggle). So
   the phone keeps precisely the navigation it has today and this cannot break
   it. A 248px rail on a 390px screen would be most of the screen.
   -------------------------------------------------------------------------*/
@media (min-width: 1101px) {

  /* 204, not 248. The rail holds seven one-word labels and an 18px icon; the
     extra 44px was margin nobody was reading, taken off a content column that
     has admin tables in it. */
  body:has(main[data-portal]) { --sidebar-w: 204px; }

  /* renderPortalNav injects the toggle at every width, so the base rule above
     this media query hides it and this switches it on beside the rail it
     controls. Without that, a phone gets an unstyled button in its header. */
  body:has(main[data-portal]) .rail-toggle { display: flex; }

  /* Collapsed: icons only. */
  html.rail-min body:has(main[data-portal]) { --sidebar-w: 64px; }

  body:has(main[data-portal]) .site-header {
    position: fixed;
    top: 0; left: 0; bottom: 0;
    width: var(--sidebar-w);
    height: 100vh;
    background: var(--green-deep);
    /* The marketing header is a translucent blur over scrolling content. The
       rail is opaque and nothing passes under it, so the blur is pure cost. */
    backdrop-filter: none;
    border-bottom: 0;
    border-right: 1px solid rgba(245, 244, 236, 0.10);
    z-index: 90;
  }

  /* .header-inner is also .container, so it arrives with a 1100px max-width,
     auto side margins and clamped padding. All three have to go or the rail's
     contents sit in a centred column 248px wide. */
  body:has(main[data-portal]) .header-inner {
    max-width: none;
    margin-inline: 0;
    padding: 26px 14px 20px;
    height: 100%;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
  }

  body:has(main[data-portal]) .logo,
  body:has(main[data-portal]) .logo:hover {
    color: var(--on-deep);
    padding: 0 10px;
    margin-bottom: 28px;
  }
  /* ⚠️ NOT pen-bright here, and not gold either. #1E7A4A on #16352A measures
     2.5:1 -- under the 3.0 that even large text needs -- which is why the dot
     read as almost absent on the rail. This is the same green family, lifted
     until it is actually visible on the deep band; it is the tone the deep-band
     carousel dots already use. */
  body:has(main[data-portal]) .logo-dot { color: #9FD4B4; }

  body:has(main[data-portal]) .site-nav {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
  }

  body:has(main[data-portal]) .site-nav > a {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 10px;
    border-radius: 7px;
    font-family: var(--font-ui);
    font-size: 0.95rem;
    font-weight: 500;
    color: var(--on-deep-soft);
    text-decoration: none;
    transition: background-color 0.12s ease, color 0.12s ease;
  }
  body:has(main[data-portal]) .site-nav > a:hover {
    background: rgba(245, 244, 236, 0.07);
    color: var(--on-deep);
  }
  body:has(main[data-portal]) .site-nav > a[aria-current="page"] {
    background: rgba(245, 244, 236, 0.13);
    color: var(--on-deep);
  }

  /* On the rail the badge sits at the far edge of its row, not tucked
     against the label -- Dylan, 6 Sep: "on the side". The link is already a
     flex row (gap: 11px above), so pushing this one child with its own
     left-auto margin is enough to send it to the end without touching the
     icon/label gap. */
  body:has(main[data-portal]) .site-nav > a .badge-count { margin-left: auto; }

  /* Log out sits at the foot of the rail rather than in the run of pages --
     it is not a place, and mixing it in makes it a mis-click. */
  body:has(main[data-portal]) #logoutLink { margin-top: auto; }

  /* ---- The icons -------------------------------------------------------
     Drawn with mask-image so they take `currentColor` and follow the link
     through hover and the active state on their own, with no second colour
     rule to keep in step.

     ⚠️ MATCHED ON AN href SUBSTRING, never the whole attribute. Netlify's
     post-processing rewrites href="admin-students.html" to "/admin-students"
     on deploy, so [href="admin-students.html"] matches locally and silently
     matches nothing on the live site -- the same trap that once made the nav
     grow a duplicate link. A substring survives both spellings.

     They are also keyed on the PAGE, not the label, so a page that appears in
     two portals keeps one icon.  */
  body:has(main[data-portal]) .site-nav > a::before {
    content: "";
    width: 18px; height: 18px;
    flex: none;
    background-color: currentColor;
    -webkit-mask: var(--nav-icon) center / contain no-repeat;
            mask: var(--nav-icon) center / contain no-repeat;
    /* A link this stylesheet has no icon for still lines up with the rest
       rather than jumping 29px left. */
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3C/svg%3E");
  }

  /* Overview / Dashboard / My learning -- a house */
  body:has(main[data-portal]) .site-nav > a[href*="admin.htm"]::before,
  body:has(main[data-portal]) .site-nav > a[href$="/admin"]::before,
  body:has(main[data-portal]) .site-nav > a[href*="dashboard"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 10.5 12 3l9 7.5'/%3E%3Cpath d='M5 9.5V21h14V9.5'/%3E%3C/svg%3E");
  }
  /* Students -- two people */
  body:has(main[data-portal]) .site-nav > a[href*="admin-students"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='9' cy='8' r='3.2'/%3E%3Cpath d='M3 20c0-3.3 2.7-5.4 6-5.4s6 2.1 6 5.4'/%3E%3Cpath d='M16.5 6.4a3.2 3.2 0 0 1 0 6.2M18 14.9c2.1.5 3.5 2.3 3.5 4.6'/%3E%3C/svg%3E");
  }
  /* Tutors -- a person with a tick */
  body:has(main[data-portal]) .site-nav > a[href*="admin-tutors"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10' cy='8' r='3.4'/%3E%3Cpath d='M3.5 20c0-3.4 2.9-5.6 6.5-5.6 1.2 0 2.3.2 3.2.7'/%3E%3Cpath d='m15.5 17.5 2 2 4-4.5'/%3E%3C/svg%3E");
  }
  /* Calls -- a video camera */
  body:has(main[data-portal]) .site-nav > a[href*="admin-calls"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='6' width='13' height='12' rx='2.5'/%3E%3Cpath d='m15.5 11 6-3.5v9L15.5 13'/%3E%3C/svg%3E");
  }
  /* Rates -- a price tag */
  body:has(main[data-portal]) .site-nav > a[href*="admin-rates"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.5 12.8 12.8 20.5a2 2 0 0 1-2.8 0l-6.5-6.5a2 2 0 0 1-.6-1.4V4.5a2 2 0 0 1 2-2h8.1a2 2 0 0 1 1.4.6l6.1 6.1a2 2 0 0 1 0 2.8Z'/%3E%3Ccircle cx='7.8' cy='7.8' r='1.4'/%3E%3C/svg%3E");
  }
  /* Finances / Payments -- a chart */
  body:has(main[data-portal]) .site-nav > a[href*="finances"]::before,
  body:has(main[data-portal]) .site-nav > a[href*="billing"]::before,
  body:has(main[data-portal]) .site-nav > a[href*="earnings"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 3v18h18'/%3E%3Cpath d='M7 15v3M12 10v8M17 6v12'/%3E%3C/svg%3E");
  }
  /* Progress / Tracker -- a rising line */
  body:has(main[data-portal]) .site-nav > a[href*="progress"]::before,
  body:has(main[data-portal]) .site-nav > a[href*="tracker"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 3v18h18'/%3E%3Cpath d='m7 15 4-4 3 3 5-6'/%3E%3C/svg%3E");
  }
  /* Lessons -- a book */
  body:has(main[data-portal]) .site-nav > a[href*="lessons"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4.5A1.5 1.5 0 0 1 5.5 3H19v15H5.5A1.5 1.5 0 0 0 4 19.5Z'/%3E%3Cpath d='M4 19.5A1.5 1.5 0 0 1 5.5 21H19'/%3E%3C/svg%3E");
  }
  /* Messages -- a speech bubble */
  /* ⚠️ REDRAWN TWICE, and the second attempt was still clipped.

     The bubble was an arc: A7.5 7.5 from (4,17) to (11.5,4). That chord is
     15.008 long and 2r is 15 -- so the radius is TOO SMALL FOR THE CHORD, and
     SVG's rule is to scale it up until it fits, turning the arc into a
     semicircle on that chord. Its leftmost point landed at x=0.25, and the 2px
     stroke took it to -0.75: outside the viewBox, and a mask has no overflow to
     spill into, so it was simply gone.

     This is a rounded rectangle with a straight tail, x and y both 4..20, so
     the stroke sits inside 3..21 with nothing to compute. The check that found
     it paints the icon at 240px and looks for ink on the border -- an earlier
     version read the numbers out of the path data and called relative segments
     like "l-4 2.5" absolute coordinates, which failed five good icons and
     passed this one. */
  body:has(main[data-portal]) .site-nav > a[href*="messages"]::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-4.5 4v-4A1.5 1.5 0 0 1 4 14.5Z'/%3E%3C/svg%3E");
  }
  /* Log out -- a door */
  body:has(main[data-portal]) #logoutLink::before {
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 21H5.5A1.5 1.5 0 0 1 4 19.5v-15A1.5 1.5 0 0 1 5.5 3H10'/%3E%3Cpath d='m15.5 8 4 4-4 4M19.5 12H9'/%3E%3C/svg%3E");
  }


  /* ---- The collapse toggle ---------------------------------------------
     Absolutely positioned so it does not join the column layout and shove the
     logo about. .site-header is the containing block, being fixed. */
  body:has(main[data-portal]) .rail-toggle {
    position: absolute;
    top: 26px; right: 10px;
    display: flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 6px;
    color: var(--on-deep-soft);
    cursor: pointer;
  }
  body:has(main[data-portal]) .rail-toggle:hover {
    background: rgba(245, 244, 236, 0.10);
    color: var(--on-deep);
  }
  body:has(main[data-portal]) .rail-toggle::before {
    content: "";
    width: 18px; height: 18px;
    background-color: currentColor;
    -webkit-mask: var(--nav-icon) center / contain no-repeat;
            mask: var(--nav-icon) center / contain no-repeat;
    --nav-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='2'/%3E%3Cpath d='M9 4v16'/%3E%3C/svg%3E");
  }

  /* ---- Collapsed ------------------------------------------------------- */
  html.rail-min body:has(main[data-portal]) .logo { display: none; }

  /* The toggle joins the column and centres once there is no logo beside it. */
  html.rail-min body:has(main[data-portal]) .rail-toggle {
    position: static;
    margin: 0 auto 22px;
  }

  html.rail-min body:has(main[data-portal]) .header-inner { padding: 26px 8px 20px; }

  /* ⭐ font-size: 0 hides the label without needing it wrapped in an element.
     renderPortalNav sets the label with link.textContent, so it is a bare TEXT
     NODE with no handle for CSS -- and giving it a wrapper would mean rewriting
     the label logic that carefully detaches and re-attaches the unread badge.
     The icon is a ::before with explicit width and height, so it is unaffected,
     and the badge sets its own font-size, so an unread count still shows on a
     collapsed rail. The text also stays in the accessibility tree, which
     display: none would not. */
  html.rail-min body:has(main[data-portal]) .site-nav > a {
    font-size: 0;
    gap: 0;
    justify-content: center;
    padding: 10px 0;
  }

  /* ---- The content column ---------------------------------------------- */
  /* ⚠️ THE FOOTER IS A SIBLING OF main, NOT A CHILD. Shifting only main left
     the footer full-width underneath a fixed rail -- and because both are
     green-deep, it did not read as a broken layout, it read as one continuous
     green block with the company details half-hidden behind the navigation.
     Anything at the top level of a portal page has to clear the rail. */
  body:has(main[data-portal]) main[data-portal],
  body:has(main[data-portal]) > .site-footer {
    margin-left: var(--sidebar-w);
  }
  /* ⚠️ THE 132 WAS A GUESS AT THE FOOTER'S HEIGHT, AND IT WAS WRONG BY 35px.
     It read `min-height: calc(100vh - 132px)`, meaning "leave room for the
     footer". The footer is 97px on a wide portal page -- so main claimed
     100vh - 132, the footer took 97, and the remaining 35px was PAPER showing
     under a deep-green footer: a pale band across the bottom of the window
     that Dylan reported as "a weird bottom white banner".

     It only appeared on a tall viewport, which is what made it look random.
     min-height binds only when the content is SHORTER than it -- so a parent
     dashboard with ten cards never showed it at any size, and a student
     dashboard with three showed it from about 1030px of viewport height up.
     The same overshoot is also why the page looked "small with a bunch of
     empty space": on a 1440-tall screen main was forced to 1308px for about
     900px of cards, so a third of the page was empty by instruction.

     A constant cannot do this job, because the footer's height is not a
     constant -- it wraps to 117px on a narrower column. So stop guessing it:
     make the page a column, let main take the slack, and the footer sits at
     the bottom at whatever height it actually is. The rail is position:fixed,
     so it is out of flow and unaffected by the flex container. */
  body:has(main[data-portal]) {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  body:has(main[data-portal]) main[data-portal] {
    /* flex-shrink 0 so a long page still grows past the viewport rather than
       being squeezed to fit it. min-height:0 undoes the auto minimum that
       would otherwise stop inner scroll containers from shrinking -- the same
       trap the .dash-grid rule further up already documents. */
    flex: 1 0 auto;
    min-height: 0;
  }
  body:has(main[data-portal]) > .site-footer { flex: 0 0 auto; }

  /* ---- How wide the content column is ----------------------------------
     --container is 1100px, which is an EDITORIAL measure: it exists so a line
     of prose on the marketing site stays readable. A portal page is not prose.
     It is a twelve-column grid of cards and, on the admin pages, tables.

     Capping those at 1100 inside a column that is 2356px wide on a large
     monitor put roughly 480px of dead paper down each side of a dashboard
     already pushed right by the rail, which is the other half of what Dylan
     saw. 1400 uses a 1920-wide screen properly while still refusing to let a
     card row run the full width of a 2560 one, which would be unreadable in
     the other direction. Portal only -- the marketing site keeps its 1100. */
  body:has(main[data-portal]) { --container: 1400px; }

  /* The rail already carries the wordmark, and the disclosure has to fit a
     column 248px narrower than it was written for. Stack it rather than let
     three spans fight over the room. */
  body:has(main[data-portal]) .site-footer { margin-top: 0; }
  body:has(main[data-portal]) .footer-bottom {
    flex-wrap: wrap;
    gap: 6px var(--space-3);
  }
  body:has(main[data-portal]) .footer-disclosure { flex-basis: 100%; }

  /* The page banner was a full-bleed green band under a paper header. Against
     a green rail it would read as one continuous green shape with the heading
     floating in it, so on the rail layout it becomes part of the page instead:
     same words, same structure, ink on the page's own background. */
  body:has(main[data-portal]) .dash-head {
    background: transparent;
    color: var(--ink);
    padding: clamp(30px, 3.4vw, 46px) 0 clamp(20px, 2.2vw, 30px);
    border-bottom: 1px solid var(--rule);
  }
  body:has(main[data-portal]) .dash-head h1 { color: var(--ink); }
  body:has(main[data-portal]) .dash-head .lead { color: var(--ink-soft); }

  /* ⚠️ EVERYTHING INSIDE .dash-head WAS COLOURED FOR A DEEP-GREEN BAND.
     Turning that band into paper did not break a layout or throw an error --
     it turned white text white-on-cream, and the page still looked finished.
     The child switcher (every student's name), "Book a free consultation" at
     1.03:1, the back links, the tracker's edit button: all invisible, all
     silent.

     This is the trap this stylesheet already has a note about further up --
     check which SURFACE a class lands on. Inverting a surface breaks every
     colour tuned for the old one, and the broken ones cannot be found by
     looking at a screenshot. tools/contrast-sweep.mjs is what finds them:
     it walks every element with its own text, composites what is actually
     behind it, and reports anything under the WCAG AA ratio for its size.

     ⚠️ The switcher is filled in by JavaScript from real data, so it is EMPTY
     under the test stub and the sweep never saw it. It is here because Dylan
     saw it on the live site. A sweep against stubbed pages cannot prove the
     absence of this bug -- only its presence. */
  body:has(main[data-portal]) .dash-head .child-switch { background: var(--wash); }
  body:has(main[data-portal]) .dash-head .child-switch button { color: var(--ink-soft); }
  body:has(main[data-portal]) .dash-head .child-switch button:hover { color: var(--ink); }
  body:has(main[data-portal]) .dash-head .child-switch button[aria-pressed="true"] {
    background: var(--green-deep);
    color: var(--on-deep);
  }
  body:has(main[data-portal]) .dash-head .child-switch button[aria-pressed="false"] {
    border-color: var(--rule-strong);
  }
  /* And the state a brand-new parent sees, which is a different element with
     its own colour and would have stayed invisible after the buttons were
     fixed. There was already a note on this rule saying ".note's ink colour is
     unreadable there" -- true of the green band, and exactly backwards now. */
  body:has(main[data-portal]) .dash-head .child-switch--empty .switch-empty {
    color: var(--ink-soft);
  }

  /* ⚠️ THE SUBJECT ROW WAS MISSED BY THE 2 SEP REPAINT ENTIRELY. It was built
     for the deep-green band -- pale cream text, and a border of cream at 32%
     alpha -- and when the band became paper it kept both: near-white text on
     an off-white ground, with an edge nobody can see. Dylan's screenshot has
     "Uni ... call to book" as a ghost, and he read the missing edge as the
     bug ("make the unclicked pill have an outline"). It is the same trap the
     block above documents, on the sibling element, and it survived because the
     contrast sweep runs on stubs where this row is empty. */
  body:has(main[data-portal]) .dash-head .subject-switch button {
    color: var(--ink-soft);
    border-color: var(--rule-strong);
    background: var(--paper);
  }
  body:has(main[data-portal]) .dash-head .subject-switch button:hover { color: var(--ink); }
  body:has(main[data-portal]) .dash-head .subject-switch button[aria-pressed="true"] {
    background: var(--green-deep);
    color: var(--on-deep);
    border-color: var(--green-deep);
  }

  body:has(main[data-portal]) .dash-head .back-link,
  body:has(main[data-portal]) .dash-head .meta-edit,
  body:has(main[data-portal]) .dash-head .who-meta { color: var(--ink-soft); }
  body:has(main[data-portal]) .dash-head .back-link:hover,
  body:has(main[data-portal]) .dash-head .back-link:focus-visible,
  body:has(main[data-portal]) .dash-head .meta-edit:hover { color: var(--ink); }
  /* Gold on paper, not the pale gold that was picked to sit on green. */
  body:has(main[data-portal]) .dash-head .who-meta .stars { color: var(--gold); }

  /* The primary button was near-white on green; on paper that is a white box
     with green text and no edge. Invert it: it is still the loudest thing in
     the band, which is the job it had. */
  body:has(main[data-portal]) .dash-head .btn-on-deep {
    background: var(--green-deep);
    color: var(--on-deep);
  }
  body:has(main[data-portal]) .dash-head .btn-on-deep:hover {
    background: var(--green-deep-hover);
    color: var(--on-deep);
  }
  /* And the outline button matches the paper .btn-outline rather than the
     on-deep one it was written as. */
  body:has(main[data-portal]) .dash-head .btn-outline-on-deep {
    color: var(--pen);
    border-color: var(--rule-strong);
  }
  body:has(main[data-portal]) .dash-head .btn-outline-on-deep:hover {
    color: var(--green-deep);
    border-color: var(--green-deep);
  }
}


/* ===========================================================================
   University prep in the tracker
   ---------------------------------------------------------------------------
   A university-prep student has no specification, so there is nothing to score,
   nothing to cover a percentage of, and no grade boundaries to work a grade
   back from. Roughly half the tracker is therefore meaningless for them -- not
   empty, MEANINGLESS, which is worse. "Course coverage 0%" and "Working grade
   --" both read as a tutor who has not done their job.

   So the two modes are shown by swapping which half of the page exists, keyed
   on one class that setActiveStudent puts on <main>.

   ⚠️ THE DEFAULT IS SPEC MODE, and it has to be. The mode is not known until a
   profile read and a child read have both come back, so the page renders once
   before anything is resolved. Defaulting the other way would flash the
   university-prep fields at every spec-point tutor on every page load -- and
   worse, `display: none` is the only thing keeping the required Content taught
   box out of a spec-point tutor's form.

   Nothing here gates the DATA. lessons.content_taught and lessons.weaknesses
   are plain nullable columns any student can carry; this only decides what is
   put in front of whom. */
.trk-uni-only { display: none; }
.dash.trk-uni .trk-uni-only { display: block; }
/* The card head's two notes sit inline beside the <h3>, so they cannot become
   block without dropping to their own line. */
.dash.trk-uni span.note.trk-uni-only { display: inline; }
.dash.trk-uni .trk-spec-only { display: none !important; }

/* Rating boundaries are spec-point machinery and go with the rest, which would
   otherwise leave the weekly note stranded in a half-width column with nothing
   beside it. */
.dash.trk-uni #weeklyNote { grid-column: span 12; }

/* ---------------------------------------------------------------------------
   Botanical corner ornamentation -- built against
   TUTORA_LEAF_BACKGROUND_SPEC.md, reviewed as a local demo and approved by
   Dylan on 5 Sep. Regenerate via tools/publish-leaf-background.py, which
   shares its variant geometry and PLACEMENTS table with tools/make-leaf-demo.py
   so the two can never quietly drift apart.
   --------------------------------------------------------------------------- */
:root {
  --leaf-svg-long-corner-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 220' width='320' height='220' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 210.0 C90.0 190.0 140.0 110.0 300.0 30.0 M91.3 167.9 C121.1 146.3 96.9 108.3 99.9 76.3'/%3E%3Cpath d='M37.7 200.3 C49.8 190.0 51.8 173.4 52.9 163.0 C46.4 171.3 36.3 184.5 37.7 200.3 Z M60.5 188.4 C72.4 197.9 88.5 196.7 98.5 195.9 C89.5 191.3 75.1 184.2 60.5 188.4 Z M83.3 173.5 C93.2 162.7 93.2 147.3 93.0 137.6 C88.0 145.9 80.2 159.2 83.3 173.5 Z M107.1 156.1 C119.5 162.8 133.8 159.2 142.8 156.7 C133.9 154.0 119.7 149.9 107.1 156.1 Z M132.8 136.6 C142.1 126.9 142.4 112.7 142.5 103.8 C137.7 111.3 130.3 123.3 132.8 136.6 Z M161.2 115.2 C172.9 120.7 185.8 116.7 193.9 114.0 C185.6 112.0 172.4 108.9 161.2 115.2 Z M193.4 92.5 C201.0 82.9 200.1 70.0 199.4 61.9 C195.7 69.1 190.0 80.7 193.4 92.5 Z M230.1 68.7 C239.3 76.0 251.5 75.2 259.2 74.5 C252.4 71.0 241.3 65.5 230.1 68.7 Z M272.4 44.4 C280.6 37.0 281.7 25.3 282.3 18.0 C277.9 23.9 271.1 33.4 272.4 44.4 Z M99.8 159.4 C109.6 160.3 117.9 154.1 123.1 150.1 C116.6 150.7 106.3 152.0 99.8 159.4 Z M106.3 136.2 C103.8 127.5 95.8 122.5 90.6 119.4 C93.4 124.7 97.8 133.1 106.3 136.2 Z M102.9 109.5 C109.9 105.1 112.2 96.8 113.6 91.6 C109.6 95.3 103.5 101.3 102.9 109.5 Z M99.6 82.1 C98.1 74.9 91.9 70.4 87.9 67.6 C89.8 72.1 92.9 79.2 99.6 82.1 Z'/%3E%3C/svg%3E");
  --leaf-svg-long-corner-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 220' width='320' height='220' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 210.0 C90.0 190.0 140.0 110.0 300.0 30.0'/%3E%3Cpath d='M37.7 200.3 C49.8 190.0 51.8 173.4 52.9 163.0 C46.4 171.3 36.3 184.5 37.7 200.3 Z M60.5 188.4 C72.4 197.9 88.5 196.7 98.5 195.9 C89.5 191.3 75.1 184.2 60.5 188.4 Z M83.3 173.5 C93.2 162.7 93.2 147.3 93.0 137.6 C88.0 145.9 80.2 159.2 83.3 173.5 Z M107.1 156.1 C119.5 162.8 133.8 159.2 142.8 156.7 C133.9 154.0 119.7 149.9 107.1 156.1 Z M132.8 136.6 C142.1 126.9 142.4 112.7 142.5 103.8 C137.7 111.3 130.3 123.3 132.8 136.6 Z M161.2 115.2 C172.9 120.7 185.8 116.7 193.9 114.0 C185.6 112.0 172.4 108.9 161.2 115.2 Z M193.4 92.5 C201.0 82.9 200.1 70.0 199.4 61.9 C195.7 69.1 190.0 80.7 193.4 92.5 Z M230.1 68.7 C239.3 76.0 251.5 75.2 259.2 74.5 C252.4 71.0 241.3 65.5 230.1 68.7 Z M272.4 44.4 C280.6 37.0 281.7 25.3 282.3 18.0 C277.9 23.9 271.1 33.4 272.4 44.4 Z'/%3E%3C/svg%3E");
  --leaf-svg-tall-edge-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 340' width='200' height='340' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.0 328.0 C62.0 236.0 28.0 118.0 92.0 14.0'/%3E%3Cpath d='M32.1 293.8 C45.6 292.7 55.2 282.3 61.2 275.7 C52.6 278.1 39.0 282.2 32.1 293.8 Z M38.7 263.2 C37.7 250.3 27.8 241.0 21.4 235.3 C23.7 243.5 27.6 256.5 38.7 263.2 Z M42.8 231.4 C54.6 227.7 61.1 216.5 65.0 209.3 C57.9 213.3 46.6 219.7 42.8 231.4 Z M45.8 198.9 C44.7 187.2 35.6 178.9 29.7 173.7 C31.9 181.2 35.6 193.0 45.8 198.9 Z M48.6 165.8 C59.0 161.6 64.1 151.0 67.2 144.3 C61.0 148.3 51.3 154.9 48.6 165.8 Z M52.5 132.3 C50.6 121.9 41.7 115.2 36.0 111.1 C38.6 117.6 42.9 127.9 52.5 132.3 Z M58.5 98.9 C68.5 97.8 75.4 89.9 79.7 84.9 C73.4 86.8 63.4 90.2 58.5 98.9 Z M67.8 65.6 C68.5 56.2 62.3 48.5 58.4 43.6 C59.2 49.8 60.6 59.6 67.8 65.6 Z M81.5 32.9 C90.3 33.5 97.6 27.7 102.1 24.0 C96.3 24.7 87.1 26.0 81.5 32.9 Z'/%3E%3C/svg%3E");
  --leaf-svg-tall-edge-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 340' width='200' height='340' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.0 328.0 C62.0 236.0 28.0 118.0 92.0 14.0'/%3E%3Cpath d='M32.1 293.8 C45.6 292.7 55.2 282.3 61.2 275.7 C52.6 278.1 39.0 282.2 32.1 293.8 Z M38.7 263.2 C37.7 250.3 27.8 241.0 21.4 235.3 C23.7 243.5 27.6 256.5 38.7 263.2 Z M42.8 231.4 C54.6 227.7 61.1 216.5 65.0 209.3 C57.9 213.3 46.6 219.7 42.8 231.4 Z M45.8 198.9 C44.7 187.2 35.6 178.9 29.7 173.7 C31.9 181.2 35.6 193.0 45.8 198.9 Z M48.6 165.8 C59.0 161.6 64.1 151.0 67.2 144.3 C61.0 148.3 51.3 154.9 48.6 165.8 Z M52.5 132.3 C50.6 121.9 41.7 115.2 36.0 111.1 C38.6 117.6 42.9 127.9 52.5 132.3 Z M58.5 98.9 C68.5 97.8 75.4 89.9 79.7 84.9 C73.4 86.8 63.4 90.2 58.5 98.9 Z M67.8 65.6 C68.5 56.2 62.3 48.5 58.4 43.6 C59.2 49.8 60.6 59.6 67.8 65.6 Z M81.5 32.9 C90.3 33.5 97.6 27.7 102.1 24.0 C96.3 24.7 87.1 26.0 81.5 32.9 Z'/%3E%3C/svg%3E");
  --leaf-svg-small-corner-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 140' width='150' height='140' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 128.0 C50.0 100.0 75.0 55.0 140.0 14.0'/%3E%3Cpath d='M23.8 117.2 C35.1 121.8 47.1 117.4 54.6 114.5 C46.7 113.0 34.1 110.8 23.8 117.2 Z M46.1 96.0 C51.0 85.8 47.6 74.3 45.3 67.2 C43.4 74.4 40.7 86.1 46.1 96.0 Z M69.1 72.3 C78.7 76.9 89.4 73.8 96.1 71.7 C89.3 69.9 78.5 67.2 69.1 72.3 Z M95.7 47.0 C101.2 38.9 99.7 28.7 98.6 22.3 C96.1 28.3 92.3 37.9 95.7 47.0 Z M128.7 21.4 C136.3 26.4 145.7 24.8 151.6 23.8 C146.0 21.5 137.1 18.1 128.7 21.4 Z'/%3E%3C/svg%3E");
  --leaf-svg-small-corner-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 140' width='150' height='140' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 128.0 C50.0 100.0 75.0 55.0 140.0 14.0'/%3E%3Cpath d='M23.8 117.2 C35.1 121.8 47.1 117.4 54.6 114.5 C46.7 113.0 34.1 110.8 23.8 117.2 Z M46.1 96.0 C51.0 85.8 47.6 74.3 45.3 67.2 C43.4 74.4 40.7 86.1 46.1 96.0 Z M69.1 72.3 C78.7 76.9 89.4 73.8 96.1 71.7 C89.3 69.9 78.5 67.2 69.1 72.3 Z M95.7 47.0 C101.2 38.9 99.7 28.7 98.6 22.3 C96.1 28.3 92.3 37.9 95.7 47.0 Z M128.7 21.4 C136.3 26.4 145.7 24.8 151.6 23.8 C146.0 21.5 137.1 18.1 128.7 21.4 Z'/%3E%3C/svg%3E");
  --leaf-svg-rising-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300' width='220' height='300' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.0 286.0 C40.0 190.0 90.0 138.0 204.0 16.0 M56.2 189.4 C73.8 161.7 109.2 183.7 137.8 180.6'/%3E%3Cpath d='M24.5 253.1 C26.1 240.8 18.7 230.0 13.9 223.4 C14.4 231.6 15.4 244.6 24.5 253.1 Z M37.1 223.6 C49.0 223.8 58.4 215.6 64.3 210.3 C56.5 211.7 44.2 214.1 37.1 223.6 Z M52.5 195.5 C56.8 184.9 52.7 173.7 50.0 166.7 C48.5 174.0 46.4 185.8 52.5 195.5 Z M71.0 167.6 C81.7 169.0 91.1 162.4 96.8 158.2 C89.7 158.7 78.3 159.7 71.0 167.6 Z M93.0 139.0 C96.8 129.4 93.0 119.2 90.5 112.9 C89.2 119.6 87.4 130.3 93.0 139.0 Z M119.0 108.4 C128.1 112.0 137.7 108.3 143.7 105.9 C137.3 104.7 127.2 103.1 119.0 108.4 Z M149.2 74.9 C153.7 66.8 151.4 57.3 149.9 51.4 C148.1 57.2 145.2 66.5 149.2 74.9 Z M184.2 37.2 C192.4 40.1 200.8 36.5 206.1 34.1 C200.4 33.3 191.3 32.2 184.2 37.2 Z M63.3 181.4 C71.1 186.0 80.5 184.0 86.4 182.6 C80.7 180.6 71.6 177.7 63.3 181.4 Z M83.7 175.2 C91.9 173.4 97.0 166.3 100.1 161.7 C95.0 163.9 87.0 167.6 83.7 175.2 Z M107.8 178.1 C109.9 185.4 116.6 189.6 120.9 192.2 C118.6 187.7 114.9 180.7 107.8 178.1 Z M132.6 180.9 C139.3 179.9 143.8 174.4 146.5 170.8 C142.3 172.4 135.6 174.9 132.6 180.9 Z'/%3E%3C/svg%3E");
  --leaf-svg-rising-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300' width='220' height='300' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.0 286.0 C40.0 190.0 90.0 138.0 204.0 16.0'/%3E%3Cpath d='M24.5 253.1 C26.1 240.8 18.7 230.0 13.9 223.4 C14.4 231.6 15.4 244.6 24.5 253.1 Z M37.1 223.6 C49.0 223.8 58.4 215.6 64.3 210.3 C56.5 211.7 44.2 214.1 37.1 223.6 Z M52.5 195.5 C56.8 184.9 52.7 173.7 50.0 166.7 C48.5 174.0 46.4 185.8 52.5 195.5 Z M71.0 167.6 C81.7 169.0 91.1 162.4 96.8 158.2 C89.7 158.7 78.3 159.7 71.0 167.6 Z M93.0 139.0 C96.8 129.4 93.0 119.2 90.5 112.9 C89.2 119.6 87.4 130.3 93.0 139.0 Z M119.0 108.4 C128.1 112.0 137.7 108.3 143.7 105.9 C137.3 104.7 127.2 103.1 119.0 108.4 Z M149.2 74.9 C153.7 66.8 151.4 57.3 149.9 51.4 C148.1 57.2 145.2 66.5 149.2 74.9 Z M184.2 37.2 C192.4 40.1 200.8 36.5 206.1 34.1 C200.4 33.3 191.3 32.2 184.2 37.2 Z'/%3E%3C/svg%3E");
  --leaf-svg-falling-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300' width='220' height='300' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16.0 16.0 C40.0 112.0 90.0 162.0 206.0 284.0 M63.0 121.4 C80.4 147.2 113.3 127.7 140.3 131.2'/%3E%3Cpath d='M25.8 48.8 C16.9 57.5 16.2 70.6 15.8 78.8 C20.5 72.0 27.7 61.1 25.8 48.8 Z M38.0 78.1 C44.7 87.9 56.9 90.8 64.6 92.5 C59.0 87.0 49.9 78.4 38.0 78.1 Z M53.1 106.0 C47.4 115.8 49.9 127.5 51.6 134.8 C54.0 127.7 57.8 116.3 53.1 106.0 Z M71.5 133.5 C80.2 139.9 91.5 138.7 98.7 137.9 C92.2 134.8 81.8 130.1 71.5 133.5 Z M93.6 161.8 C88.3 170.7 90.6 181.3 92.1 187.9 C94.3 181.5 97.8 171.2 93.6 161.8 Z M119.7 192.0 C127.8 197.5 138.0 196.1 144.4 195.1 C138.4 192.6 128.9 188.6 119.7 192.0 Z M150.3 225.3 C146.7 233.8 149.9 243.0 152.1 248.7 C153.3 242.7 155.2 233.2 150.3 225.3 Z M185.8 262.8 C192.8 268.0 201.9 267.1 207.7 266.4 C202.5 263.9 194.1 260.1 185.8 262.8 Z M69.9 128.8 C78.1 132.7 87.3 130.0 93.0 128.1 C87.2 126.6 77.8 124.4 69.9 128.8 Z M89.4 135.0 C92.8 142.6 100.8 146.1 105.9 148.3 C102.7 143.7 97.6 136.7 89.4 135.0 Z M112.1 132.8 C119.4 130.7 123.5 123.9 126.0 119.5 C121.6 121.9 114.6 125.7 112.1 132.8 Z M135.4 130.8 C138.5 136.8 145.2 139.3 149.5 140.7 C146.7 137.2 142.1 131.7 135.4 130.8 Z'/%3E%3C/svg%3E");
  --leaf-svg-falling-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300' width='220' height='300' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16.0 16.0 C40.0 112.0 90.0 162.0 206.0 284.0'/%3E%3Cpath d='M25.8 48.8 C16.9 57.5 16.2 70.6 15.8 78.8 C20.5 72.0 27.7 61.1 25.8 48.8 Z M38.0 78.1 C44.7 87.9 56.9 90.8 64.6 92.5 C59.0 87.0 49.9 78.4 38.0 78.1 Z M53.1 106.0 C47.4 115.8 49.9 127.5 51.6 134.8 C54.0 127.7 57.8 116.3 53.1 106.0 Z M71.5 133.5 C80.2 139.9 91.5 138.7 98.7 137.9 C92.2 134.8 81.8 130.1 71.5 133.5 Z M93.6 161.8 C88.3 170.7 90.6 181.3 92.1 187.9 C94.3 181.5 97.8 171.2 93.6 161.8 Z M119.7 192.0 C127.8 197.5 138.0 196.1 144.4 195.1 C138.4 192.6 128.9 188.6 119.7 192.0 Z M150.3 225.3 C146.7 233.8 149.9 243.0 152.1 248.7 C153.3 242.7 155.2 233.2 150.3 225.3 Z M185.8 262.8 C192.8 268.0 201.9 267.1 207.7 266.4 C202.5 263.9 194.1 260.1 185.8 262.8 Z'/%3E%3C/svg%3E");
  --leaf-svg-forked-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 180' width='200' height='180' fill='none' stroke='%23C9D4CB' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 168.0 C58.0 138.0 88.0 88.0 168.0 18.0 M63.3 122.4 C82.2 102.8 107.3 126.7 131.0 129.0'/%3E%3Cpath d='M26.6 156.3 C34.2 147.9 34.2 135.9 34.0 128.4 C30.2 134.8 24.2 145.2 26.6 156.3 Z M48.1 137.6 C58.3 141.1 68.7 136.6 75.2 133.6 C68.2 132.6 56.9 131.3 48.1 137.6 Z M69.8 115.6 C74.1 106.4 70.9 96.1 68.7 89.7 C67.1 96.3 64.8 106.7 69.8 115.6 Z M93.5 90.4 C102.3 94.4 111.9 91.3 118.0 89.3 C111.8 87.8 101.8 85.6 93.5 90.4 Z M121.0 62.0 C125.6 54.2 123.6 44.9 122.2 39.0 C120.2 44.7 117.3 53.8 121.0 62.0 Z M154.1 30.4 C161.8 34.0 170.3 31.3 175.6 29.6 C170.2 28.2 161.5 26.2 154.1 30.4 Z M70.4 117.1 C77.0 122.3 85.7 121.6 91.3 121.1 C86.3 118.6 78.4 114.8 70.4 117.1 Z M88.0 115.5 C95.7 116.0 102.0 110.9 105.9 107.6 C100.9 108.3 92.9 109.5 88.0 115.5 Z M107.1 121.9 C108.1 128.9 113.7 133.7 117.3 136.6 C115.8 132.2 113.3 125.2 107.1 121.9 Z M126.7 128.4 C133.0 129.2 138.5 125.3 141.9 122.8 C137.7 123.1 131.0 123.7 126.7 128.4 Z'/%3E%3C/svg%3E");
  --leaf-svg-forked-gold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 180' width='200' height='180' fill='none' stroke='%238C6B16' stroke-width='1.55' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.0 168.0 C58.0 138.0 88.0 88.0 168.0 18.0'/%3E%3Cpath d='M26.6 156.3 C34.2 147.9 34.2 135.9 34.0 128.4 C30.2 134.8 24.2 145.2 26.6 156.3 Z M48.1 137.6 C58.3 141.1 68.7 136.6 75.2 133.6 C68.2 132.6 56.9 131.3 48.1 137.6 Z M69.8 115.6 C74.1 106.4 70.9 96.1 68.7 89.7 C67.1 96.3 64.8 106.7 69.8 115.6 Z M93.5 90.4 C102.3 94.4 111.9 91.3 118.0 89.3 C111.8 87.8 101.8 85.6 93.5 90.4 Z M121.0 62.0 C125.6 54.2 123.6 44.9 122.2 39.0 C120.2 44.7 117.3 53.8 121.0 62.0 Z M154.1 30.4 C161.8 34.0 170.3 31.3 175.6 29.6 C170.2 28.2 161.5 26.2 154.1 30.4 Z'/%3E%3C/svg%3E");
}

/* The hero's exercise-book ruling comes off -- the sprigs replace the job
   it was doing. */
.hero::before {
  -webkit-mask-image: none !important;
          mask-image: none !important;
  background-image: none !important;
}

/* Every section that can carry a motif needs a positioning context and a
   z-index floor for its own content, so artwork can never land on top of
   text -- the motif is always behind, whatever gets added later. */
.hero, .section, .page-hero, .footer-cta { position: relative; overflow: hidden; }
.hero > *, .section > *, .page-hero > *, .footer-cta > * {
  position: relative;
  z-index: 1;
}

.leaf-decoration {
  position: absolute;
  z-index: 0;
  display: block;
  pointer-events: none;
  user-select: none;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}
.leaf-decoration--light { opacity: var(--leaf-decoration-opacity, 0.48); }
.leaf-decoration--gold { opacity: var(--leaf-decoration-opacity, 0.76); }

/* TABLET: fewer, smaller -- drop anything marked secondary and scale the
   rest down, rather than pretend a desktop composition still fits. */
@media (max-width: 1199px) {
  .leaf-decoration { transform: scale(0.82); }
  .leaf-decoration--secondary { display: none; }
}
/* MOBILE: off entirely -- a squished desktop composition is not the same
   as a phone-tuned one (mobile-parity-rule). */
@media (max-width: 767px) {
  .leaf-decoration { display: none; }
}
