/* =============================================================================
   student-portal — design system
   -----------------------------------------------------------------------------
   Mobile-first. Single column by default; the 12-column bento grid switches on
   at 768px. Colours are the existing OxBridge blue (#7CABE6) and coral
   (#F26A64), deepened where they have to carry text at an accessible contrast.

   Everything here is namespaced under .sp- so it cannot collide with the
   Bootstrap 4 rules the rest of the portal still loads.
   ========================================================================== */

.sp-root {
  --sp-brand:        #1E5F9E;   /* deepened OxBridge blue - text, buttons, links */
  --sp-brand-mid:    #7CABE6;   /* the existing brand blue - fills, accents      */
  --sp-brand-soft:   #E4EFF9;
  --sp-coral:        #C6483F;   /* deepened coral - warnings that carry text     */
  --sp-coral-mid:    #F26A64;   /* the existing brand coral                      */
  --sp-coral-soft:   #FBE9E7;
  --sp-green:        #12694A;
  --sp-green-soft:   #E2F1EA;
  --sp-amber:        #8A5A00;
  --sp-amber-soft:   #FBF0DA;

  --sp-ink:          #16232F;
  --sp-ink-soft:     #40546A;
  --sp-muted:        #6B7E93;
  --sp-line:         #DCE6F0;
  --sp-line-strong:  #C3D4E5;
  --sp-ground:       #F4F8FC;
  --sp-surface:      #FFFFFF;

  --sp-radius:       12px;
  --sp-radius-sm:    8px;
  --sp-shadow:       0 1px 2px rgba(20,45,75,.05), 0 6px 16px rgba(20,45,75,.05);
  --sp-gap:          14px;

  --sp-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  background: var(--sp-ground);
  color: var(--sp-ink);
  font-family: var(--sp-sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
}

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

.sp-wrap { max-width: 1120px; margin: 0 auto; padding: 0 14px 56px; }

/* ---------- header ------------------------------------------------------- */

.sp-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 12px 0 16px;
}
/* Logo and contact details travel together on the left, so the header stays a
   two-part bar - identity here, destinations there - rather than becoming three
   things fighting over space-between. */
.sp-barleft { display: flex; align-items: center; gap: 20px; min-width: 0; }

.sp-brandmark { display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; }
.sp-brandmark img { width: 34px; height: auto; display: block; }
.sp-brandmark b { font-size: 15px; font-weight: 700; line-height: 1.15; display: block; color: var(--sp-ink); }
.sp-brandmark span { font-size: 11.5px; color: var(--sp-muted); display: block; }

/* --- how to reach the centre -------------------------------------------- */
/* Muted, so it reads as a detail beside the logo and not as two more tabs.

   STACKED, NOT SIDE BY SIDE, and that is what makes both readable. Written out
   in a row they need about 300px and overflow the header even at the layout's
   own 1120px maximum, which is why the address was an unexplained icon at every
   width. In a column the block is only as wide as the LONGER of the two - about
   155px, five more than the row of one number and one icon it replaces - and
   two lines tall, which is the height of the logo lockup beside it. So it costs
   nothing and says everything.

   THE WIDTHS ARE MEASURED, not guessed: 24 viewport widths against this
   stylesheet, comparing every line count with and without. See
   student_portal_contact_bar() for what shows where. */
.sp-contact { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.sp-contact a {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-size: 12.5px; font-weight: 600; color: var(--sp-muted); text-decoration: none;
}
.sp-contact a:hover, .sp-contact a:focus { color: var(--sp-brand); text-decoration: none; }
.sp-contact svg { flex: none; }

/* Below the layout's own maximum the words no longer fit, so both give way to
   their icons and the block goes back to a row. .sp-nav wraps rather than
   overflows, so keeping them would push the whole header onto a second line
   rather than merely making it tight. */
@media (max-width: 1119px) {
  .sp-contact { flex-direction: row; align-items: center; gap: 6px; }
  .sp-contact a span { display: none; }
  .sp-contact a { padding: 2px; }
}

.sp-nav { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.sp-nav a {
  font-size: 13.5px; font-weight: 600; color: var(--sp-ink-soft); text-decoration: none;
  padding: 7px 11px; border-radius: var(--sp-radius-sm);
}
.sp-nav a:hover, .sp-nav a:focus { background: var(--sp-brand-soft); color: var(--sp-brand); }
.sp-nav a.sp-on { background: var(--sp-brand-soft); color: var(--sp-brand); }

/* The Invoice dot. One class, two placements: inline after the label up here,
   pinned to the icon in the phone tabs below. Coral rather than the brand
   colour, because the brand colour is also what marks the ACTIVE tab -- a dot
   in it would read as "you are here" on every page except the one you are on.
   Never the only signal: the strip on the page says the same thing in words,
   and the link carries it in aria-label for anyone who cannot see the dot. */
.sp-nav a .sp-dot {
  display: inline-block; width: 6px; height: 6px; margin-left: 5px;
  border-radius: 50%; background: var(--sp-coral);
  /* Nudged up to sit against the cap-height rather than the baseline. */
  vertical-align: 2px;
}

/* Who is signed in. Muted and unpadded so it reads as a label rather than as a
   sixth tab someone might try to click. Trimmed rather than wrapped: a long
   name must not push Log out onto a second line. */
.sp-who {
  font-size: 12.5px; color: var(--sp-muted); font-weight: 600;
  /* 180px until the contact details arrived. The name is the one thing in this
     bar that already trims itself, so it is what pays for the two icons beside
     the logo; 110px still fits a first name and an initial. */
  margin-left: 6px; max-width: 110px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- the phone tab bar --------------------------------------------
   Hidden on a laptop, where the header nav is already right. Below 560px the
   header keeps only the logo and Log out, and the destinations move to a
   fixed bar in thumb reach. */
.sp-tabs { display: none; }

@media (max-width: 560px) {
  .sp-nav a:not(.sp-nav-out) { display: none; }
  /* The header keeps logo, name and Log out. A phone has room for a first name
     and not much more, so the trim tightens rather than the name disappearing —
     seeing WHOSE portal this is matters most on the device it gets checked on. */
  .sp-who { margin-left: 0; }

  /* A PHONE GETS ITS OWN ROW FOR THE CONTACT DETAILS, and that is what makes
     the wordmark, the number and the address all fit at once. Measured: on one
     row they need about 476px against the 362px a 390 wide screen actually has,
     so no amount of trimming was ever going to do it. Given a row to
     themselves they have the full width and need about 280px, which fits down
     to 320.

     display:contents on the wrapper is what allows it. .sp-contact is nested
     inside .sp-barleft, so it cannot be ordered against .sp-nav from there;
     dissolving the wrapper makes the brandmark and the contact block direct
     children of .sp-bar, and then order + a 100% basis drops the contact onto
     a second row while the brandmark and Log out keep the first. The wrapper is
     a plain div with no role of its own, so nothing is lost from the
     accessibility tree by dissolving it.

     The header is taller on a phone as a result. That is the trade: two rows
     that say who you are and how to reach us, against one row that said
     neither. */
  .sp-barleft { display: contents; }
  .sp-bar { flex-wrap: wrap; row-gap: 6px; }
  .sp-brandmark { order: 1; }
  .sp-nav { order: 2; }

  .sp-contact {
    order: 3; flex: 0 0 100%;
    flex-direction: row; align-items: center; flex-wrap: wrap;
    gap: 4px 16px; padding-top: 2px;
  }
  /* Written out, not icons. Overrides the 1119px rule above, which this block
     follows in the file. */
  .sp-contact a span { display: inline; }
  .sp-contact a { padding: 0; }

  .sp-who { max-width: 72px; }

  .sp-tabs {
    /* However many tabs student_portal_nav() defines, in one equal row. This
       was pinned to exactly five columns, so the sixth tab (Courses, added by
       special-course-bookings s24) wrapped onto a second row inside a bar that
       is fixed to the bottom of the screen. Counting in CSS is a trap the next
       tab would spring again. */
    display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 800;
    background: var(--sp-surface);
    border-top: 1px solid var(--sp-line);
    /* Clear of the iPhone home indicator, and 0 on anything without one. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 14px rgba(20, 45, 75, .06);
  }
  .sp-tab {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; padding: 7px 2px 8px; text-decoration: none;
    color: var(--sp-muted); font-size: 10px; font-weight: 600;
    /* Apple and Google both ask for 44px of tappable height. */
    min-height: 52px;
  }
  .sp-tab span { line-height: 1; }
  .sp-tab.sp-on { color: var(--sp-brand); }
  .sp-tab.sp-on svg { stroke-width: 2.1; }

  /* Positioned against the tab, NOT wrapped around the icon: the icon is drawn
     by student_portal_tab_icon() and wrapping it would mean every tab carrying
     an extra element for the sake of one. 50% is the tab's centre, so +7px puts
     the dot on the icon's top-right corner at any tab width. */
  .sp-tab { position: relative; }
  .sp-tab .sp-dot {
    position: absolute; top: 5px; left: 50%; margin-left: 7px;
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--sp-coral);
    /* Against the bar, so it stays legible where it overlaps the glyph. */
    box-shadow: 0 0 0 2px var(--sp-surface);
  }

  /* Room to scroll past the bar, so it never sits on the last card. */
  .sp-wrap { padding-bottom: 78px; }
}

/* The wordmark used to be hidden below 430px, because with the contact details
   sharing its row it was the one thing that would not give. They no longer
   share a row on a phone - see the .sp-barleft rule in the 560px block - so it
   is back at every width. */

.sp-hello { margin: 4px 0 18px; }
.sp-hello h1 { font-size: 23px; line-height: 1.2; margin: 0; font-weight: 700; }
.sp-hello p { margin: 4px 0 0; color: var(--sp-muted); font-size: 14px; }

/* ---------- grid --------------------------------------------------------- */

.sp-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-gap); }
.sp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }

@media (min-width: 768px) {
  .sp-wrap { padding: 0 22px 72px; }
  .sp-grid { grid-template-columns: repeat(12, 1fr); gap: 16px; }
  .sp-c4  { grid-column: span 4; }
  .sp-c5  { grid-column: span 5; }
  .sp-c6  { grid-column: span 6; }
  .sp-c7  { grid-column: span 7; }
  .sp-c8  { grid-column: span 8; }
  .sp-c12 { grid-column: span 12; }
  /* The calendar is wrapped in #sp-cal-region so a month change can swap
     its contents in one go. That wrapper is itself a child of .sp-grid, so
     without a span of its own it auto-placed into a SINGLE column and took
     the whole calendar down to a twelfth of the page. The card inside it is
     a .sp-c12, but that only spans the region, not the grid. */
  .sp-cal-region { grid-column: span 12; }
  .sp-stats { display: contents; }   /* stat tiles join the 12-col grid directly */
  .sp-hello h1 { font-size: 27px; }
}

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

.sp-card {
  background: var(--sp-surface); border: 1px solid var(--sp-line);
  border-radius: var(--sp-radius); box-shadow: var(--sp-shadow);
  padding: 16px; display: flex; flex-direction: column;
}
.sp-card > h2 {
  font-size: 11.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--sp-muted); margin: 0 0 12px; display: flex; justify-content: space-between;
  align-items: baseline; gap: 10px;
}
.sp-card > h2 a { font-size: 12px; letter-spacing: 0; text-transform: none; color: var(--sp-brand); font-weight: 600; text-decoration: none; }
.sp-card > h2 a:hover { text-decoration: underline; }

/* hero ------------------------------------------------------------------- */

.sp-hero { border-color: var(--sp-brand-mid); background: linear-gradient(180deg, var(--sp-brand-soft) 0%, var(--sp-surface) 62%); }
.sp-hero .sp-when { font-size: 12.5px; font-weight: 700; color: var(--sp-brand); text-transform: uppercase; letter-spacing: .06em; }
.sp-hero .sp-what { font-size: 21px; font-weight: 700; margin: 5px 0 3px; line-height: 1.2; }
.sp-hero .sp-where { font-size: 14px; color: var(--sp-ink-soft); margin: 0; }
.sp-hero .sp-act { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }

/* stat tiles ------------------------------------------------------------- */

/* THE DASHBOARD STAT TILES, currently unused. The three that lived here --
   attendance, homework submitted, latest score -- were removed on 2026-08-31
   because none of them had data for a regular course, so all a family saw was a
   row of dashes. Kept whole, not deleted: they come back when Progress lands.
   See the note where they were, in dashboard.php. */
.sp-stat { text-align: left; }
.sp-stat .sp-lab { font-size: 11.5px; color: var(--sp-muted); font-weight: 600; line-height: 1.25; }
.sp-stat .sp-big { font-size: 27px; font-weight: 800; line-height: 1.05; margin-top: 5px; letter-spacing: -.02em; }
.sp-stat .sp-sub { font-size: 11.5px; color: var(--sp-muted); margin-top: 3px; }
.sp-stat.sp-empty .sp-big { font-size: 15px; font-weight: 600; color: var(--sp-muted); letter-spacing: 0; }

/* ---------- alert strip -------------------------------------------------- */

.sp-alert {
  border-radius: var(--sp-radius); padding: 12px 14px; display: flex; gap: 11px;
  align-items: flex-start; font-size: 14px; border: 1px solid transparent;
}
.sp-alert b { display: block; font-size: 14.5px; margin-bottom: 1px; }
.sp-alert p { margin: 0; }
.sp-alert .sp-alert-act { margin-left: auto; flex: 0 0 auto; }
.sp-alert-warn { background: var(--sp-coral-soft); border-color: var(--sp-coral-mid); color: var(--sp-coral); }
.sp-alert-warn p { color: var(--sp-ink-soft); }
.sp-alert-info { background: var(--sp-brand-soft); border-color: var(--sp-brand-mid); color: var(--sp-brand); }
.sp-alert-info p { color: var(--sp-ink-soft); }
@media (max-width: 560px) {
  .sp-alert { flex-direction: column; }
  .sp-alert .sp-alert-act { margin-left: 0; width: 100%; }
  .sp-alert .sp-alert-act .sp-btn { width: 100%; }
}

/* ---------- lesson rows -------------------------------------------------- */

.sp-lesson {
  display: flex; gap: 12px; align-items: center; padding: 10px 0;
  border-bottom: 1px solid var(--sp-line);
}
.sp-lesson:last-child { border-bottom: 0; padding-bottom: 0; }
.sp-lesson:first-of-type { padding-top: 0; }
.sp-date {
  width: 48px; flex: 0 0 auto; text-align: center; border-radius: var(--sp-radius-sm);
  background: var(--sp-brand-soft); padding: 6px 0;
}
.sp-date .sp-d { font-size: 17px; font-weight: 800; color: var(--sp-brand); line-height: 1; }
.sp-date .sp-m { font-size: 10px; font-weight: 700; color: var(--sp-brand); text-transform: uppercase; letter-spacing: .04em; }
.sp-date.sp-today { background: var(--sp-brand); }
.sp-date.sp-today .sp-d, .sp-date.sp-today .sp-m { color: #fff; }
.sp-lesson .sp-info { flex: 1 1 auto; min-width: 0; }
.sp-lesson .sp-info strong { font-size: 14.5px; font-weight: 600; display: block; }
.sp-lesson .sp-info span { display: block; color: var(--sp-muted); font-size: 12.5px; margin-top: 1px; }

/* ---------- class rows (full-width, one class per row) ------------------- */

.sp-classrow { display: grid; grid-template-columns: 1fr; gap: 14px; }

.sp-cr-id .sp-cr-subject { font-size: 19px; font-weight: 700; line-height: 1.2; margin: 0 0 5px; }
.sp-cr-meta { display: grid; gap: 4px 18px; font-size: 13.5px; }
.sp-cr-meta div { display: flex; gap: 8px; }
.sp-cr-meta dt { color: var(--sp-muted); margin: 0; min-width: 46px; }
.sp-cr-meta dd { margin: 0; color: var(--sp-ink-soft); }
.sp-cr-when { font-size: 16px; font-weight: 700; margin: 0 0 6px; }
.sp-cr-act { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }

@media (min-width: 768px) {
  /* identity | details | actions */
  .sp-classrow { grid-template-columns: minmax(190px, 1.1fr) minmax(220px, 1.3fr) auto; gap: 26px; align-items: start; }
  .sp-cr-act { justify-content: flex-end; }
}
@media (max-width: 767px) {
  .sp-cr-act .sp-btn { flex: 1 1 auto; }
}

/* ---------- buttons ------------------------------------------------------ */

.sp-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border: 1px solid transparent; background: var(--sp-brand); color: #fff;
  font-family: inherit; font-size: 14px; font-weight: 600; line-height: 1.2;
  padding: 10px 16px; border-radius: var(--sp-radius-sm); cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.sp-btn:hover, .sp-btn:focus { background: #17507F; color: #fff; text-decoration: none; }
.sp-btn-ghost { background: var(--sp-surface); color: var(--sp-brand); border-color: var(--sp-line-strong); }
.sp-btn-ghost:hover, .sp-btn-ghost:focus { background: var(--sp-brand-soft); color: var(--sp-brand); }
.sp-btn-coral { background: var(--sp-coral); }
.sp-btn-coral:hover, .sp-btn-coral:focus { background: #A93B33; }
.sp-btn-sm { font-size: 13px; padding: 7px 12px; }
.sp-btn[disabled], .sp-btn.sp-off {
  background: var(--sp-line); color: var(--sp-muted); cursor: not-allowed;
  border-color: var(--sp-line-strong);
}
.sp-root a:focus-visible, .sp-root button:focus-visible {
  outline: 2px solid var(--sp-brand); outline-offset: 2px;
}

.sp-why { font-size: 12.5px; color: var(--sp-coral); margin: 8px 0 0; }

/* ---------- class switcher ----------------------------------------------- */

.sp-switch { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 16px; }
.sp-switch a {
  font-size: 13.5px; font-weight: 600; text-decoration: none;
  color: var(--sp-ink-soft); background: var(--sp-surface);
  border: 1px solid var(--sp-line); border-radius: 20px; padding: 7px 15px;
}
.sp-switch a:hover, .sp-switch a:focus { border-color: var(--sp-brand-mid); color: var(--sp-brand); }
.sp-switch a.sp-on { background: var(--sp-brand); border-color: var(--sp-brand); color: #fff; }

/* ---------- week table --------------------------------------------------- */

.sp-tablewrap { overflow-x: auto; }
.sp-table { border-collapse: collapse; width: 100%; font-size: 14px; }
.sp-table th {
  text-align: left; font-size: 11px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--sp-muted); padding: 9px 12px 9px 0;
  border-bottom: 1px solid var(--sp-line-strong); white-space: nowrap;
}
.sp-table td {
  padding: 11px 12px 11px 0; border-bottom: 1px solid var(--sp-line);
  vertical-align: middle;
}
.sp-table tr:last-child td { border-bottom: 0; }
.sp-table td .sp-btn { margin: 2px 6px 2px 0; }
.sp-sub2 { display: block; font-size: 11.5px; color: var(--sp-muted); margin-top: 2px; }
/* Only for short, atomic sub-labels such as a week's date range - a range split
   across two lines ("30 Aug -" / "5 Sep") reads as broken. NOT on sp-sub2
   itself: it also carries the homework status, which is a sentence and must be
   free to wrap. */
.sp-sub2.sp-nowrap { white-space: nowrap; }
.sp-dash { color: var(--sp-muted); font-size: 13px; }

/* On a phone the table becomes one block per week, each cell labelled. */
@media (max-width: 700px) {
  .sp-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .sp-table, .sp-table tbody, .sp-table tr, .sp-table td { display: block; width: 100%; }
  .sp-table tr {
    border: 1px solid var(--sp-line); border-radius: var(--sp-radius-sm);
    padding: 10px 12px; margin-bottom: 10px; background: var(--sp-surface);
  }
  .sp-table tr:last-child { margin-bottom: 0; }
  .sp-table td {
    border-bottom: 0; padding: 5px 0;
    display: flex; align-items: baseline; gap: 12px;
  }
  .sp-table td::before {
    content: attr(data-label); flex: 0 0 96px; font-size: 11px; font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase; color: var(--sp-muted);
  }
  .sp-table td:first-child::before { content: ""; flex: 0 0 0; }
  .sp-table td:first-child { display: block; padding-bottom: 8px; }
  .sp-sub2 { display: inline; margin-left: 8px; }
}

/* ---------- misc --------------------------------------------------------- */

/* s27e (philip 2026-09-11, option B): every pill carries a thin outline in its
   own text colour, so a pale pill keeps a crisp edge on any background --
   notably a special course's strong band (FEB's yellow washed the pale fills
   out). Padding drops by the border's 1px so pills stay exactly the same size. */
.sp-pill {
  display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 8px;
  border: 1px solid currentColor;
  border-radius: 20px; white-space: nowrap;
}
.sp-pill-ok    { background: var(--sp-green-soft); color: var(--sp-green); }
.sp-pill-due   { background: var(--sp-amber-soft); color: var(--sp-amber); }
.sp-pill-late  { background: var(--sp-coral-soft); color: var(--sp-coral); }
.sp-pill-info  { background: var(--sp-brand-soft); color: var(--sp-brand); }
.sp-pill-off   { background: var(--sp-line);       color: var(--sp-muted); }

/* A week can hold several worksheets. One is a button; several open in a
   dialogue. Not an inline disclosure: the table scrolls horizontally, so
   anything expanding in place either stretches the row or is clipped by the
   scroller. */
.sp-modal {
  /* Above the admin preview banner, which is drawn by
     newmalden/admin/student_portal_admin_view.php with an inline z-index:1050.
     That banner sits in normal flow, so on an ordinary page it pushes content
     down and covers nothing - but a dialogue is centred in the VIEWPORT, and at
     900 its top slid underneath the banner and lost a line or two of whatever
     it was showing. Most visible on a phone, where the dialogue is tall enough
     to reach the top of the screen. A dialogue is the front-most thing on a
     page; this says so, and leaves room above 1050 rather than sitting one
     apart from it. */
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1200;
  display: flex; align-items: center; justify-content: center; padding: 18px;
}
.sp-modal[hidden] { display: none; }
.sp-modal-back { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: rgba(16,35,55,.45); }
.sp-modal-box {
  position: relative; z-index: 1; background: var(--sp-surface);
  border-radius: var(--sp-radius); box-shadow: 0 12px 40px rgba(20,45,75,.28);
  width: 100%; max-width: 400px; max-height: 80vh; display: flex; flex-direction: column;
}
.sp-modal-head {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 14px; border-bottom: 1px solid var(--sp-line);
}
.sp-modal-head h3 {
  margin: 0; flex: 1 1 auto; font-size: 14px; font-weight: 700; color: var(--sp-ink);
}
.sp-modal-x {
  flex: 0 0 auto; border: 0; background: none; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--sp-muted); padding: 0 2px;
}
.sp-modal-x:hover { color: var(--sp-ink); }
.sp-modal-x { text-decoration: none; }
.sp-modal-body { overflow-y: auto; padding: 6px 0; }

/* The Schedule day dialogue. Same furniture, wider box and padded body: it
   holds a five-column table rather than a list of files. */
.sp-modal-wide .sp-modal-box { max-width: 640px; max-height: 86vh; }
.sp-modal-wide .sp-modal-body { padding: 14px; }

/* The academic week, under the date in the day dialogue's title. sp-sub2 gives
   it the size and the muted colour; the weight has to be taken back off it,
   because it is inside an h3 and would otherwise inherit bold and read as a
   second heading. It stays a block at every width - sp-sub2 turns inline below
   700px for the week tables, where a label belongs beside its value, but here
   that would push a date and a week number onto one line on the narrowest
   screen and neither would be easy to pick out. */
.sp-modal-head h3 .sp-sub2 { font-weight: 500; }
@media (max-width: 700px) {
  .sp-modal-head h3 .sp-sub2 { display: block; margin-left: 0; }
}

.sp-filelist { list-style: none; margin: 0; padding: 0; }
.sp-filelist li + li { border-top: 1px solid var(--sp-line); }
.sp-filelist li a {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 14px; font-size: 13.5px; color: var(--sp-brand); text-decoration: none;
}
.sp-filelist li a:hover { background: var(--sp-brand-soft); }
/* Document glyph, drawn in CSS so there is no image to go missing. */
.sp-filelist li a::before {
  content: ""; flex: 0 0 auto;
  width: 11px; height: 14px; border: 1.5px solid var(--sp-muted); border-radius: 2px;
  box-shadow: inset 0 -3.5px 0 -2.5px var(--sp-muted), inset 0 -7px 0 -6px var(--sp-muted);
}
.sp-filelist .sp-fname { flex: 1 1 auto; word-break: break-word; }
.sp-filelist .sp-fext {
  flex: 0 0 auto; font-size: 10px; font-weight: 700; letter-spacing: .04em;
  color: var(--sp-muted); background: var(--sp-ground); border-radius: 3px; padding: 2px 5px;
}

.sp-money { font-size: 27px; font-weight: 800; letter-spacing: -.02em; line-height: 1.05; }
.sp-money-sub { font-size: 12.5px; color: var(--sp-muted); margin-top: 3px; }

.sp-none { color: var(--sp-muted); font-size: 13.5px; margin: 0; }
.sp-notice { font-size: 14px; color: var(--sp-ink-soft); }
.sp-notice + .sp-notice { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--sp-line); }

.sp-foot {
  margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--sp-line);
  font-size: 12px; color: var(--sp-muted); text-align: center;
}

/* The changeover band. Was the amber dashed "preview" warning; now a quiet
   statement of which portal you are on with the way back to the other one.

   Deliberately NOT amber any more. Amber-and-dashed said "this is unfinished,
   do not rely on it", which was right while nothing linked here and is wrong
   the moment families are sent here on purpose. Brand-soft says "this is a
   notice", which is what it now is.

   Goes on 1 October 2026 with the function that draws it. */
/* THE CHANGEOVER BANNER, LOUDER (`philip`, 2026-08-31). It was a quiet tinted
   strip at 12.5px that read as a footnote, and families were missing both
   halves of what it says: that this is a new portal at all, and that the old
   one is still there. It now carries a solid brand-coloured "New" tag, says the
   thing in a sentence rather than a fragment, and offers the way back as a
   bordered control instead of an underlined word.

   STILL A BANNER, NOT AN ALERT. Brand blue rather than the warn or coral
   palette: nothing is wrong, and a family who meets a red bar on every page
   learns to stop reading banners. It is on every page for one month - see
   student_portal_switch_band() - so it has to be noticeable without being
   tiring. */
/* LOUDER FROM 1 SEPTEMBER 2026, and the weight went into the BAND, not the
   button. The two bands are doing opposite jobs: the one on the old portal is a
   promotion and wants people to move, this one is an escape hatch and only has
   to be findable by someone who wants it. Filling this button would make "Use
   the old portal" the most prominent control on the dashboard - outranking Join
   and Pay - and advertise the way out on every page for a month. So the fill
   deepens, the border goes to the full brand blue at 2px and the type goes up a
   point and a half; the button stays outlined.

   Deliberately NOT a literal in the token block: --sp-brand-soft is shared with
   .sp-nav and the hero, and deepening it there would tint half the portal. */
.sp-flag {
  display: flex; flex-wrap: wrap; align-items: center; gap: 9px 14px;
  background: #D3E5F7; color: var(--sp-brand);
  border: 2px solid var(--sp-brand);
  border-radius: var(--sp-radius-sm); padding: 13px 15px; font-size: 15px;
  margin-bottom: 14px;
}
.sp-flag-tag {
  flex: 0 0 auto; background: var(--sp-brand); color: #fff;
  font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 20px;
}
.sp-flag-say { flex: 1 1 240px; line-height: 1.45; min-width: 0; }
.sp-flag-say b { color: var(--sp-ink); font-weight: 700; }
/* The closing date. Upright, not italic - it is emphasis, not a title. */
.sp-flag-say em { font-style: normal; font-weight: 700; color: var(--sp-brand); }
/* A control, not a word in a sentence: the way back has to be findable by
   someone scanning for it rather than reading. */
.sp-flag-back {
  flex: 0 0 auto; color: var(--sp-brand); font-weight: 700; text-decoration: none;
  border: 2px solid var(--sp-brand); border-radius: 22px;
  padding: 7px 14px; background: var(--sp-surface); white-space: nowrap;
  font-size: 13.5px;
}
.sp-flag-back:hover, .sp-flag-back:focus {
  background: var(--sp-brand); color: #fff; border-color: var(--sp-brand);
}
/* Pushed right where there is room, and full width beneath on a phone. */
@media (min-width: 560px) { .sp-flag-back { margin-left: auto; } }
@media (max-width: 559px) { .sp-flag-back { flex: 1 1 100%; text-align: center; } }

/* ---------- online-payment module compatibility ---------------------------
   The online-invoice-payment module owns the "Pay by bank" control and is the
   single source of truth for it, so this portal includes its partial rather
   than reimplementing payment logic. That partial emits Bootstrap markup, which
   this portal does not load.

   Rather than edit another module, these rules give its exact class names a
   look consistent with the rest of the portal. Everything is scoped under
   .sp-root so nothing leaks to pages that DO load Bootstrap. If the module
   changes its markup, only this block needs revisiting.
   ------------------------------------------------------------------------ */

.sp-root .row        { display: block; }
.sp-root .text-center{ text-align: left; }        /* the portal is left-aligned */
.sp-root .col-md-8   { max-width: 100%; }
.sp-root .mx-auto    { margin-left: 0; margin-right: 0; }
.sp-root .pb-4       { padding-bottom: 0; }
.sp-root .mt-2       { margin-top: 8px; }
.sp-root .mb-0       { margin-bottom: 0; }
.sp-root .mb-1       { margin-bottom: 4px; }
.sp-root .mb-2       { margin-bottom: 8px; }

.sp-root .btn {
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; background: var(--sp-brand); color: #fff;
  font-family: inherit; font-size: 14px; font-weight: 600; line-height: 1.2;
  padding: 10px 16px; border-radius: var(--sp-radius-sm); cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.sp-root .btn:hover, .sp-root .btn:focus { background: #17507F; color: #fff; }
.sp-root .btn-lg { font-size: 15px; padding: 12px 20px; }
.sp-root .btn[disabled] {
  background: var(--sp-line); color: var(--sp-muted);
  border-color: var(--sp-line-strong); cursor: not-allowed;
}

.sp-root .alert {
  border-radius: var(--sp-radius-sm); padding: 12px 14px; margin: 0 0 12px;
  border: 1px solid transparent; font-size: 14px;
}
.sp-root .alert-success { background: var(--sp-green-soft); border-color: var(--sp-green); color: var(--sp-green); }
.sp-root .alert-info    { background: var(--sp-brand-soft); border-color: var(--sp-brand-mid); color: var(--sp-brand); }
.sp-root .alert-warning { background: var(--sp-amber-soft); border-color: var(--sp-amber); color: var(--sp-amber); }
.sp-root .alert-danger  { background: var(--sp-coral-soft); border-color: var(--sp-coral-mid); color: var(--sp-coral); }

.sp-root .border   { border: 1px solid var(--sp-line) !important; }
.sp-root .rounded  { border-radius: var(--sp-radius-sm); }
.sp-root .p-3      { padding: 14px; }
.sp-root .bg-light { background: var(--sp-ground); }
.sp-root .lead     { font-size: 15.5px; margin: 0 0 4px; }
.sp-root small     { font-size: 12.5px; color: var(--sp-muted); }

/* ---------- upcoming sessions (dashboard) -------------------------------- */

.sp-session {
  display: flex; gap: 12px; align-items: center;
  padding: 10px 0; border-bottom: 1px solid var(--sp-line);
}
.sp-session:last-child { border-bottom: 0; padding-bottom: 0; }
.sp-session:first-child { padding-top: 0; }
.sp-session-time {
  flex: 0 0 76px; font-weight: 700; font-size: 14px; color: var(--sp-brand); line-height: 1.2;
}
.sp-session-time span { display: block; font-weight: 500; font-size: 11.5px; color: var(--sp-muted); }
.sp-session-info { flex: 1 1 auto; min-width: 0; }
.sp-session-info strong { display: block; font-size: 14.5px; font-weight: 600; }
.sp-session-info span { display: block; font-size: 12.5px; color: var(--sp-muted); margin-top: 1px; }
/* A column, because an online row carries two controls now — the Join widget
   (or the pill that stands in for it) and the Resources link under it. */
.sp-session-act { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; }
/* Today's lesson resource. A column so a week holding more than one worksheet
   stacks instead of widening the row. */
.sp-session-res { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
@media (max-width: 560px) { .sp-session-res { align-items: flex-start; } }
@media (max-width: 560px) {
  .sp-session { flex-wrap: wrap; }
  .sp-session-act { flex-basis: 100%; margin-top: 6px; align-items: flex-start; }
}

/* ---------- previous classes accordion ----------------------------------- */

.sp-acc { border: 1px solid var(--sp-line); border-radius: var(--sp-radius-sm); margin-bottom: 8px; }
.sp-acc:last-child { margin-bottom: 0; }
.sp-acc > summary {
  cursor: pointer; padding: 11px 14px; font-weight: 600; font-size: 14.5px;
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  list-style: none;
}
.sp-acc > summary::-webkit-details-marker { display: none; }
.sp-acc > summary::after { content: "›"; transform: rotate(90deg); color: var(--sp-muted); font-size: 18px; line-height: 1; }
.sp-acc[open] > summary::after { transform: rotate(-90deg); }
.sp-acc > summary span { font-weight: 500; font-size: 12.5px; color: var(--sp-muted); margin-left: auto; margin-right: 4px; }
.sp-acc > summary:hover { background: var(--sp-ground); }
.sp-acc-body { padding: 0 14px 10px; }
.sp-acc-row {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 0; border-top: 1px solid var(--sp-line);
}
.sp-acc-row > div { flex: 1 1 auto; min-width: 0; }
.sp-acc-row strong { font-size: 14px; font-weight: 600; }

/* ---------- materials filters -------------------------------------------- */

.sp-filters { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; flex-wrap: wrap; }
.sp-filters label {
  font-size: 11.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--sp-muted);
}
.sp-filters select {
  font-family: inherit; font-size: 14px; font-weight: 600; color: var(--sp-ink);
  background: var(--sp-surface); border: 1px solid var(--sp-line-strong);
  border-radius: var(--sp-radius-sm); padding: 8px 12px; cursor: pointer;
}
.sp-filters select:focus-visible { outline: 2px solid var(--sp-brand); outline-offset: 2px; }

/* the mode label inside a subject pill */
.sp-switch a small {
  display: block; font-size: 10.5px; font-weight: 500; opacity: .75; margin-top: 1px;
}

/* --- Join button + its explanation ------------------------------------- */
/* The reason the button is off appears ON HOVER rather than being printed under
   every button all day.

   NOT the title attribute, which is what this was first built with. A native
   tooltip waits about a second before it appears, and a second is long enough
   that a parent has already moved the pointer away and concluded the button is
   simply dead. This is drawn by the page, so it appears on the first frame.

   .sp-joinnote below it is a different thing: the one note that has to be
   readable WITHOUT hovering, because a phone has no hover. Only a locked
   account uses it, and it is empty the rest of the time - hence :empty. */
.sp-join { position: relative; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.sp-joinnote { font-size: 11px; color: var(--sp-muted); line-height: 1.3; text-align: right; max-width: 190px; }
.sp-joinnote:empty { display: none; }

/* The disabled Join button must not take the pointer. Two reasons, and both are
   the point of this block: a disabled button swallows hover, so the wrapper
   would never light the note; and it draws the "no entry" cursor, which tells a
   parent the thing is broken at the exact moment we are trying to tell them it
   is merely waiting. With pointer-events off, the wrapper answers for it. */
.sp-join .sp-btn[disabled], .sp-join .sp-btn.sp-off { pointer-events: none; }
.sp-join, .sp-join form { cursor: default; }

.sp-jointip {
  position: absolute; top: 100%; right: 0; margin-top: 6px; z-index: 30;
  background: var(--sp-ink); color: #fff;
  font-size: 11.5px; font-weight: 500; line-height: 1.35;
  padding: 6px 9px; border-radius: var(--sp-radius-sm);
  /* WIDTH IS max-content, and that is not decoration. An absolutely positioned
     box with right:0 gets its available width from its containing block, and
     the containing block here is .sp-join - which is only as wide as a Join
     button. Left to shrink-to-fit inside that, the note came out one word per
     line. max-content makes it as wide as its own text instead, and the
     max-width caps it so it wraps like a sentence rather than a ribbon.

     The second max-width keeps it on screen on a narrow phone; a browser too
     old for min() ignores the line and keeps the 210px above it. */
  width: max-content;
  max-width: 210px;
  max-width: min(210px, calc(100vw - 32px));
  text-align: left;
  /* opacity rather than display or visibility, so the text stays in the
     accessibility tree and a screen reader still reads the reason out - which
     is what the always-visible line used to do for it. */
  /* No transition. The whole point of replacing the title attribute was that
     waiting to be told is what made the button read as broken, and a fade is
     just a shorter wait. */
  opacity: 0; pointer-events: none;
}
.sp-jointip:empty { display: none; }

/* Guarded on a real pointer. A tap on a touch screen leaves :hover stuck on the
   tapped element in several mobile browsers, which would pin the tip open over
   the row below it with no way to dismiss it - which is also why a tap opens it
   through a class instead, set and cleared by the poller. */
@media (hover: hover) {
  .sp-join:hover .sp-jointip { opacity: 1; }
}
.sp-join:focus-within .sp-jointip { opacity: 1; }

/* Tapped open. A phone has no hover, so without this the reason the button is
   off is unreadable on the device most families use. */
.sp-join.sp-tip-on .sp-jointip { opacity: 1; }
@media (max-width: 640px) {
  .sp-join { align-items: flex-start; }
  .sp-joinnote { text-align: left; max-width: none; }
  /* No hover on a touch screen, so the tip can never be summoned. Left in the
     markup for the accessibility tree, taken out of the layout. */
  .sp-jointip { left: 0; right: auto; }
}

/* DEAD FOR NOW, KEPT ON PURPOSE. Schedule used to nest the same widget inside
   a class row's action column next to the My Work button, and on a narrow
   screen that sibling stretched to the full width so this one had to as well.
   The class list has left Schedule and nothing renders .sp-cr-act any more, but
   these three rules are the whole of what a class row needs from the Join
   widget - deleting them and rediscovering the sizing problem is the expensive
   half. Removable once it is settled that class cards are not coming back.
   (.sp-cr-meta is NOT dead: billing.php still uses it.) */
@media (max-width: 767px) {
  .sp-cr-act .sp-join { flex: 1 1 auto; align-items: stretch; }
  .sp-cr-act .sp-join .sp-btn { width: 100%; }
  .sp-cr-act .sp-joinnote { text-align: left; max-width: none; }
}

/* --- Homework that is open for submission ------------------------------- */
.sp-duelist { display: flex; flex-direction: column; }
.sp-due {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--sp-line);
}
.sp-due:first-child { padding-top: 0; }
.sp-due:last-child  { border-bottom: 0; padding-bottom: 0; }
.sp-due-what { flex: 1 1 auto; min-width: 0; }
.sp-due-what strong { display: block; font-size: 14.5px; font-weight: 600; }
.sp-due-what > span { display: block; font-size: 12.5px; color: var(--sp-muted); margin-top: 1px; }
.sp-due-what .sp-pill { margin-top: 5px; }
.sp-due-when { flex: 0 0 auto; text-align: right; }
.sp-due-when strong { display: block; font-size: 13px; font-weight: 600; color: var(--sp-ink-soft); }
.sp-due-when span   { display: block; font-size: 11.5px; color: var(--sp-muted); }
.sp-due-act { flex: 0 0 auto; }

/* A RESUBMISSION IS NOT A REPLACE (luke, 2026-08-31).
   The row used to differ from an ordinary "you can replace it" only by a pill
   and a button label, so it read as an invitation rather than a request the
   pupil has to answer. It is now a plate of its own: coral edge, coral ground,
   the reason in words and a coral button. Coral is this portal's existing
   warning colour, so nothing new is introduced. */
.sp-due-resub {
  background: var(--sp-coral-soft);
  border-left: 4px solid var(--sp-coral);
  border-bottom-color: transparent;
  border-radius: var(--sp-radius-sm);
  padding: 12px 14px;
  margin: 6px 0;
}
.sp-due-resub + .sp-due { border-top: 1px solid var(--sp-line); }
/* Beats .sp-due:first-child / :last-child, which strip the padding an
   ordinary row does not need and this one does. */
.sp-duelist .sp-due-resub:first-child { padding-top: 12px; }
.sp-duelist .sp-due-resub:last-child  { padding-bottom: 12px; }
.sp-due-resub .sp-due-when strong { color: var(--sp-coral); }
.sp-due-resub .sp-due-when span   { color: var(--sp-coral); font-weight: 600; }

/* The reason, in a sentence. Sits under the pill and above the fold of the
   row on a phone, because it is the part that tells the pupil what to do. */
.sp-due-why {
  margin: 6px 0 0;
  font-size: 12.5px; line-height: 1.5;
  color: var(--sp-ink-soft);
  max-width: 56ch;
}
@media (max-width: 640px) {
  .sp-due { flex-wrap: wrap; }
  .sp-due-when { text-align: left; }
  .sp-due-act { flex-basis: 100%; }
  .sp-due-act .sp-btn { width: 100%; text-align: center; }
}

/* --- "coming soon" cards on the Progress page --------------------------- */
/* Visibly quieter than a live card, so the page reads as a promise rather
   than as four cards that failed to load. */
.sp-soon { background: var(--sp-ground); border-style: dashed; box-shadow: none; }
/* No :has() — older iPad and Android browsers in this audience do not have it,
   so the card carries an explicit class instead. */
.sp-soon > h2, .sp-live > h2 { display: flex; align-items: center; gap: 8px; justify-content: flex-start; }
.sp-soon > h2 .sp-pill, .sp-live > h2 .sp-pill { text-transform: none; letter-spacing: 0; }
/* The "when" line under a coming-soon card. Small and quiet, but in the brand
   colour so it reads as a commitment rather than as more body copy. */
.sp-soon-when { margin: 10px 0 0; font-size: 12.5px; font-weight: 700; color: var(--sp-brand); }

/* --- the embedded invoice ----------------------------------------------- */
/* The frame carries the real invoice's own Bootstrap, so it gets no styling
   here beyond filling the card and having no border of its own. Height is set
   by script; this is only the value before the first measurement lands. */
.sp-invoice-host { overflow: hidden; }
.sp-invoice-frame { display: block; width: 100%; height: 620px; border: 0; }

/* --- the Schedule calendar ----------------------------------------------- */
/* A month grid built from CSS grid rather than a table: every row is one
   Sunday-to-Saturday academic week, with the week number in a narrow gutter to
   the left. See schedule.php for why the week starts on Sunday. */
/* The month title is NOT a direct child of .sp-card, so `.sp-card > h2` never
   reached it and it fell back to the browser's default 24px bold. At that size
   "September 2026" plus the three buttons was wider than a handset, and
   flex-wrap put the buttons on a second row - but only for the long month
   names, so the header jumped for September, November and December and stayed
   put for the rest. Sized explicitly, and never allowed to wrap. */
.sp-cal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: nowrap;
}
.sp-cal-title { margin: 0; font-size: 21px; font-weight: 700; line-height: 1.2; white-space: nowrap; }
.sp-cal-mshort { display: none; }
.sp-cal-nav { flex: 0 0 auto; }
.sp-cal-nav { display: flex; gap: 6px; }

/* The grid can be wider than a phone. It scrolls inside its own box rather
   than making the whole page scroll sideways. */
.sp-caltable { overflow-x: auto; margin-top: 14px; -webkit-overflow-scrolling: touch; }
.sp-cal {
  /* The week-number gutter, as a variable because the holiday bars are placed
     with calc() against it. Hard-coding 34px in both places meant narrowing the
     gutter for a phone silently slid every bar off its days. */
  --sp-gut: 34px;
  min-width: 640px; border: 1px solid var(--sp-line); border-radius: 10px; overflow: hidden;
}
/* minmax(0, 1fr), NOT 1fr. Each row is its own grid, and a bare 1fr means
   minmax(auto, 1fr) - the auto floor is the column's min-content, which a
   nowrap chip inflates. One long subject name in one cell therefore widened
   that column and narrowed the other six IN THAT ROW ONLY, so the row stopped
   lining up with the rest of the month. Measured on live: six rows at 143.7px
   per column and the seventh at 132.1px with Saturday at 213.3px. A zero floor
   lets the chip ellipsis do its job instead. */
.sp-cal-row { display: grid; grid-template-columns: var(--sp-gut) repeat(7, minmax(0, 1fr)); position: relative; }
.sp-cal-row + .sp-cal-row { border-top: 1px solid var(--sp-line); }

.sp-cal-wk {
  display: flex; align-items: center; justify-content: center;
  background: var(--sp-ground); color: var(--sp-muted);
  font-size: 11.5px; font-weight: 700; border-right: 1px solid var(--sp-line);
}

.sp-cal-dow { background: var(--sp-ground); }
.sp-cal-dow .sp-cal-cell {
  padding: 7px 6px; text-align: center; font-size: 11.5px; font-weight: 700;
  color: var(--sp-muted); text-transform: uppercase; letter-spacing: .05em; min-height: 0;
}
.sp-cal-dow abbr { text-decoration: none; border: 0; }

.sp-cal-cell { min-height: 92px; border-left: 1px solid var(--sp-line); }
.sp-cal-cell:first-child { border-left: 0; }

/* The whole cell is the target, so a tap anywhere on the day opens it. */
.sp-cal-day { display: block; height: 100%; padding: 5px 5px 7px; text-decoration: none; color: inherit; }
.sp-cal-day:hover { background: var(--sp-ground); }
.sp-cal-num { display: block; font-size: 12.5px; font-weight: 700; color: var(--sp-ink-soft); margin-bottom: 3px; }

/* Days from the neighbouring months stay visible - a week is easier to read
   whole - but recede so the month in view is obvious. */
.sp-cal-out { background: #FAFCFE; }
.sp-cal-out .sp-cal-num { color: var(--sp-muted); font-weight: 400; }
.sp-cal-closed { background: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(107,126,147,.07) 5px, rgba(107,126,147,.07) 10px); }

/* A holiday course, drawn as a bar over the days it actually covers.
   Absolutely placed rather than given grid columns of its own: a grid item that
   claims columns pushes the day cells out of row 1, and the cells must keep
   their places. The gutter is a fixed 34px and the seven days share the rest,
   so left/width in calc() land exactly on the day boundaries at any width.
   pointer-events: none so the day underneath stays clickable. */
.sp-cal-hol {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--sp-brand);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1.55;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  top: calc(26px + var(--sp-bar-i, 0) * 21px);
}
/* Room for the bars between the date and the first lesson, in EVERY cell of the
   row - including the days the bar does not cover - so the lessons across a row
   stay on one line. --sp-bars is set on the row by the page. */
.sp-cal-row-hol .sp-cal-num { margin-bottom: calc(5px + var(--sp-bars, 1) * 21px); }
.sp-cal-today .sp-cal-num { color: var(--sp-brand); }
.sp-cal-today .sp-cal-num::after { content: ""; display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--sp-brand); margin-left: 4px; vertical-align: middle; }
.sp-cal-sel { box-shadow: inset 0 0 0 2px var(--sp-brand); }

.sp-cal-tag { display: block; font-size: 10.5px; color: var(--sp-muted); margin-bottom: 3px; }

/* One chip per session. Kept to a single line so a busy day stays readable;
   the day panel below carries the full detail. */
.sp-chip {
  display: block; font-size: 11px; line-height: 1.35; padding: 2px 5px; margin-bottom: 3px;
  border-radius: 5px; background: rgba(43,90,160,.09); color: var(--sp-ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sp-chip b { font-weight: 700; }
.sp-chip-off { background: var(--sp-line); color: var(--sp-muted); text-decoration: line-through; }

/* Moving month swaps this region's contents rather than reloading. The old
   month leaves towards the direction of travel and the new one arrives from the
   far side, which is what makes it read as movement rather than a flicker. */
.sp-cal-region { transition: opacity .13s ease, transform .13s ease; }
.sp-cal-region.sp-cal-leaving  { opacity: 0; transform: translateX(var(--sp-dir, 0px)); }
.sp-cal-region.sp-cal-entering { opacity: 0; transform: translateX(calc(var(--sp-dir, 0px) * -1)); transition: none; }

@media (prefers-reduced-motion: reduce) {
  .sp-cal-region,
  .sp-cal-region.sp-cal-leaving,
  .sp-cal-region.sp-cal-entering { transition: none; transform: none; }
}

.sp-sect { margin: 6px 0 -4px; font-size: 11.5px; font-weight: 700; letter-spacing: .07em;
            text-transform: uppercase; color: var(--sp-muted); }

.sp-cal-key { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0 0; }
.sp-cal-key .sp-chip { display: inline-block; margin: 0; }

/* ---------- the calendar on a phone --------------------------------------
   A 640px grid inside a sideways scroller is not something anyone can read on a
   handset: two thirds of the month is off-screen and the lesson names are too
   narrow to finish a word. Below 560px it becomes what every phone calendar is
   - a compact month that fits the screen, one dot per lesson instead of its
   name, and the day itself opening for the detail.

   Nothing is lost by dropping to dots. The names were unreadable at this width
   anyway, and the day dialogue already lists every lesson in full with its
   time, room and tutor. */
@media (max-width: 560px) {
  .sp-cal-nav .sp-btn span { display: none; }   /* arrows only on a phone */

  /* "Sep 2026" rather than "September 2026". Even at 17px the long spelling
     plus the buttons overruns a 320px handset, and an abbreviated month is
     what every phone calendar shows anyway. */
  .sp-cal-head  { gap: 8px; }
  .sp-cal-title { font-size: 17px; }
  .sp-cal-mlong  { display: none; }
  .sp-cal-mshort { display: inline; }

  .sp-caltable { overflow-x: visible; }
  .sp-cal { min-width: 0; --sp-gut: 19px; }

  .sp-cal-cell { min-height: 54px; }
  .sp-cal-day  { padding: 4px 1px 5px; text-align: center; }
  /* A fixed height on EVERY number, not just today's, so the circle below can
     exist without pushing that one cell's lessons out of line with the row. */
  .sp-cal-num  { font-size: 12.5px; height: 22px; line-height: 22px; margin-bottom: 3px; }
  .sp-cal-wk   { font-size: 9px; }

  /* Today is a filled circle round the date, the way every phone calendar
     marks it. The laptop's small dot beside the number is dropped here: next to
     the lesson dots it read as a fourth lesson. */
  .sp-cal-today .sp-cal-num::after { display: none; }
  .sp-cal-today .sp-cal-num {
    width: 22px; border-radius: 50%; margin-left: auto; margin-right: auto;
    background: var(--sp-brand); color: #fff;
  }
  .sp-cal-dow .sp-cal-cell { padding: 6px 0; font-size: 9.5px; letter-spacing: .02em; }
  .sp-cal-tag  { display: none; }   /* the hatched background already says it */

  /* One dot per lesson. font-size 0 collapses the time and subject inside the
     chip without hiding the element, so the dot keeps the chip's own colour
     and its title attribute still works on a long press. */
  .sp-cal-day .sp-chip {
    display: inline-block; vertical-align: middle;
    width: 6px; height: 6px; min-width: 0; max-width: none;
    margin: 1px; padding: 0; border-radius: 50%;
    background: var(--sp-brand-mid); color: transparent;
    font-size: 0; line-height: 0; overflow: hidden;
  }
  .sp-cal-day .sp-chip-off    { background: var(--sp-muted); }

  .sp-cal-hol {
    font-size: 9px; padding: 0 5px; line-height: 1.65;
    top: calc(28px + var(--sp-bar-i, 0) * 17px);
  }
  .sp-cal-row-hol .sp-cal-num { margin-bottom: calc(3px + var(--sp-bars, 1) * 19px); }

  /* The key has to speak the same language as the grid, so it shows the dots
     rather than the chips the grid no longer draws. */
  .sp-cal-key { gap: 14px; }
  .sp-cal-key .sp-chip {
    background: none; padding: 0; font-size: 11px;
    color: var(--sp-ink-soft); text-decoration: none;
  }
  .sp-cal-key .sp-chip::before {
    content: ""; display: inline-block; vertical-align: middle;
    width: 6px; height: 6px; border-radius: 50%; margin-right: 5px;
    background: var(--sp-brand-mid);
  }
  .sp-cal-key .sp-chip-off::before    { background: var(--sp-muted); }
}

/* --- absence requests on the Schedule calendar ---------------------------- */
/* Requested but not yet confirmed by the office: deliberately NOT struck
   through. Only a logged absence gets that, because struck through reads as
   "settled" and a request is not settled until someone acts on it. */

/* A tickable lesson. The chip IS the label, so the whole thing is a target -
   a 11px checkbox on a phone is not. */


/* ---------- Special Courses ----------------------------------------------
   The landing page at /students/special_courses.php and its confirmation view. Added
   by the special-course-bookings feature's sub-stage s24; see that page's own
   docblock for why it lives in this module rather than in that one.

   Only two components are new — a course card and a booking tile. Everything
   else on the page is this file's existing vocabulary (.sp-card, .sp-grid,
   .sp-pill, .sp-chip, .sp-btn, .sp-sect, .sp-alert, .sp-table), untouched.

   PER-COURSE COLOUR. Each card carries three custom properties set inline by
   the page: --c-strong, --c-soft and --c-ink. They come from
   special_course_bookings_email_palette(), the same lookup that paints the
   booking form and the confirmation email, so one course looks like itself
   across all three. NO HEX FROM THAT TABLE APPEARS IN THIS FILE, and none may
   be added: a new course colour is a one-line change in that one function.
   --c-ink is the text colour that palette specifies for its own strong
   colour, so the Book button is never given a white that the palette did not
   ask for.
   -------------------------------------------------------------------------- */

.sp-sc-course, .sp-sc-booked {
  padding: 0;          /* the band is flush to the card edge */
  overflow: hidden;    /* so the band's corners follow the card's radius */
  /* The grid stretches the WRAPPER, not the card inside it. Without this a
     course with one venue stops short of one with two, and their Book buttons
     land at different heights in the same row. */
  height: 100%;
}

.sp-sc-booked {
  text-decoration: none; color: inherit;
  transition: box-shadow .13s ease, transform .13s ease;
}
.sp-sc-booked:hover {
  box-shadow: 0 2px 4px rgba(20,45,75,.07), 0 10px 24px rgba(20,45,75,.09);
  transform: translateY(-1px);
}
.sp-sc-booked:focus-visible { outline: 3px solid var(--c-strong); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .sp-sc-booked { transition: none; }
  .sp-sc-booked:hover { transform: none; }
}

/* the name band -----------------------------------------------------------
   s27e (special-course-bookings decision 27, philip 2026-09-11): the band is
   the course's STRONG colour with the palette's own ink as text, the same
   pairing as the confirmation email's hero and label bars and the booking
   wizard's card heads. It used to be the pale soft colour, so one course
   looked like two different colours depending on where a parent saw it.
   The strong bottom rule is REMOVED rather than recoloured: it now matched
   the band itself, and the card's own --sp-line border already edges it.
   A status pill in the band keeps its own opaque pale background, so its
   text never depends on the course colour behind it. */

.sp-sc-band {
  background: var(--c-strong); padding: 12px 16px;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.sp-sc-band-name { font-size: 15.5px; font-weight: 700; color: var(--c-ink); }
/* Full ink, no opacity: fading it would push the dates on the lighter
   courses (OCT, EIC, SIC) even further below the ink-on-strong contrast luke
   accepted in s23 D3. Size and weight already make it the quieter line. */
.sp-sc-band-when {
  font-size: 12.5px; font-weight: 600; color: var(--c-ink); white-space: nowrap;
}
/* A status pill is a box, not a line of text, so it centres rather than
   sitting on the name's baseline. */
.sp-sc-band-tile { align-items: center; }

/* course card ------------------------------------------------------------- */

.sp-sc-body  { padding: 14px 16px 16px; }
.sp-sc-blurb { margin: 14px 16px 0; font-size: 14px; color: var(--sp-ink-soft); line-height: 1.55; }

.sp-sc-venues { margin: 12px 16px 0; display: flex; flex-direction: column; }
.sp-sc-venue {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 0; border-top: 1px solid var(--sp-line); font-size: 13.5px;
}
.sp-sc-venue b    { display: block; font-weight: 600; color: var(--sp-ink); }
.sp-sc-venue span { font-size: 12.5px; color: var(--sp-muted); }
.sp-sc-venue em   {
  font-style: normal; font-weight: 700; color: var(--sp-ink);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* `margin-top: auto` is what pins the button to the foot of a stretched card,
   so two cards of different content depth still line their buttons up. */
.sp-sc-act {
  margin: auto 16px 0; padding: 14px 0 16px;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
}
.sp-sc-book { background: var(--c-strong); color: var(--c-ink); border-color: var(--c-strong); }
/* A flat dark overlay darkens any accent without this file needing to know
   what colour it is; opacity would read as the button switching off on the
   lighter courses. */
.sp-sc-book:hover { background-image: linear-gradient(rgba(0,0,0,.13), rgba(0,0,0,.13)); }
.sp-sc-dep { font-size: 12.5px; color: var(--sp-muted); }

/* booking reference ------------------------------------------------------- */

.sp-sc-ref {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; font-weight: 700; color: var(--sp-ink);
}

.sp-sc-sect2 { margin-top: 24px; }

/* A grid ITEM is not a grid, so two panels stacked inside one column sit flush
   against each other. This gives that column the same rhythm .sp-grid gives
   the page. */
.sp-sc-side { display: flex; flex-direction: column; gap: var(--sp-gap); }

/* ---------- Enrolment booking -------------------------------------------
   The class picker at /students/enrolment_booking.php.
   students/new_academic_year_booking_2026.php keeps booking_NAY.css and is
   untouched -- it stays live as the old portal's backstop through September.

   THIS IS A PORT, not a redesign. The layout, the two-step wizard, the
   collapsed cards, the date pills, the fixed bottom bar and the selection
   sheet are all the current picker's own reskin layer, brought across so the
   experience is preserved by construction rather than by re-derivation
   (`philip`, 2026-08-28: keep the experience the same).

   What changed in the crossing is the palette: its base variables are mapped
   onto this file's --sp-* tokens, so the picker wears the portal's blue inside
   the portal's header. THE TEN SUBJECT HUES ARE CARRIED OVER UNTOUCHED -- they
   are categorical, they are what a family reads the timetable by, and they are
   the same hues the current page uses.

   Class names are load-bearing here in a way they are not elsewhere in this
   file: .title, .span, .checkbox and the subject keys are what the page's
   script looks for. Renaming one breaks a behaviour, not a style.
   -------------------------------------------------------------------------- */

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

.sp-root {
  /* Mapped from the current picker's own names, left in the comments so the
     two can be compared line for line. */
  --n-surface:   var(--sp-surface);        /* --surface   #ffffff */
  --n-surface-2: var(--sp-ground);         /* --surface-2 #f5f7fb */
  --n-ink:       var(--sp-ink);            /* --ink       #16202e */
  --n-ink-2:     var(--sp-ink-soft);       /* --ink-2     #3d4a5c */
  --n-muted:     var(--sp-muted);          /* --ink-muted #67758c */
  --n-line:      var(--sp-line);           /* --line      #dde3ec */
  --n-line-2:    var(--sp-line-strong);    /* --line-strong #ccd5e1 */
  --n-accent:    var(--sp-brand);          /* --accent    #2f6bd4 -> portal blue */
  --n-accent-2:  #17507F;                  /* --accent-strong, the portal's own hover */
  --n-accent-sf: var(--sp-brand-soft);     /* --accent-soft */
  --n-accent-ink:#FFFFFF;
  --n-good:      #2c9c68;  --n-good-sf: #e4f4ec;
  --n-warn:      #c9861c;  --n-warn-sf: #fbefd9;

  /* Categorical subject hues -- carried over exactly. */
  --s-Maths:#e0348c; --s-English:#2563eb; --s-Science:#16a34a;
  --s-ProblemSolving:#f5811f; --s-Literacy:#8b3fc9; --s-VRandNVR:#0ea5c4;
  --s-CreativeandEssayWriting:#d62828; --s-ReadingComprehension:#c99700;
  --s-EssayWriting:#4338ca; --s-WritingSkillsandReadingComprehension:#0d8a6e;
}

/* Clear the fixed bar at every width. */
.sp-root { padding-bottom: 78px; }

/* ---- step 1: centre and subjects ---------------------------------------- */

.nay26-h2 { margin-top: 22px; }
.nay26-hint { margin: 6px 0 0; }
.nay26-compare { margin: 12px 0 0; font-size: 13.5px; color: var(--n-ink-2); line-height: 1.6; }

.nay26-centres { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.nay26-centre {
  font: inherit; text-align: left; cursor: pointer;
  padding: 12px 14px; min-height: 48px;
  border: 1.5px solid var(--n-line); border-radius: var(--sp-radius-sm);
  background: var(--n-surface); color: var(--n-ink);
}
.nay26-centre b    { display: block; font-size: 14.5px; font-weight: 700; }
.nay26-centre span { display: block; font-size: 12.5px; color: var(--n-muted); margin-top: 2px; }
.nay26-centre:hover { border-color: var(--n-line-2); }
.nay26-centre.is-active { border-color: var(--n-accent); background: var(--n-accent); }
.nay26-centre.is-active b, .nay26-centre.is-active span { color: var(--n-accent-ink); }
/* The tick the current page puts on the chosen centre. */
.nay26-centre.is-active b::before { content: "\2713\00a0\00a0"; font-weight: 800; }
.nay26-centre:focus-visible { outline: 3px solid var(--n-accent); outline-offset: 2px; }

/* A CENTRE WITH NOTHING FOR THIS PUPIL'S YEAR. Selected on [disabled] and not on
   a class, because paintCentre() rebuilds this button's className from scratch
   on every repaint and would wipe a class the first time anything was clicked.

   FADED, NOT HIDDEN. Removing the box would leave a family wondering whether we
   teach at Sutton at all; leaving it visible and plainly off answers "not for
   your year" instead. The hover lift is cancelled too - a box that lights up
   under the cursor and then refuses the click reads as a fault.

   The note keeps full contrast while the rest of the box drops to 55%: it is
   the one thing on a disabled box worth reading, and dimming the reason along
   with everything else is how you end up with an explanation nobody can see. */
.nay26-centre[disabled] {
  cursor: not-allowed; opacity: .55;
  background: var(--n-surface-2); border-style: dashed;
}
.nay26-centre[disabled]:hover { border-color: var(--n-line); }
.nay26-centre[disabled] .nay26-centre-none {
  opacity: 1; font-weight: 600; color: var(--n-ink-2);
}

/* A subject pill, with its own colour as a dot -- the same colour its classes
   carry on the timetable, so the two are read as one system. */
.nay26-subjects { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 12px; }
.nay26-subj {
  display: inline-flex; align-items: center; cursor: pointer;
  padding: 10px 14px; min-height: 44px; border-radius: 999px;
  border: 1.5px solid var(--n-line); background: var(--n-surface);
  font-size: 13.5px; font-weight: 600; color: var(--n-ink-2);
}
.nay26-subj input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.nay26-subj span::before {
  content: ""; display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--sc, var(--n-accent)); margin-right: 8px; vertical-align: middle;
}
.nay26-subj:hover { border-color: var(--n-line-2); }
/* .is-on is set by the page's script; :has() is kept alongside it so the state
   is also correct for the instant before the script runs. */
.nay26-subj.is-on,
.nay26-subj:has(input:checked) { border-color: var(--n-accent); background: var(--n-accent-sf); color: var(--n-accent); }
.nay26-subj:focus-within { outline: 3px solid var(--n-accent); outline-offset: 2px; }

.nay26-subj.Maths { --sc: var(--s-Maths); }
.nay26-subj.English { --sc: var(--s-English); }
.nay26-subj.Science { --sc: var(--s-Science); }
.nay26-subj.ProblemSolving { --sc: var(--s-ProblemSolving); }
.nay26-subj.Literacy { --sc: var(--s-Literacy); }
.nay26-subj.VRandNVR { --sc: var(--s-VRandNVR); }
.nay26-subj.CreativeandEssayWriting { --sc: var(--s-CreativeandEssayWriting); }
.nay26-subj.ReadingComprehension { --sc: var(--s-ReadingComprehension); }
.nay26-subj.EssayWriting { --sc: var(--s-EssayWriting); }
.nay26-subj.WritingSkillsandReadingComprehension { --sc: var(--s-WritingSkillsandReadingComprehension); }

/* ---- step 2: the timetable ---------------------------------------------- */

.nay26-daytabs { display: none; }
.nay26-scrollhint { margin: 10px 0 0; }

.nay26-gridwrap { margin-top: 10px; }
/* table-layout:fixed is what guarantees the columns. Without it a browser
   sizes each one to its content, so a day with a long tutor name steals width
   from the rest and the whole grid shifts every time the filters change. With
   it, the time column is a fixed 68px and the six days split what is left --
   equal, stable, and unable to overflow. */
.nay26-grid { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; }
.nay26-grid th, .nay26-grid td {
  border-bottom: 1px solid var(--n-line); padding: 4px; vertical-align: top; text-align: left;
}
.nay26-corner { width: 68px; }
.table-content {
  background: var(--n-surface-2); color: var(--n-ink-2); font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em; font-size: .78rem; text-align: center;
}
.table-col-1 {
  width: 68px; background: var(--n-surface-2); color: var(--n-ink-2); font-weight: 650;
  font-variant-numeric: tabular-nums; white-space: nowrap; border-right: 1px solid var(--n-line);
  font-size: .78rem;
}

/* ---- one class ---------------------------------------------------------- */

/* The 4px left edge IS the subject colour. It is the fastest thing to read on
   a dense grid and the only cue that survives at a glance. */
.title {
  color: var(--n-ink); background: var(--n-surface); text-align: center;
  border: 1.5px solid var(--n-line); border-left: 4px solid var(--sc, var(--n-accent));
  border-radius: var(--sp-radius-sm); padding: 6px 8px; margin: 3px 0;
  box-shadow: var(--sp-shadow); transition: box-shadow .16s, border-color .16s;
  /* min-width is gone: inside a fixed-layout column it would be the one thing
     able to force the table wider than its container. */
  overflow-wrap: anywhere;
}
.title:hover { box-shadow: 0 2px 4px rgba(20,45,75,.07), 0 10px 24px rgba(20,45,75,.09); }
/* Title and subtitle, not two paragraphs. The tutor sits directly under the
   subject on its own line with a hair of separation. */
.title strong { display: block; color: var(--n-ink); font-weight: 700; font-size: .84rem; line-height: 1.2; }
.title span.nay26-tutor { display: block; margin-top: 0; color: var(--n-ink-2); font-weight: 500; font-size: .72rem; line-height: 1.25; }

.title.Maths, .title.Mathswaiting { --sc: var(--s-Maths); }
.title.English, .title.Englishwaiting { --sc: var(--s-English); }
.title.Science, .title.Sciencewaiting { --sc: var(--s-Science); }
.title.ProblemSolving, .title.ProblemSolvingwaiting { --sc: var(--s-ProblemSolving); }
.title.Literacy, .title.Literacywaiting { --sc: var(--s-Literacy); }
.title.VRandNVR, .title.VRandNVRwaiting { --sc: var(--s-VRandNVR); }
.title.CreativeandEssayWriting, .title.CreativeandEssayWritingwaiting { --sc: var(--s-CreativeandEssayWriting); }
.title.ReadingComprehension, .title.ReadingComprehensionwaiting { --sc: var(--s-ReadingComprehension); }
.title.EssayWriting, .title.EssayWritingwaiting { --sc: var(--s-EssayWriting); }
.title.WritingSkillsandReadingComprehension, .title.WritingSkillsandReadingComprehensionwaiting { --sc: var(--s-WritingSkillsandReadingComprehension); }

/* The native checkbox is hidden -- the date pills ARE the control. */
.title input.checkbox { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
.title label { margin: 0; display: inline; }

.title .span {
  display: inline-block; cursor: pointer; border: 1.5px solid var(--n-line);
  border-radius: 8px; padding: 6px 12px; margin: 4px 5px 0 0;
  font-size: .8rem; font-weight: 600; color: var(--n-ink-2);
  font-variant-numeric: tabular-nums; transition: border-color .14s, background .14s, color .14s;
}
.title .span:hover { border-color: var(--n-line-2); }
.title .span.nay26-pill-on { border-color: var(--n-accent); background: var(--n-accent); color: var(--n-accent-ink); }

/* NO "Available" BADGE (`philip`, 2026-08-28). It was drawn by CSS on every
   card that was not full, so it appeared on the large majority of them and
   distinguished nothing -- while costing 32px of height on each. Available is
   the default state and is now simply the absence of a badge. Only the
   exception is labelled. */
.fully_booked_classes span.nay26-fullwl {
  display: inline-block; color: #7a4d00; background: var(--n-warn-sf);
  font-weight: 650; font-size: .66rem; padding: 1px 6px; border-radius: 6px; margin-top: 2px;
  line-height: 1.4;
}
/* The tutor and the badge share a line. On its own row the badge repeated the
   same fault as the subject/tutor gap -- a third stacked line on the tallest
   cards in the grid, and rows are sized by their tallest card. */
.fully_booked_classes span.nay26-tutor { display: inline; }
.fully_booked_classes span.nay26-tutor::after { content: " "; }
.title.nay26-sel { border-color: var(--n-accent); box-shadow: 0 0 0 2px var(--n-accent), var(--sp-shadow); }

/* Collapse and expand. An available card shows subject and tutor only; picking
   it reveals the start dates below, and because the group re-centres as they
   grow the name lifts -- which is the whole motion. */
.title:not(.fully_booked_classes) { display: flex; flex-direction: column; justify-content: center; min-height: 42px; }
.title:not(.fully_booked_classes):not(.nay26-sel) { cursor: pointer; }
.nay26-dates {
  overflow: hidden; max-height: 0; opacity: 0; padding: 0; text-align: center;
  transition: max-height .26s ease, opacity .18s ease, padding .26s ease;
}
.nay26-datetitle {
  font-size: .66rem; font-weight: 700; color: var(--n-ink-2);
  letter-spacing: .04em; text-transform: uppercase; margin-bottom: 4px;
}
.title.nay26-sel .nay26-dates {
  max-height: 320px; opacity: 1; padding: 6px 8px; margin-top: 5px;
  background: var(--n-surface-2); border: 1px solid var(--n-line); border-radius: var(--sp-radius-sm);
}
/* THE DATES SIT IN A ROW. Each start date is its own <label>, which stacked
   them one per line and made a selected card 163px -- taller than the three
   cards around it put together, and the row is sized by its tallest card.

   NOWRAP. With flex-wrap the two pills dropped onto separate lines as soon as a
   column got narrow -- which on a 1280 or 1366 laptop is most of them -- and the
   card a family is looking hardest at was the tallest thing in the grid. They
   shrink to share the line instead.

   THE ROW IS AN INNER ELEMENT, not .nay26-dates itself. Put the flex on the
   block and the caption above the pills becomes a third flex item, taking a
   share of the width and squashing the two dates to slivers - which is exactly
   what happened the first time the caption went back in. Anything that belongs
   ABOVE the pills goes in .nay26-dates; only the pills go in .nay26-daterow. */
.title.nay26-sel .nay26-daterow {
  display: flex; flex-wrap: nowrap; gap: 4px; justify-content: center; align-items: stretch;
}
.title.nay26-sel .nay26-dates label { flex: 1 1 0; min-width: 0; }
.title.nay26-sel .nay26-dates .span {
  /* Sharing a line makes each pill narrow; a min-height keeps it a target
     rather than a 15px sliver. */
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 28px; padding: 4px; margin: 0;
  font-size: .74rem; white-space: nowrap; text-align: center;
}
/* SHOWN, ON ITS OWN ROW, ON ONE LINE AT EVERY WIDTH.
   It was hidden before on the grounds that two dated pills say what they are.
   They do not: a card opens to "16 Sep" and "23 Sep" with no wording near them,
   which reads as information about the class rather than a question being put.

   nowrap is the whole point - a caption that wraps to two lines is what made
   this look broken the first time - so the text is sized and, in the tablet
   band, shortened to guarantee it. See the two spellings in the markup.

   SENTENCE CASE AND NO TRACKING, unlike the collapsed style it inherits. This
   is an instruction rather than a label, so it reads better unshouted, and the
   uppercase and .04em letter-spacing were spending width this cannot spare.

   The cost is real and was why it went: a grid row is as tall as its tallest
   card, so this puts about thirteen pixels on any row holding a selected one.
   Being understood is worth thirteen pixels. */
.title.nay26-sel .nay26-datetitle {
  display: block; white-space: nowrap; overflow: hidden;
  text-transform: none; letter-spacing: 0; font-weight: 600;
  font-size: .58rem; line-height: 1.25; margin-bottom: 4px;
}
.nay26-dt-short { display: none; }

/* THE NARROW BAND, and only it. Measured against this stylesheet at 9.28px:
   "Choose start date" is 85px and the box inside a card gives 134px at the
   layout's 1120px maximum, 112px at a 1024px viewport, 101px at 960 and 81px at
   800. So the long spelling runs out somewhere in the 800s.

   The switch is at 960 rather than at the crossover, to leave about 16px rather
   than about 4px - a caption sized to the pixel is one font substitution away
   from wrapping, and wrapping is the whole thing this is avoiding.

   Below 721px the grid shows one day across the full screen, so the long
   spelling is back and can be read at a normal size. */
@media (min-width: 721px) and (max-width: 959px) {
  .nay26-dt-long  { display: none; }
  .nay26-dt-short { display: inline; }
}

.title.nay26-sel .nay26-dates label { display: inline-flex; }
@media (prefers-reduced-motion: reduce) { .nay26-dates { transition: none; } }

.nay26-nonehere { margin-top: 12px; }

/* ---- the fixed bottom bar ----------------------------------------------- */

.nay26-sumbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1030;
  background: var(--n-surface); border-top: 1px solid var(--n-line);
  box-shadow: 0 -6px 22px rgba(20,32,48,.12);
  padding: 9px 16px calc(9px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 10px;
  /* Promotes the bar to its own layer so iOS keeps it pinned during a scroll
     gesture instead of letting it ride up and snap back at scroll-end. */
  transform: translateZ(0); will-change: transform;
}
.nay26-sb-info {
  flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start;
  text-align: left; background: none; border: 0; cursor: pointer; padding: 4px 2px;
  font: inherit; line-height: 1.25;
}
.nay26-sb-count { font-weight: 700; font-size: .95rem; color: var(--n-ink); }
.nay26-sb-sub   { font-size: .8rem; color: var(--n-muted); }
.nay26-sb-continue {
  flex: none; cursor: pointer; font: inherit; font-size: .95rem; font-weight: 690;
  color: var(--n-accent-ink); background: var(--n-accent); border: 0;
  border-radius: var(--sp-radius-sm); padding: 12px 22px; min-height: 44px;
}
.nay26-sb-continue:hover { background: var(--n-accent-2); }
.nay26-sb-continue:disabled { opacity: .5; cursor: not-allowed; }
@media (min-width: 721px) { .nay26-sumbar { padding-left: 24px; padding-right: 24px; } }

/* ---- the selection sheet ------------------------------------------------ */

.nay26-sumback {
  position: fixed; inset: 0; z-index: 1055; background: rgba(10,16,26,.5);
  opacity: 0; pointer-events: none; transition: opacity .25s;
}
.nay26-sumback.open { opacity: 1; pointer-events: auto; }

.nay26-sumpanel {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1060; background: var(--n-surface);
  border-radius: 20px 20px 0 0; box-shadow: 0 -8px 30px rgba(20,32,48,.25);
  transform: translateY(101%); transition: transform .3s cubic-bezier(.32,.72,0,1);
  max-height: 84vh; display: flex; flex-direction: column;
}
.nay26-sumpanel.open { transform: translateY(0); }
/* On a laptop it docks in the corner rather than rising from the floor. */
@media (min-width: 721px) {
  .nay26-sumpanel {
    left: auto; right: 22px; bottom: 22px; width: 380px;
    border-radius: var(--sp-radius); transform: translateY(calc(100% + 40px));
  }
  .nay26-sumpanel.open { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .nay26-sumpanel, .nay26-sumback { transition: none; }
}

.nay26-phead {
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 18px 13px; border-bottom: 1px solid var(--n-line);
}
.nay26-phead h3 { margin: 0; font-size: 1.05rem; font-weight: 720; color: var(--n-ink); }
.nay26-pclose {
  background: none; border: 0; cursor: pointer; color: var(--n-muted);
  font-size: 22px; line-height: 1; padding: 2px 6px; border-radius: 8px;
}
.nay26-pbody { padding: 10px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.nay26-pfoot {
  border-top: 1px solid var(--n-line);
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 9px;
}
.nay26-pempty { text-align: center; color: var(--n-muted); padding: 26px 18px; font-size: .9rem; }
.nay26-note { margin: 0 2px 2px; font-size: .86rem; color: var(--n-ink-2); line-height: 1.4; }

.nay26-continue, .nay26-avail, .nay26-skip {
  display: flex; align-items: center; justify-content: center; width: 100%;
  cursor: pointer; font: inherit; border-radius: var(--sp-radius-sm); text-decoration: none;
}
.nay26-continue {
  font-size: .96rem; font-weight: 690; color: var(--n-accent-ink);
  background: var(--n-accent); border: 0; padding: 13px;
}
.nay26-continue:hover { background: var(--n-accent-2); color: var(--n-accent-ink); }
.nay26-avail {
  font-size: .92rem; font-weight: 660; color: var(--n-accent);
  background: var(--n-accent-sf); border: 1.5px solid var(--n-accent); padding: 12px;
}
.nay26-avail:hover { color: var(--n-accent); }
.nay26-skip {
  font-size: .86rem; font-weight: 600; color: var(--n-muted);
  background: none; border: 0; padding: 8px; text-decoration: underline;
}
.nay26-skip:hover { color: var(--n-ink-2); }

.nay26-sumrow {
  display: flex; gap: 10px; align-items: flex-start; padding: 11px;
  border-radius: var(--sp-radius-sm); background: var(--n-surface-2); border: 1px solid var(--n-line);
}
/* THE SUBJECT COLOUR, taken from --sc and not from the card's border.
   It used to be read as getComputedStyle(card).borderLeftColor, and
   `.title.nay26-sel` sets `border-color` -- a shorthand that covers the left
   edge too. Only SELECTED cards appear in this sheet, so every dot resolved to
   the selection blue and the sheet had no colour coding at all. Reading --sc
   through a var() lets the browser resolve it and is immune to whatever the
   selected state does to the border. */
.nay26-sr-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 5px; flex: none; background: var(--sc, var(--n-accent)); }
.nay26-sr-main { flex: 1; min-width: 0; }
.nay26-sr-subj { font-weight: 680; font-size: .92rem; color: var(--n-ink); }
.nay26-sr-meta { font-size: .8rem; color: var(--n-muted); }
.nay26-sr-wl {
  font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--n-warn); background: var(--n-warn-sf); padding: 1px 6px; border-radius: 5px; margin-left: 6px;
}
.nay26-sr-rm {
  flex: none; background: none; border: 0; cursor: pointer; color: var(--n-muted);
  width: 28px; height: 28px; border-radius: 7px; font-size: 16px;
}
.nay26-sr-rm:hover { background: var(--n-warn-sf); color: var(--n-warn); }

/* ---- one day at a time on a phone --------------------------------------- */

@media (max-width: 720px) {
  .nay26-scrollhint { display: none; }
  /* One day fills the screen here, so the caption is not fighting for width and
     can be read at the size the rest of the card is. */
  .title.nay26-sel .nay26-datetitle { font-size: .72rem; }
  /* All six share the row, so Monday to Saturday is on screen at 320px with no
     sideways scrolling. flex:1 with a zero basis is what makes them equal
     regardless of whether the label is "Mon" or "Wed". */
  .nay26-daytabs { display: flex; gap: 5px; padding: 2px 0 12px; }
  .nay26-daytab {
    flex: 1 1 0; min-width: 0; cursor: pointer; font: inherit; font-weight: 650;
    font-size: .82rem; text-align: center;
    border: 1.5px solid var(--n-line); background: var(--n-surface); color: var(--n-ink-2);
    padding: 10px 2px; min-height: 44px; border-radius: 999px;
  }
  .nay26-daytab.is-active { background: var(--n-accent); border-color: var(--n-accent); color: var(--n-accent-ink); }

  /* The day header row goes -- the active tab already names the day. */
  .nay26-grid thead { display: none; }
  .nay26-gridwrap[data-day="Mon"] td[data-day]:not([data-day="Mon"]),
  .nay26-gridwrap[data-day="Tue"] td[data-day]:not([data-day="Tue"]),
  .nay26-gridwrap[data-day="Wed"] td[data-day]:not([data-day="Wed"]),
  .nay26-gridwrap[data-day="Thu"] td[data-day]:not([data-day="Thu"]),
  .nay26-gridwrap[data-day="Fri"] td[data-day]:not([data-day="Fri"]),
  .nay26-gridwrap[data-day="Sat"] td[data-day]:not([data-day="Sat"]) { display: none; }
}

/* ---- the whole week at once --------------------------------------------- */

@media (min-width: 721px) {
  .nay26-centres { flex-direction: row; flex-wrap: wrap; }
  .nay26-centre  { flex: 1 1 190px; }
}

/* ---- instructions -------------------------------------------------------
   Same furniture as the selection sheet, so the page has one overlay idiom
   rather than two. On a laptop it centres rather than docking in the corner:
   this one is read once and dismissed, not consulted while working. */

.nay26-insback {
  position: fixed; inset: 0; z-index: 1065; background: rgba(10,16,26,.5);
  opacity: 0; pointer-events: none; transition: opacity .25s;
}
.nay26-insback.open { opacity: 1; pointer-events: auto; }

.nay26-inspanel {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1070; background: var(--n-surface);
  border-radius: 20px 20px 0 0; box-shadow: 0 -8px 30px rgba(20,32,48,.25);
  transform: translateY(101%); transition: transform .3s cubic-bezier(.32,.72,0,1);
  max-height: 88vh; display: flex; flex-direction: column;
}
.nay26-inspanel.open { transform: translateY(0); }
@media (min-width: 721px) {
  .nay26-inspanel {
    left: 50%; right: auto; bottom: auto; top: 50%; width: min(520px, calc(100vw - 40px));
    border-radius: var(--sp-radius); transform: translate(-50%, -46%) scale(.98);
    opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s;
  }
  .nay26-inspanel.open { transform: translate(-50%, -50%) scale(1); opacity: 1; pointer-events: auto; }
}
@media (prefers-reduced-motion: reduce) { .nay26-inspanel { transition: none; } }

.nay26-inslist { margin: 0; padding-left: 22px; display: flex; flex-direction: column; gap: 9px; }
.nay26-inslist li { font-size: .92rem; line-height: 1.5; color: var(--n-ink-2); }
.nay26-inslist b { color: var(--n-ink); }

/* Group heading inside the selection sheet. Only drawn when there is more than
   one group, so a family with only places never sees a label saying so. */
.nay26-srgroup {
  font-size: .7rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--n-muted); padding: 6px 2px 0;
}
.nay26-srgroup:not(:first-child) { margin-top: 6px; border-top: 1px solid var(--n-line); padding-top: 12px; }

/* The dismissed instructions fly into the link that reopens them; the link
   pulses so the eye lands where the panel went. */
.nay26-inspanel.is-flying {
  transition: transform .38s cubic-bezier(.4, 0, .2, 1), opacity .38s ease;
  pointer-events: none;
}
#nay26InsOpen.is-pulsing {
  animation: nay26-pulse 1.1s ease-out 1;
  border-radius: 5px;
}
@keyframes nay26-pulse {
  0%   { box-shadow: 0 0 0 0 var(--n-accent-sf); background: var(--n-accent-sf); }
  35%  { box-shadow: 0 0 0 6px var(--n-accent-sf); background: var(--n-accent-sf); }
  100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .nay26-inspanel.is-flying { transition: none; }
  #nay26InsOpen.is-pulsing { animation: none; }
}

/* Admin-only start-date diagnostic (?spdiag=1). Deliberately plain -- it is a
   readout, not part of the product. */
.nay26-diag {
  margin-top: 16px; padding: 12px 14px; border: 1px dashed var(--n-line-2);
  border-radius: var(--sp-radius-sm); background: var(--n-surface-2); font-size: 12.5px;
}
.nay26-diag p { margin: 6px 0 10px; color: var(--n-ink-2); line-height: 1.5; }
.nay26-diag table { border-collapse: collapse; width: 100%; }
.nay26-diag th, .nay26-diag td {
  border: 1px solid var(--n-line); padding: 4px 7px; text-align: left; font-variant-numeric: tabular-nums;
}
.nay26-diag th { background: var(--n-surface); font-weight: 700; }

/* ---------- My Bookings --------------------------------------------------
   The payment summary. Three lines and a total, because the figure the
   campaign engine produces is the sum of unrelated things -- the first month's
   sessions, an outstanding deposit, and the current invoice month's balance
   carried in -- and one number cannot be reconciled against the invoice page.
   -------------------------------------------------------------------------- */

.sp-mc-sum { width: 100%; margin-top: 4px; }
.sp-mc-sum th, .sp-mc-sum td { padding: 9px 0; border-bottom: 1px solid var(--sp-line); text-align: left; }
.sp-mc-sum th { font-weight: 500; color: var(--sp-ink-soft); font-size: 14px; }
.sp-mc-sum td { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.sp-mc-total th, .sp-mc-total td { border-bottom: 0; border-top: 2px solid var(--sp-line-strong); font-size: 15px; }
.sp-mc-total th { font-weight: 700; color: var(--sp-ink); }

/* On My Bookings the grid is a read-out, not a picker: nothing in a cell is
   clickable, so the card loses its pointer and hover lift. */
.sp-mc-sum + p { margin-top: 12px; }


/* ---------- Enrolment: review & accept -----------------------------------
   The terms screen at /students/enrolment_accept.php. Its element ids are
   load-bearing -- the submit handoff is lifted from the page this replaces and
   finds its fields by id -- so these rules style what is there rather than
   asking the markup to change shape.
   -------------------------------------------------------------------------- */

.sp-acc-list th, .sp-acc-list td { padding: 10px 0; vertical-align: top; }
/* .sp-table th is a COLUMN-header style -- 11px, muted, letter-spaced, UPPERCASE
   -- and it is right for a thead. These th's are ROW headers holding a class
   name, so it rendered "PROBLEM SOLVING" and "WEDNESDAY · 17:00-18:00". Reset
   here rather than in .sp-table, which every other portal table depends on. */
.sp-acc-list th {
  font-weight: 400; font-size: 14px; text-transform: none; letter-spacing: 0;
  white-space: normal; color: var(--sp-ink-soft);
}
.sp-acc-list th b { display: block; font-weight: 700; color: var(--sp-ink); }
.sp-acc-list th span { display: block; font-size: 12.5px; color: var(--sp-muted); }
/* The Waiting list pill is a span inside that th, so the display:block above
   caught it and stretched it into a full-width pink bar across the row. */
.sp-acc-list th b .sp-pill {
  display: inline-block; margin-left: 7px; vertical-align: 1px; font-weight: 700;
}
.sp-acc-list td { text-align: right; font-size: 12.5px; color: var(--sp-muted); white-space: nowrap; }
.sp-acc-wait th b { color: var(--sp-ink-soft); }

/* A key term is a tickable block, not a paragraph with a box beside it: each
   one is agreed separately and the gate checks all four. */
.sp-acc-term {
  display: flex; gap: 11px; align-items: flex-start; cursor: pointer;
  padding: 12px 0; border-top: 1px solid var(--sp-line); font-size: 14px; line-height: 1.55;
}
.sp-acc-term input { margin: 3px 0 0; flex: none; }
.sp-acc-term b { color: var(--sp-ink); }

.sp-acc-goals { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
.sp-acc-goals label { display: flex; align-items: center; gap: 9px; cursor: pointer; font-size: 14px; }
.sp-acc-goals input[type="text"] {
  flex: 1 1 auto; min-width: 0; margin-left: 6px; padding: 7px 10px;
  border: 1px solid var(--sp-line-strong); border-radius: var(--sp-radius-sm); font: inherit; font-size: 13.5px;
}
.sp-acc-goals input[type="text"]:disabled { background: var(--sp-ground); color: var(--sp-muted); }

.sp-acc-field { display: block; margin-top: 14px; }
.sp-acc-field > span { display: block; font-size: 13.5px; font-weight: 600; margin-bottom: 5px; }
.sp-acc-field em { font-style: normal; font-weight: 400; color: var(--sp-muted); }
.sp-acc-field input[type="text"], .sp-acc-field textarea {
  width: 100%; padding: 9px 11px; font: inherit; font-size: 14px;
  border: 1px solid var(--sp-line-strong); border-radius: var(--sp-radius-sm);
  background: var(--sp-surface); color: var(--sp-ink);
}
.sp-acc-field textarea { resize: vertical; }
.sp-acc-two { display: flex; flex-wrap: wrap; gap: 14px; }
.sp-acc-two .sp-acc-field { flex: 1 1 220px; }

.sp-acc-check {
  display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
  margin-top: 14px; font-size: 14px; line-height: 1.5;
}
.sp-acc-check input { margin: 3px 0 0; flex: none; }
.sp-acc-terms { font-weight: 600; }

.sp-acc-note, .sp-acc-msg {
  margin-top: 10px; padding: 11px 13px; border-radius: var(--sp-radius-sm);
  font-size: 13px; line-height: 1.55;
}
.sp-acc-note { background: var(--sp-brand-soft); color: var(--sp-ink-soft); }
.sp-acc-bad  { background: var(--sp-coral-soft); color: var(--sp-coral); }
.sp-acc-warn { background: var(--sp-amber-soft); color: var(--sp-amber); }
.sp-acc-good { background: var(--sp-green-soft); color: var(--sp-green); }

/* =============================================================================
   ENROLMENT — the two-step booking page
   -----------------------------------------------------------------------------
   One URL, two steps: the timetable, then review & accept. Everything below is
   shared by both steps or belongs to the second one; the picker's own reskin
   layer (.nay26-*) is above.
   ========================================================================== */

/* ---- the step marker ------------------------------------------------------
   Both steps named rather than counted, so a family can see what is still to
   come before committing to the first. */
.sp-en-steps {
  display: flex; align-items: center; gap: 8px;
  list-style: none; margin: 0 0 14px; padding: 0;
  font-size: 13px;
}
.sp-en-step {
  display: flex; align-items: center; gap: 7px;
  color: var(--sp-muted); min-width: 0;
}
/* The rule between two steps. A pseudo-element on the SECOND step, not a
   border on the first, so it never appears after the last one. */
.sp-en-step + .sp-en-step::before {
  content: ""; display: block; width: 18px; height: 1px;
  background: var(--sp-line-strong); flex: none;
}
.sp-en-stepn {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--sp-line-strong);
  background: var(--sp-surface);
  font-size: 12px; font-weight: 700; line-height: 1;
}
.sp-en-steplab { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sp-en-step.is-on { color: var(--sp-ink); font-weight: 650; }
.sp-en-step.is-on .sp-en-stepn {
  background: var(--sp-brand); border-color: var(--sp-brand); color: #fff;
}
.sp-en-step.is-done { color: var(--sp-ink-soft); }
.sp-en-step.is-done .sp-en-stepn {
  background: var(--sp-green-soft); border-color: var(--sp-green); color: var(--sp-green);
}
/* Below 420px the two labels and the rule will not fit beside each other, and
   an ellipsis on "Review & accept" tells a family nothing. The rule goes and
   the labels keep their words. */
@media (max-width: 420px) {
  .sp-en-steps { font-size: 12.5px; gap: 6px; }
  .sp-en-step + .sp-en-step::before { width: 10px; }
}

/* ---- "How it works", inside the instructions overlay --------------------- */
.nay26-howlist {
  margin: 0 0 6px; padding: 0; list-style: none;
  counter-reset: sphow;
  display: flex; flex-direction: column; gap: 12px;
}
.nay26-howlist li {
  counter-increment: sphow;
  position: relative; padding-left: 34px;
  font-size: .92rem; line-height: 1.5; color: var(--n-ink-2);
}
.nay26-howlist li::before {
  content: counter(sphow);
  position: absolute; left: 0; top: 1px;
  display: flex; align-items: center; justify-content: center;
  width: 23px; height: 23px; border-radius: 50%;
  background: var(--n-accent-sf); color: var(--n-accent);
  font-size: 12px; font-weight: 700; line-height: 1;
}
.nay26-howlist b { display: block; color: var(--n-ink); }
.nay26-howlist span { display: block; margin-top: 1px; }
.nay26-inssub {
  margin: 18px 0 9px; padding-top: 14px;
  border-top: 1px solid var(--n-line);
  font-size: 13px; font-weight: 700; color: var(--n-ink);
}

/* ---- step 2: the week view ------------------------------------------------
   The same Monday-to-Saturday shape the picker and My Bookings use, because the
   family chose these classes out of a grid and that is the layout they already
   have in their head. Every day is drawn, empty ones included: a gap in the
   week is information, not a blank. */
.sp-rv-week {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px;
  margin-top: 4px;
}
.sp-rv-col {
  border: 1px solid var(--sp-line); border-radius: var(--sp-radius-sm);
  background: var(--sp-surface); overflow: hidden; min-width: 0;
}
.sp-rv-col.sp-rv-off { background: var(--sp-ground); border-style: dashed; }
.sp-rv-head {
  padding: 7px 9px; border-bottom: 1px solid var(--sp-line);
  background: var(--sp-brand-soft);
  font-size: 12px; line-height: 1.3;
}
.sp-rv-off .sp-rv-head { background: transparent; color: var(--sp-muted); }
.sp-rv-day { display: block; font-weight: 700; color: var(--sp-ink); }
.sp-rv-off .sp-rv-day { color: var(--sp-muted); font-weight: 600; }
.sp-rv-head small { display: block; color: var(--sp-ink-soft); font-size: 11px; }
.sp-rv-body { padding: 8px; display: flex; flex-direction: column; gap: 8px; }
.sp-rv-empty { color: var(--sp-line-strong); text-align: center; font-size: 14px; padding: 6px 0; }
.sp-rv-block {
  border-left: 3px solid var(--sp-brand-mid); padding-left: 8px;
  font-size: 12.5px; line-height: 1.4; min-width: 0;
}
.sp-rv-block b { display: block; color: var(--sp-ink); font-size: 13px; }
.sp-rv-meta { display: block; color: var(--sp-muted); overflow-wrap: anywhere; }
/* Printed on the class only when the classes sharing a day start on DIFFERENT
   dates -- when they all start together the date sits on the column head
   instead. Given weight, because it is the line that stops a family reading
   the day's first start date as everyone's. */
.sp-rv-start {
  display: inline-block; margin-top: 3px; padding: 1px 6px; border-radius: 999px;
  background: var(--sp-brand-soft); color: var(--sp-brand);
  font-size: 11.5px; font-weight: 650; white-space: nowrap;
}

/* Six columns stop being readable long before the phone breakpoint — the class
   names wrap to one word a line. Below 720px the week becomes six rows, which
   is the same information read top to bottom, and the empty days collapse to a
   single line rather than a tall blank card. */
@media (max-width: 720px) {
  .sp-rv-week { grid-template-columns: 1fr; gap: 6px; }
  .sp-rv-col { display: flex; align-items: stretch; }
  .sp-rv-head {
    flex: 0 0 104px; border-bottom: 0; border-right: 1px solid var(--sp-line);
    display: flex; flex-direction: column; justify-content: center;
  }
  .sp-rv-body { flex: 1; min-width: 0; }
  .sp-rv-off .sp-rv-body { padding: 6px 8px; }
  .sp-rv-empty { text-align: left; padding: 0; }
}

.sp-rv-wait { margin-top: 18px; }
.sp-rv-wait h3 { font-size: 14px; margin: 0 0 6px; }

/* ---- step 2: the bottom bar ----------------------------------------------
   The picker's bar, carried onto the review step so the flow does not lose its
   only thumb-reach control halfway through. The info block is static here —
   there is nothing left to recount — so it is a div, not a button. */
.nay26-sb-back {
  flex: none; cursor: pointer; font: inherit; font-size: 1.05rem; line-height: 1;
  color: var(--n-ink-2); background: var(--n-surface);
  border: 1.5px solid var(--n-line-2); border-radius: var(--sp-radius-sm);
  padding: 0 14px; min-height: 44px;
}
.nay26-sb-back:hover { border-color: var(--n-ink-2); }
.nay26-sb-static { cursor: default; }

/* THE BAR AND THE PHONE TAB BAR ARE BOTH FIXED TO THE BOTTOM.
   The summary bar (z-index 1030) sat directly on top of the portal's own tab
   bar (z-index 800), so on a phone the picker silently lost the whole
   navigation — and the review step, which had no summary bar, got it back. The
   two steps disagreed about whether the portal had a nav. They stack now: the
   tab bar keeps the floor and the summary bar sits on it.
   52px is .sp-tab's min-height; the safe-area inset is already paid once by the
   tab bar, so this offset must not add it a second time. */
@media (max-width: 560px) {
  .nay26-sumbar {
    bottom: calc(52px + env(safe-area-inset-bottom, 0px));
    padding-bottom: 9px;
  }
  /* Room to scroll past BOTH bars, so neither ever sits on the last card.
     Scoped to the enrolment pages: they are the only ones with a summary bar,
     and on every other page this would be a band of empty screen. */
  .sp-en-page { padding-bottom: 152px; }
}

/* ---- step 2: the required sections ----------------------------------------
   Three of the cards on the review step must be completed before a booking can
   be sent. They say so up front, and if Accept is pressed early every one that
   is outstanding is marked at once -- top to bottom -- rather than one at a
   time. */
/* HIDDEN UNTIL THE SECTION IS ACTUALLY MARKED. Three "Required" pills on a form
   a family has only just opened is a page telling them off for not yet having
   done something it has not asked them to do. The pill appears with the red
   edge, at the moment it means something, and goes again when the section is
   put right -- one signal, not two. The requirement is still stated in each
   section's own copy ("Please read and tick each one", "Choose at least one"),
   which is what carries it before the first attempt and to a screen reader. */
.sp-acc-req {
  display: none; vertical-align: middle; margin-left: 8px;
  padding: 2px 8px; border-radius: 999px;
  background: var(--sp-coral-soft); color: var(--sp-coral);
  font-size: 11px; font-weight: 700; letter-spacing: .01em;
  text-transform: uppercase; line-height: 1.6;
}
.sp-card.rat-bad .sp-acc-req { display: inline-block; }
/* The Terms card carries its pill on a line of its own (no visible heading to
   hang it off). Zero height until the pill inside it is shown. */
.sp-acc-reqline { line-height: 0; }
.sp-card.rat-bad .sp-acc-reqline { line-height: inherit; margin-bottom: 6px; }
.sp-acc-reqline .sp-acc-req { margin-left: 0; }
/* The marked state. A tinted left edge and a ring, not a full red fill: the
   card still has to be readable while it is being corrected. */
.sp-card.rat-bad {
  border-color: var(--sp-coral-mid);
  box-shadow: 0 0 0 2px var(--sp-coral-soft), var(--sp-shadow);
}
.sp-card.rat-bad > h2 { color: var(--sp-coral); }
@media (prefers-reduced-motion: no-preference) {
  .sp-card.rat-bad { transition: box-shadow .18s, border-color .18s; }
}

/* "Accept and Go to Pay" is a long label for a bar that also holds a Back
   button and a two-line total. Below 380px the label is what gives -- it wraps
   rather than squeezing the total out of the bar. */
@media (max-width: 380px) {
  .nay26-sb-continue { padding: 10px 12px; font-size: .88rem; }
  .nay26-sb-back { padding: 0 10px; }
}

/* ---- step 2: the progress dialog ------------------------------------------
   Shown while the booking is being written. Above the summary bar (1030) and
   the phone tab bar (800), because nothing behind it can be acted on until the
   write settles. */
.sp-proc-back {
  position: fixed; inset: 0; z-index: 1100;
  background: rgba(16, 34, 55, .45);
}
.sp-proc {
  position: fixed; z-index: 1101;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(360px, calc(100vw - 32px));
  background: var(--sp-surface); border-radius: var(--sp-radius);
  box-shadow: 0 18px 48px rgba(16, 34, 55, .28);
  padding: 26px 22px 22px; text-align: center;
}
.sp-proc[hidden], .sp-proc-back[hidden] { display: none; }

.sp-proc-icon {
  width: 46px; height: 46px; margin: 0 auto 14px;
  border-radius: 50%; display: flex; align-items: center; justify-content: center;
  font-size: 24px; font-weight: 700; line-height: 1;
  background: var(--sp-brand-soft); color: var(--sp-brand);
}
.sp-proc[data-state="done"]   .sp-proc-icon { background: var(--sp-green-soft); color: var(--sp-green); }
.sp-proc[data-state="failed"] .sp-proc-icon { background: var(--sp-coral-soft); color: var(--sp-coral); }

.sp-proc-spin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2.5px solid var(--sp-brand-mid); border-top-color: transparent;
  animation: sp-proc-turn .8s linear infinite;
}
@keyframes sp-proc-turn { to { transform: rotate(360deg); } }
/* A spinner is decoration; the words carry the meaning. Someone who has asked
   for less motion still gets the dialog, just without the turning. */
@media (prefers-reduced-motion: reduce) {
  .sp-proc-spin { animation: none; border-top-color: var(--sp-brand-soft); }
}

.sp-proc-title { margin: 0; font-size: 16px; font-weight: 700; color: var(--sp-ink); }
.sp-proc-sub   { margin: 6px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--sp-muted); }
.sp-proc-sub a { color: var(--sp-brand); }
.sp-proc-acts  { margin-top: 18px; }
.sp-proc-acts[hidden] { display: none; }
.sp-proc-acts .sp-btn { width: 100%; }

/* ---- the marketing opt-in, on the review step -----------------------------
   Its own block inside "A few details" rather than one more line in the list:
   the copy names the company, limits the scope and gives two ways out, and at
   that length it stops reading as a checkbox and starts reading as a notice. */
.sp-acc-optin {
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--sp-line);
}
.sp-acc-optin h3 {
  margin: 0; font-size: 14px; font-weight: 700; color: var(--sp-ink);
}
.sp-acc-optin h3 em { font-style: normal; font-weight: 400; color: var(--sp-muted); }
.sp-acc-optin .sp-acc-check { margin-top: 8px; font-size: 13.5px; }
.sp-acc-optin a { color: var(--sp-brand); }

/* ---- staying in touch, on My Bookings -------------------------------------
   The family's own control over course emails. Equal prominence either way:
   whichever direction they are going, it is one button in the same place. */
.sp-mkt-state { margin: 0; font-size: 14px; line-height: 1.6; color: var(--sp-ink-soft); }
.sp-mkt-state b { color: var(--sp-ink); }
.sp-mkt-acts {
  margin-top: 14px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.sp-mkt-busy { font-size: 13px; color: var(--sp-muted); }
.sp-mkt-busy[hidden] { display: none; }
.sp-mkt .sp-none { margin-top: 12px; }

/* =============================================================================
   ENROLMENT PICKER — the one-screen pass (2026-08-28)
   -----------------------------------------------------------------------------
   The picker was 3,485px tall on a 1440x820 laptop: 4.25 screens for a Year 5
   pupil with every subject ticked. Most of it was in the cards -- a collapsed
   card was 112px for two lines of text, of which 26px was an empty line box
   between subject and tutor and 32px was an "Available" badge that appeared on
   nearly every card. Both are gone above; the rules below carry the rest.
   ========================================================================== */

/* ---- remove, from the card itself ---------------------------------------- */
.nay26-rm {
  position: absolute; top: -7px; right: -7px; z-index: 3;
  width: 21px; height: 21px; border-radius: 50%; border: 0; padding: 0;
  background: var(--sp-coral); color: #fff;
  font: inherit; font-size: 15px; font-weight: 700; line-height: 21px; text-align: center;
  cursor: pointer; box-shadow: 0 1px 4px rgba(20,45,75,.3);
  /* Only on a card that is actually selected -- an unselected grid carries no
     controls it cannot act on. */
  display: none;
}
.title { position: relative; }
.title.nay26-sel .nay26-rm { display: block; }
.nay26-rm:hover { background: #a83b33; }
.nay26-rm:focus-visible { outline: 2px solid var(--n-ink); outline-offset: 2px; }

/* THE hidden ATTRIBUTE NEEDS HELP HERE. `[hidden]{display:none}` is a UA rule
   with the weight of a bare attribute selector, and .sp-card sets display, so
   the class wins and the "hidden" card stays on screen at full height. Setting
   it hidden then measuring 232px is exactly what that looks like. */
.sp-card[hidden] { display: none; }

/* The full card's <label> wraps only a visually-hidden checkbox, so it shows
   nothing and still claimed a 19px line box on every waiting-list card. The
   checkbox is driven by the card's click handler, not by the label, and a
   display:none checkbox still submits its value when checked. */
.fully_booked_classes label { display: none; }

/* ---- the collapsed filter bar -------------------------------------------- */
.nay26-fbar {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  background: var(--n-surface); border: 1px solid var(--n-line);
  border-radius: var(--sp-radius-sm); padding: 7px 10px; margin-bottom: 10px;
}
.nay26-fbar {
  /* Animated in and out rather than shown and hidden -- see .is-collapsed. */
  max-height: 0; opacity: 0; overflow: hidden;
  padding-top: 0; padding-bottom: 0; margin-bottom: 0; border-width: 0;
  transform: translateY(-6px);
  transition: max-height .34s cubic-bezier(.4,0,.2,1), opacity .2s ease .1s,
              transform .28s cubic-bezier(.4,0,.2,1) .06s,
              padding .34s cubic-bezier(.4,0,.2,1), margin .34s cubic-bezier(.4,0,.2,1),
              border-width .2s;
}
.nay26-fbar.is-in {
  max-height: 220px; opacity: 1; transform: none;
  padding-top: 7px; padding-bottom: 7px; margin-bottom: 10px; border-width: 1px;
}

/* THE COLLAPSE. The controls fold upward into the bar rather than blinking out
   of existence: height and opacity run together over a third of a second, and
   the bar rises into the space a beat later, so the eye follows one thing
   becoming the other. display:none cannot be transitioned, which is why neither
   element uses the hidden attribute any more. */
#nay26Filters {
  overflow: hidden;
  max-height: 900px; opacity: 1; transform: none;
  transition: max-height .38s cubic-bezier(.4,0,.2,1), opacity .22s ease,
              transform .3s cubic-bezier(.4,0,.2,1),
              margin .38s cubic-bezier(.4,0,.2,1), padding .38s cubic-bezier(.4,0,.2,1);
}
#nay26Filters.is-collapsed {
  max-height: 0; opacity: 0; transform: translateY(-10px) scale(.985);
  transform-origin: top center;
  margin-top: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0;
  /* Without this the card's own border survives the collapse as a 2px line. */
  border-width: 0;
  pointer-events: none;
}
/* Someone who has asked for less motion still gets the change, just not the
   travel -- the state is what matters, the movement is the flourish. */
@media (prefers-reduced-motion: reduce) {
  #nay26Filters, .nay26-fbar { transition: none; transform: none; }
  #nay26Filters.is-collapsed { transform: none; }
}
/* Once the filters fold, the page-level intro has done its job too -- it
   explains how to choose a centre, which by then they have. */
.sp-en-page.is-folded .sp-hello { display: none; }
.nay26-fbar-lab {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--n-muted);
}
.nay26-fbar-sep { width: 1px; align-self: stretch; background: var(--n-line); margin: 0 3px; }
.nay26-fchip {
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  padding: 4px 11px; border-radius: 20px; min-height: 30px;
  border: 1px solid var(--n-line-2); background: var(--n-surface); color: var(--n-ink-2);
}
.nay26-fchip.is-on { background: var(--n-accent); border-color: var(--n-accent); color: var(--n-accent-ink); }
.nay26-fchip.is-count { cursor: default; background: var(--n-surface-2); }
/* Same rule as the boxes above, same reason for the attribute selector:
   paintFbar() rewrites className on every repaint. */
.nay26-fchip[disabled] {
  cursor: not-allowed; opacity: .45;
  background: var(--n-surface-2); text-decoration: line-through;
}
.nay26-fbar-change {
  margin-left: auto; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  background: none; border: 0; padding: 4px 2px; color: var(--n-accent);
  text-decoration: underline; text-underline-offset: 2px;
}

/* ---- the availability invitation, under the grid ------------------------- */
.nay26-availline {
  margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: var(--n-muted);
}
.nay26-availlink {
  font: inherit; font-size: 13px; font-weight: 650; cursor: pointer;
  background: none; border: 0; padding: 0; color: var(--n-accent);
  text-decoration: underline; text-underline-offset: 2px;
}

/* ---- the availability popover -------------------------------------------- */
.nay26-avback {
  position: fixed; inset: 0; z-index: 1100; background: rgba(16,34,55,.45);
}
.nay26-avpanel {
  position: fixed; z-index: 1101; left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: min(760px, calc(100vw - 28px)); max-height: calc(100vh - 48px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--n-surface); border-radius: var(--sp-radius);
  box-shadow: 0 18px 48px rgba(16,34,55,.28);
}
.nay26-avback[hidden], .nay26-avpanel[hidden] { display: none; }
.nay26-avpanel iframe { flex: 1; width: 100%; min-height: 60vh; border: 0; display: block; }
/* On a phone it is a full-height sheet -- the form inside is long and a centred
   box would spend most of the screen on its own margins. */
@media (max-width: 560px) {
  .nay26-avpanel {
    left: 0; top: auto; bottom: 0; transform: none;
    width: 100%; max-height: 92vh; border-radius: var(--sp-radius) var(--sp-radius) 0 0;
  }
}

/* ---- the sheet's own availability button --------------------------------- */
button.nay26-avail {
  font: inherit; cursor: pointer;
}

/* =============================================================================
   THE PHONE PASS — undoing the density where it was never needed
   -----------------------------------------------------------------------------
   The one-screen work above shrank type and tightened boxes to fit SIX day
   columns beside each other. A phone does not show six columns: below 720px the
   grid already switches to one day at a time and every card is full width, so
   none of that pressure exists and all the shrinking buys is smaller text.

   So the compact sizes are a LAPTOP treatment, and the phone gets its size
   back. 11.5px tutor names and a 10.6px badge are the wrong answer on the
   device this portal is most often opened on.
   ========================================================================== */

@media (max-width: 720px) {
  /* Readable again: 15px subject, 13px tutor, 12px badge. */
  .title strong { font-size: .95rem; line-height: 1.3; }
  .title span.nay26-tutor { font-size: .82rem; }
  .fully_booked_classes span.nay26-fullwl { font-size: .75rem; padding: 2px 8px; }
  /* The badge goes back to its own line here -- the row-height argument for
     inlining it only applies to a grid with six columns sharing a row. */
  .fully_booked_classes span.nay26-tutor { display: block; }
  .fully_booked_classes span.nay26-tutor::after { content: none; }

  /* The card IS the tap target, so it carries the 44px. */
  .title:not(.fully_booked_classes) { min-height: 52px; }
  .title { padding: 9px 10px; margin: 5px 0; }
  .nay26-grid td { padding: 6px; }

  /* Remove: 21px is a mouse target, not a thumb one. The circle stays 32px so
     it does not swamp the corner of the card, and an invisible inset extends
     the thumb area past 44px without changing what is drawn. */
  .nay26-rm { width: 32px; height: 32px; font-size: 20px; line-height: 32px; top: -10px; right: -8px; }
  .nay26-rm::before { content: ""; position: absolute; top: -7px; right: -7px; bottom: -7px; left: -7px; }

  /* Start dates: full-size pills that can be hit without aiming. */
  .title .span {
    font-size: .86rem; padding: 11px 14px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .title.nay26-sel .nay26-dates { gap: 7px; padding: 8px; }

  /* The filter bar's chips are controls -- Apple and Google both ask 44px. */
  .nay26-fchip { font-size: .85rem; min-height: 44px; padding: 6px 14px; }
  .nay26-fbar { gap: 8px; padding: 9px 10px; }
  .nay26-fbar-change { min-height: 44px; font-size: .85rem; padding: 4px 6px; }
  /* Wraps to its own row rather than being squeezed against the chips. */
  .nay26-fbar-sep { display: none; }

  /* The bar's Back is a single glyph, so nothing was holding it to a thumb
     width -- it came out 38px against a 44px height. */
  .nay26-sb-back { min-width: 44px; }

  /* The invitation under the grid, and the sheet's waiting-list tag. */
  .nay26-availline { font-size: .88rem; }
  .nay26-availlink { font-size: .88rem; }
  .nay26-sr-wl { font-size: .72rem; }

  /* Every tickable row is a thumb target, not just the ones whose text happens
     to wrap onto three lines. The single-line ones came out at 21px (the sibling
     question) and 42px -- and the 42px one was the Terms of Service box itself,
     the single most important control on the page. */
  .sp-acc-check { min-height: 44px; align-items: center; padding: 4px 0; }
  .sp-acc-check input { margin-top: 0; }
  .sp-acc-term { padding: 14px 0; }

  /* The review step's week view already stacks below 720px; its meta lines are
     the only thing there running small. */
  .sp-rv-block { font-size: .82rem; }
  .sp-rv-block b { font-size: .88rem; }
  .sp-rv-start { font-size: .78rem; }
  /* The start date on a day's heading was the smallest text anywhere on the
     page at 11px. */
  .sp-rv-head small { font-size: 12px; }
}

/* ---- the review week: waiting-list requests sit in it too ----------------- */
.sp-rv-block.sp-rv-waiting {
  border-left-color: var(--sp-coral-mid);
  background: var(--sp-coral-soft);
  border-radius: 0 6px 6px 0; padding: 5px 8px;
}
.sp-rv-waiting b { color: var(--sp-coral); }
.sp-rv-wl {
  display: inline-block; margin: 2px 0 1px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--sp-coral); background: var(--sp-surface);
  padding: 1px 6px; border-radius: 5px;
}

/* ---- room to breathe between sections ------------------------------------
   The cards on My Bookings and the review step sat flush against each other, so
   four unrelated things -- a timetable, a total, a consent, a set of terms --
   read as one long column. */
.sp-en-page .sp-card + .sp-card,
.sp-mc-page .sp-card + .sp-card { margin-top: 18px; }
@media (min-width: 768px) {
  .sp-en-page .sp-card + .sp-card,
  .sp-mc-page .sp-card + .sp-card { margin-top: 22px; }
}
.sp-acc-optin-solo { margin: 0; padding: 0; border-top: 0; }
/* "(optional)" rides the card heading without inheriting its uppercase
   letter-spaced treatment -- it is an aside, not part of the label. */
.sp-h2-opt { font-style: normal; text-transform: none; letter-spacing: 0;
  font-weight: 500; color: var(--sp-muted); }

/* Selections held at a centre that is not the one on screen. Mixing centres is
   allowed, so this states a fact rather than warning about one. */
.nay26-elsewhere {
  margin: 10px 0 0; font-size: 13px; line-height: 1.5;
  color: var(--n-ink-2); background: var(--n-accent-sf);
  border-radius: var(--sp-radius-sm); padding: 8px 11px;
}
.nay26-elsewhere span { font-weight: 700; color: var(--n-ink); }
@media (max-width: 720px) { .nay26-elsewhere { font-size: .88rem; } }

/* ---------- the welcome guide --------------------------------------------
   A spotlight walkthrough: the page dims except the one thing being talked
   about, with a bubble pointing at it. See partials/welcome.php for how the
   targets are found and what happens when one is missing.

   THE DIM IS THE RING'S OWN SHADOW. A transparent box with a 9999px spread
   darkens everything outside itself and nothing inside, which is a hole cut in
   one element - no SVG mask, no clip-path, no four-panel arithmetic that can
   be one pixel out. It also means the hole is animated for free by moving one
   box, which is what makes stepping between targets read as a movement rather
   than a redraw.

   NOTHING IN THE HOLE IS CLICKABLE. pointer-events sit on .sp-wg-hit, which
   covers the whole screen underneath the ring, so a family cannot half-follow
   the guide by clicking the lit link and leaving the tour open on a page that
   no longer has it. The ring itself takes no clicks, or it would swallow the
   bubble's buttons where the two overlap. */
.sp-wg[hidden] { display: none; }
/* ABOVE EVERYTHING THIS PAGE PUTS ON SCREEN. It was at 120, under the phone tab
   bar at 800 and the admin preview banner at 1050 - so on a phone the bar stayed
   at full brightness while the rest of the page dimmed behind it, which read as
   the highlight not working, and the box itself was drawn UNDERNEATH the bar,
   where Back and Next could not be reached without scrolling first.

   1300 puts it over the tab bar, the banner and the 1200 dialogues. Being the
   front-most thing is what a guide has to be: it dims the page, and anything
   that escapes the dim looks like it was deliberately left lit. */
.sp-wg { position: fixed; inset: 0; z-index: 1300; }
.sp-wg-hit { position: absolute; inset: 0; }
.sp-wg-ring {
  position: fixed; pointer-events: none;
  border-radius: var(--sp-radius-sm);
  box-shadow: 0 0 0 9999px rgba(12, 26, 42, .62), 0 0 0 2px var(--sp-brand-mid) inset;
  transition: top .22s ease, left .22s ease, width .22s ease, height .22s ease;
}
/* No target for this step, so nothing is lit: the ring is parked off-screen
   and its shadow becomes a plain full-screen dim. The box does not move for
   this - it is in the same place it is for every other step. */
.sp-wg.is-plain .sp-wg-ring { box-shadow: 0 0 0 9999px rgba(12, 26, 42, .62); }

/* THE BOX STAYS PUT. It used to be positioned against whatever was lit, so the
   words moved on every step while the page under them stood still - which read
   as frantic and meant finding the text again each time. It now has one home,
   at the foot of the screen, and only the light travels.

   The second slot is not a layout preference, it is the one case the first
   cannot cover: on a phone the navigation is a bar fixed to the bottom of the
   screen, so a box at the foot would sit on top of the very tab it is
   describing. place() measures for that and moves it there only then, and the
   move is a slide rather than a reappearance - see the transition below. */
.sp-wg-pop {
  position: fixed; z-index: 1; width: min(360px, calc(100vw - 32px));
  left: 50%; transform: translateX(-50%);
  background: var(--sp-surface); color: var(--sp-ink);
  border-radius: var(--sp-radius); box-shadow: 0 14px 44px rgba(12, 26, 42, .34);
  padding: 16px 16px 12px;
  /* BOTH SLOTS ARE A `top`, SET IN PIXELS BY place(), so the move between them
     is a distance the browser can travel. Anchoring the low slot with `bottom`
     instead would have been simpler to write and impossible to animate: the two
     slots would be different properties, and switching property is a jump.

     It also slides, rather than jumping, when the box merely CHANGES HEIGHT
     between steps - a longer paragraph grows it upward from a fixed foot, which
     is the same movement and reads as the same thing. */
  transition: top .3s cubic-bezier(.4, 0, .2, 1);
}
/* The first placement of a tour has nowhere to slide FROM. */
.sp-wg-pop.is-instant { transition: none; }
.sp-wg-pop h3 { margin: 0 26px 6px 0; font-size: 15px; font-weight: 700; line-height: 1.3; }
.sp-wg-pop p  { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--sp-ink-soft); }
.sp-wg-pop p b { font-weight: 700; color: var(--sp-ink); }

.sp-wg-foot {
  display: flex; align-items: center; gap: 10px;
  margin-top: 14px; padding-top: 11px; border-top: 1px solid var(--sp-line);
}
.sp-wg-dots { display: flex; gap: 5px; flex: 1 1 auto; }
.sp-wg-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--sp-line-strong);
}
.sp-wg-dot.is-on { background: var(--sp-brand); }
.sp-wg-acts { display: flex; gap: 6px; flex: 0 0 auto; }
/* Back is hidden on the first step with the hidden ATTRIBUTE, and .sp-btn sets
   a display of its own - which beats the browser's own [hidden] rule and left a
   dead Back button on step one. Said explicitly here rather than by swapping to
   a class, because hidden is also what tells a screen reader it is not there. */
.sp-wg-acts .sp-btn[hidden] { display: none; }
.sp-wg-x {
  position: absolute; top: 8px; right: 8px;
  border: 0; background: none; cursor: pointer; padding: 2px 5px;
  font-size: 20px; line-height: 1; color: var(--sp-muted);
}
.sp-wg-x:hover { color: var(--sp-ink); }

/* On a phone the bubble is wide and low, and the tab bar it points at is fixed
   to the bottom of the screen, so it is nearly always sitting above something.
   Full width less a margin, rather than a narrow column in the middle. */
@media (max-width: 560px) {
  .sp-wg-pop { width: calc(100vw - 24px); padding: 14px 14px 10px; }
  .sp-wg-pop h3 { font-size: 14.5px; }
  .sp-wg-pop p  { font-size: 13px; }
}

@media (prefers-reduced-motion: reduce) {
  .sp-wg-ring, .sp-wg-pop { transition: none; }
}

/* "Show me around", in the footer of every page. Quiet - it is an offer, not an
   instruction, and a family who does not want it should be able to ignore it
   without it competing with the page. */
.sp-foot-tour { color: var(--sp-brand); text-decoration: underline; text-underline-offset: 2px; }
.sp-foot-tour:hover { color: var(--sp-ink); }

/* ---------- the portal's front door --------------------------------------
   Login, and password reset. Those two pages live outside this module - in
   students/ - but they are the first thing a family sees of the portal and
   have to look like it, so their styling lives here with everything else
   rather than being copied into each of them (`philip`, 2026-08-31).

   They load this whole stylesheet for the design tokens and for sp-card,
   sp-btn and sp-alert; this block is only what those two pages need on top.

   .d-none IS BOOTSTRAP'S, AND IT IS LOAD-BEARING. The login script reveals its
   unverified-email notice with classList.remove('d-none'). Bootstrap used to
   supply that rule and no longer does, and without it that notice would not be
   hidden by anything - so every visitor would be told their email was
   unverified. It is here rather than in the page so the next page to use the
   pattern cannot forget it. */
.d-none { display: none; }

.sp-auth {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 24px 16px; background: var(--sp-ground);
}
.sp-auth-box { width: 100%; max-width: 400px; }
.sp-auth-brand { display: flex; align-items: center; gap: 11px; justify-content: center; margin-bottom: 18px; }
.sp-auth-brand img { width: 44px; height: auto; display: block; flex: 0 0 auto; }
.sp-auth-brand b { display: block; font-size: 17px; font-weight: 700; color: var(--sp-ink); line-height: 1.15; }
.sp-auth-brand span { display: block; font-size: 12.5px; color: var(--sp-muted); }
.sp-auth-card h1 { margin: 0 0 4px; font-size: 19px; font-weight: 700; color: var(--sp-ink); }
.sp-auth-card > p { margin: 0 0 16px; font-size: 13.5px; color: var(--sp-muted); }
.sp-auth-field { margin-bottom: 11px; }
.sp-auth-field label {
  display: block; font-size: 12px; font-weight: 600; color: var(--sp-ink-soft); margin-bottom: 4px;
}
.sp-auth-field input {
  width: 100%; font: inherit; font-size: 15px; color: var(--sp-ink);
  padding: 11px 12px; min-height: 46px;
  border: 1.5px solid var(--sp-line); border-radius: var(--sp-radius-sm);
  background: var(--sp-surface);
}
.sp-auth-field input:focus {
  outline: 2px solid var(--sp-brand); outline-offset: 1px; border-color: var(--sp-brand);
}
.sp-auth-go { width: 100%; justify-content: center; margin-top: 6px; min-height: 46px; font-size: 15px; }
/* Where these pages' scripts write their errors. They set the colour inline, so
   this only has to give it room and keep it off the button. */
#msg_result { display: block; font-size: 13px; line-height: 1.45; margin: 8px 0 0; }
#msg_result:empty { margin: 0; }
.sp-auth-links { margin: 14px 0 0; font-size: 13px; color: var(--sp-muted); text-align: center; line-height: 1.7; }
.sp-auth-links a { color: var(--sp-brand); font-weight: 600; text-decoration: none; }
.sp-auth-links a:hover { text-decoration: underline; }
.sp-auth-foot { margin: 16px 0 0; text-align: center; font-size: 11.5px; color: var(--sp-muted); }
.sp-auth-foot a { color: var(--sp-muted); }

/* ==========================================================================
   student-portal-special-courses s3 - page tabs, special-course money boxes,
   the light course band and the two-tone focus ring.
   (spec §5.4 "Build rules from the Iterate design review", "Preflight design
   corrections", gate 2 "Small text", Preflight decisions "Course card band".)

   ONE BLOCK, APPENDED. No rule above this line is edited by this sub-stage;
   where a live rule needs a new property (.sp-sc-ref nowrap) it is added here.

   NO transform / filter / will-change / perspective / contain on .sp-wrap,
   .sp-pagetabset, .sp-pagetabs or .sp-pagepanel. Any of those makes the
   element a containing block for position:fixed descendants, and the welcome
   guide's ring (.sp-wg-ring, position:fixed) would then be placed against the
   wrong box.

   NOT .sp-tabs / .sp-tab: those are the phone navigation bar.
   ========================================================================== */

/* ---------- page tabs (partials/page_tabs.php) ---------------------------- */

.sp-pagetabset { min-width: 0; }

/* A strip joined to the page by one rule; the selected tab sits on that rule
   with a 3px brand underline. */
.sp-pagetabs {
  display: flex; gap: 4px;
  border-bottom: 1px solid var(--sp-line-strong);
}
.sp-pagetab {
  -webkit-appearance: none; appearance: none;
  font: inherit; background: transparent; border: 0; border-radius: 0;
  border-bottom: 3px solid transparent;
  margin: 0 0 -1px; padding: 10px 20px 10px 4px;
  cursor: pointer; text-align: left; white-space: nowrap;
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  color: var(--sp-ink-soft);
}
.sp-pagetab + .sp-pagetab { padding-left: 16px; }
.sp-pagetab-name { font-size: 16px; font-weight: 650; line-height: 1.25; }
/* The second line: what is behind the tab, never an amount. --sp-ink-soft,
   not --sp-muted (gate 2: 7.30:1 on --sp-ground). */
.sp-pagetab-state {
  font-size: 12.5px; font-weight: 500; line-height: 1.35; color: var(--sp-ink-soft);
  display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.sp-pagetab:hover { color: var(--sp-brand); border-bottom-color: var(--sp-line-strong); }
/* After :hover on purpose, same weight: the selected tab keeps its look. */
.sp-pagetab[aria-selected="true"] { color: var(--sp-ink); border-bottom-color: var(--sp-brand); }
/* Inset ring so the strip's own rule never cuts through it. The .sp-root form
   outranks the portal-wide button:focus-visible rule. */
.sp-pagetab:focus-visible,
.sp-root .sp-pagetab:focus-visible {
  outline: 2px solid var(--sp-brand); outline-offset: -2px;
  border-radius: 6px 6px 0 0;
}

/* "Payment due": ink-soft words with a coral dot on Monthly invoice; amber
   words and an amber dot on Special courses (gate 2, --sp-amber 5.55:1). */
.sp-pagetab-due {
  display: inline-flex; align-items: center; gap: 5px;
  font-weight: 700; color: var(--sp-ink-soft);
}
.sp-pagetab-due::before {
  content: ""; flex: 0 0 auto;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--sp-coral);
}
.sp-pagetab-due-special { color: var(--sp-amber); }
.sp-pagetab-due-special::before { background: var(--sp-amber); }

.sp-pagepanel { padding-top: 18px; }
/* Precedent .sp-modal[hidden]. A panel can also be a flex or grid box, and a
   class's display would otherwise beat the browser's own [hidden] rule. */
.sp-pagepanel[hidden] { display: none; }
.sp-pagepanel:focus-visible { outline: 2px solid var(--sp-brand); outline-offset: 4px; }

/* ---------- special-course money (course cards, My Bookings and Invoice) --- */

.sp-scm-sum { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.sp-scm-sum > div {
  border: 1px solid var(--sp-line); border-radius: var(--sp-radius-sm);
  background: var(--sp-surface); padding: 10px 12px; min-width: 0;
}
/* Box labels in --sp-ink-soft (gate 2: 6.90:1 on amber-soft). */
.sp-scm-sum small {
  display: block; font-size: 11px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--sp-ink-soft);
}
.sp-scm-sum strong {
  display: block; font-size: 19px; font-weight: 700; color: var(--sp-ink);
  font-variant-numeric: tabular-nums;
}
/* Still to pay, when something is owed. */
.sp-scm-sum > .sp-scm-owe { border-color: var(--sp-amber); background: var(--sp-amber-soft); }
.sp-scm-sum > .sp-scm-owe strong { font-size: 23px; }
/* "To be refunded" (Refund due / Cancelled cards): neutral, never amber. */
.sp-scm-sum > .sp-scm-refund { border-color: var(--sp-line-strong); background: var(--sp-surface); }
.sp-scm-pay { display: flex; align-items: center; gap: 10px 16px; flex-wrap: wrap; margin-top: 14px; }
/* Section labels ("Payment", "Payments"): --sp-ink-soft, not muted (4.17:1). */
.sp-scm-h {
  margin: 18px 0 4px; font-size: 11.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--sp-ink-soft);
}

/* Dates beside a course name on My Bookings' rows. */
.sp-mb-sc-when { font-size: 12.5px; color: var(--sp-ink-soft); }

/* ---------- the light course band (new course cards only) -----------------
   Preflight decisions, design-sage f9: palette soft background, 8px palette
   strong left edge, name in --sp-ink (>= 11.38:1 on every light colour). Used
   WITH .sp-sc-band on the new My Bookings and Invoice cards; the live strong
   band on the shop window is untouched. */
.sp-sc-band-soft { background: var(--c-soft); border-left: 8px solid var(--c-strong); }
.sp-sc-band-soft .sp-sc-band-name,
.sp-sc-band-soft .sp-sc-band-when { color: var(--sp-ink); }

/* References never break across a line (spec §5.4). */
.sp-sc-ref { white-space: nowrap; }

/* ---------- two-tone focus ring --------------------------------------------
   For controls sitting on a course colour, and on the 4+ banner's brand
   background, where a brand ring measures 1.00-1.41:1. White against the
   control, ink outside it: one of the two always contrasts. Put the class on
   the control itself or on the container holding the controls. */
.sp-focus-duo:focus-visible,
.sp-root .sp-focus-duo:focus-visible,
.sp-root .sp-focus-duo a:focus-visible,
.sp-root .sp-focus-duo button:focus-visible {
  outline: 2px solid #fff; outline-offset: 0;
  box-shadow: 0 0 0 4px var(--sp-ink);
}

@media (max-width: 560px) {
  /* Two tabs share the width; no sideways scrolling strip; 48px targets. */
  .sp-pagetabs { gap: 0; }
  .sp-pagetab,
  .sp-pagetab + .sp-pagetab {
    flex: 1 1 0; min-width: 0; min-height: 48px;
    padding: 9px 8px; white-space: normal; justify-content: center;
  }
  .sp-pagetab-name { font-size: 15px; }

  .sp-scm-sum { grid-template-columns: 1fr 1fr; }
  .sp-scm-sum > .sp-scm-owe,
  .sp-scm-sum > .sp-scm-refund { grid-column: span 2; }
  .sp-scm-pay [data-sp-pay-slot] { flex-basis: 100%; }
}
/* ======== end student-portal-special-courses s3 ========================== */

/* ==========================================================================
   student-portal-special-courses s4 - the Home banner (dashboard.php).
   (spec §2.1; gate 2 "Banner A2"; §5.4 "Banner on a phone", "Preflight
   design corrections", "Controls on a course colour"; Preflight decisions
   "4+ banner booked state". Mockup v7.5 Home; where they differ, the spec.)

   ONE BLOCK, APPENDED. No rule above this line is edited by this sub-stage.
   The s25 .sp-sc-cta rules it replaced were removed in the Verify tidy-up
   (2026-09-15).

   Three shapes, each inside a full-width .sp-c12 row:
     .sp-scb-one    1 course     palette soft ground, 1px line border, 8px
                                 palette strong left edge, ink text; brand
                                 Book now; See details in ink, underlined.
     .sp-scb-multi  2-3 courses  white card; one row per course with a 5px
                                 palette strong left edge and a brand Book.
     .sp-scb-many   4+ courses   brand blue strip: count, coloured dots, one
                                 white "See all courses" button.
   Course colours come only from the inline --c-strong / --c-soft properties
   (student_portal_courses_palette_style()). No text sits on a palette strong
   colour; the only white text is on the 4+ brand strip. Controls on a course
   colour or on the brand strip carry .sp-focus-duo (s3's two-tone ring).

   NO transform / filter / will-change / perspective / contain (the welcome
   guide's ring is position:fixed), no !important, not .sp-tabs / .sp-tab.
   ========================================================================== */

.sp-scb {
  border-radius: var(--sp-radius); box-shadow: var(--sp-shadow);
  padding: 16px 18px;
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px 20px;
}
.sp-scb-say { flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sp-scb-kicker {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
}
/* The course name (1 course) or the count (4+) is the section's h2. */
.sp-scb-title {
  margin: 0; font-size: 20px; font-weight: 700; line-height: 1.2;
  letter-spacing: 0; text-transform: none; color: inherit; text-wrap: balance;
}
.sp-scb-meta { font-size: 14px; font-weight: 600; }
.sp-scb-act { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; }
.sp-scb-btn { font-size: 15px; padding: 11px 20px; }
/* The booked words join the dates line on a phone only; a laptop has the pill. */
.sp-scb-bookedtxt { display: none; }

/* ---------- 1 course: A2 --------------------------------------------------- */

.sp-scb-one {
  background: var(--c-soft, var(--sp-brand-soft)); color: var(--sp-ink);
  border: 1px solid var(--sp-line);
  border-left: 8px solid var(--c-strong, var(--sp-brand));
}
.sp-scb-one .sp-scb-kicker,
.sp-scb-one .sp-scb-meta { color: var(--sp-ink-soft); }
.sp-scb-titlelink,
.sp-scb-titlelink:hover { color: inherit; }
.sp-scb-titlelink { text-decoration: none; }
.sp-scb-titlelink:hover { text-decoration: underline; }
/* See details: --sp-ink, underlined (brand blue fails on the MOCK soft shade). */
.sp-scb-link,
.sp-scb-link:hover {
  color: var(--sp-ink); font-size: 14px; font-weight: 700;
  text-decoration: underline; text-underline-offset: 3px;
}

/* ---------- 2-3 courses ---------------------------------------------------- */

.sp-scb-multi { padding: 14px; }
.sp-scb-multi > h2 { margin-bottom: 10px; }
.sp-scb-list { display: grid; grid-template-columns: 1fr; gap: 10px; }
.sp-scb-item {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px; min-width: 0;
  background: var(--sp-surface);
  border: 1px solid var(--sp-line);
  border-left: 5px solid var(--c-strong, var(--sp-brand));
  border-radius: var(--sp-radius-sm); padding: 10px 12px;
}
.sp-scb-item-say { flex: 1 1 auto; min-width: 0; }
.sp-scb-item-title { display: block; font-size: 15px; font-weight: 700; line-height: 1.25; color: var(--sp-ink); }
.sp-scb-item-title a,
.sp-scb-item-title a:hover { color: var(--sp-ink); }
.sp-scb-item-title a { text-decoration: none; }
.sp-scb-item-title a:hover { text-decoration: underline; }
.sp-scb-item-when { display: block; font-size: 12.5px; color: var(--sp-ink-soft); }
.sp-scb-item-act {
  display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap;
  gap: 8px; margin-left: auto;
}
@media (min-width: 768px) {
  .sp-scb-list.sp-scb-n2 { grid-template-columns: repeat(2, 1fr); }
  .sp-scb-list.sp-scb-n3 { grid-template-columns: repeat(3, 1fr); }
}

/* ---------- 4+ courses: brand strip ---------------------------------------- */

.sp-scb-many { position: relative; background: var(--sp-brand); color: #fff; }
.sp-scb-dots {
  list-style: none; margin: 6px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  font-size: 13px; font-weight: 600;
}
.sp-scb-dots li { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.sp-scb-dots i {
  flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%;
  background: var(--c-strong, #fff); box-shadow: 0 0 0 1.5px rgba(255,255,255,.6);
}
/* White button, ink words; the s3 .sp-focus-duo ring on the section. */
.sp-scb-many .sp-scb-btn { background: #fff; color: var(--sp-ink); border-color: #fff; }
.sp-scb-many .sp-scb-btn:hover,
.sp-scb-many .sp-scb-btn:focus { background: var(--sp-brand-soft); color: var(--sp-ink); border-color: var(--sp-brand-soft); }

/* ---------- phone: compact, one row per course ------------------------------
   spec §5.4 "Banner on a phone": title (links to details), dates, one button
   at least 44px tall. Hidden: kicker, venue text, See details (incl. the 2-3
   card's h2 link), pills (the booked words move into the dates line) and the
   names under the 4+ dots (kept for a screen reader). 2-3 rows lose their
   card borders and stack as a divided list. */
@media (max-width: 560px) {
  .sp-scb { flex-wrap: nowrap; padding: 12px 12px 12px 14px; gap: 8px 12px; }
  .sp-scb-say { flex: 1 1 0; gap: 1px; }
  .sp-scb-kicker,
  .sp-scb-more,
  .sp-scb-link,
  .sp-scb-act .sp-pill,
  .sp-scb-item-act .sp-pill,
  .sp-scb-multi > h2 a { display: none; }
  .sp-scb-bookedtxt { display: inline; font-weight: 700; }
  .sp-scb-title { font-size: 16px; line-height: 1.25; }
  .sp-scb-meta { font-size: 13px; }
  .sp-scb-titlelink,
  .sp-scb-item-title a {
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
  }
  .sp-scb-act { flex: 0 0 auto; }
  .sp-scb-btn { font-size: 14px; padding: 10px 14px; min-height: 44px; }

  .sp-scb-dots { margin-top: 5px; gap: 6px; }
  .sp-scb-dots i { width: 12px; height: 12px; }
  .sp-scb-dotname {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }

  .sp-scb-multi { padding: 10px 12px; }
  .sp-scb-multi > h2 { margin-bottom: 4px; }
  .sp-scb-list { gap: 0; }
  .sp-scb-item {
    flex-wrap: nowrap; border: 0; border-radius: 0;
    border-top: 1px solid var(--sp-line);
    border-left: 5px solid var(--c-strong, var(--sp-brand));
    padding: 6px 0 6px 10px;
  }
  .sp-scb-item:first-child { border-top: 0; }
  .sp-scb-item-title { font-size: 14.5px; }
  .sp-scb-item-act { flex: 0 0 auto; margin-left: 0; }
  .sp-scb-item-act .sp-btn { min-height: 44px; }
}
/* ======== end student-portal-special-courses s4 ========================== */

/* ==========================================================================
   student-portal-special-courses s5 - My Bookings > Special courses
   (my_classes.php). (spec §2.2, §2.7; §5.4 my_classes.php row, "Preflight
   design corrections"; Preflight decisions "Course card band". Mockup v7.5
   My Bookings; where they differ, the spec.)

   ONE BLOCK, APPENDED. No rule above this line is edited by this sub-stage.
   The course card is the live .sp-sc-course / .sp-sc-band / .sp-sc-body with
   s3's .sp-sc-band-soft band and .sp-scm-* money boxes. This block adds only
   the card's meta and link rows, chip spacing inside its body, the "Also open
   to book" rows, and the space above Staying in touch.

   NO transform / filter / will-change / perspective / contain (the welcome
   guide's ring is position:fixed), no !important, not .sp-tabs / .sp-tab.
   ========================================================================== */

/* Staying in touch follows the tabset, not a card, so the page's
   .sp-card + .sp-card spacing (room to breathe, above) needs this form too. */
.sp-mc-page .sp-pagetabset + .sp-card { margin-top: 18px; }
@media (min-width: 768px) {
  .sp-mc-page .sp-pagetabset + .sp-card { margin-top: 22px; }
}

/* ---------- course card body ----------------------------------------------- */

/* A card note line (every booking on the course cancelled). --sp-ink-soft, not muted. */
.sp-mb-scb-meta { margin: 0 0 10px; font-size: 13.5px; line-height: 1.5; color: var(--sp-ink-soft); }
.sp-mc-page .sp-mb-scb-paid { margin: 14px 0 0; }
.sp-mb-scb-links {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  margin: 16px 0 0; font-size: 13.5px; font-weight: 600;
}

/* ---------- Also open to book ---------------------------------------------- */

.sp-mb-sc-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px;
  padding: 10px 0; border-top: 1px solid var(--sp-line);
  color: var(--sp-ink); text-decoration: none;
}
.sp-root a.sp-mb-sc-row { color: var(--sp-ink); text-decoration: none; }
.sp-mb-sc-row:first-of-type { border-top: 0; padding-top: 2px; }
/* The course's colour as a square beside its name; decoration only. */
.sp-mb-sc-row i {
  flex: 0 0 auto; width: 12px; height: 12px; border-radius: 3px;
  background: var(--c-strong, var(--sp-brand));
}
.sp-mb-sc-row b { font-size: 14.5px; font-weight: 700; line-height: 1.3; }
.sp-mb-sc-go {
  margin-left: auto; font-size: 13.5px; font-weight: 600;
  color: var(--sp-brand); white-space: nowrap;
}
.sp-root a.sp-mb-sc-row:hover b { text-decoration: underline; }

/* ---------- phone ---------------------------------------------------------- */

@media (max-width: 560px) {
  .sp-mb-scb-links { gap: 0 18px; margin-top: 10px; }
  .sp-mb-scb-links a { display: inline-flex; align-items: center; min-height: 44px; }

  /* Name over dates, Book on the right; each row at least 44px tall. */
  .sp-mb-sc-row {
    display: grid; grid-template-columns: 12px minmax(0, 1fr) auto;
    column-gap: 12px; row-gap: 1px; min-height: 44px;
  }
  .sp-mb-sc-row i               { grid-column: 1; grid-row: 1 / span 2; align-self: center; }
  .sp-mb-sc-row b               { grid-column: 2; grid-row: 1; }
  .sp-mb-sc-row .sp-mb-sc-when  { grid-column: 2; grid-row: 2; }
  .sp-mb-sc-go                  { grid-column: 3; grid-row: 1 / span 2; margin-left: 0; align-self: center; }
}
/* ======== end student-portal-special-courses s5 ========================== */

/* ==========================================================================
   student-portal-special-courses s6 - Invoice > Special courses
   (billing.php). (spec §2.4, §2.7; §5.4 billing.php row, "Preflight
   corrections for s1", "Preflight design corrections"; Preflight decisions
   "Course card band". Mockup v7.5 Invoice; where they differ, the spec.)

   ONE BLOCK, APPENDED. No rule above this line is edited by this sub-stage.
   The course card reuses the live .sp-sc-course / .sp-sc-body, s3's
   .sp-sc-band-soft band and .sp-scm-* money boxes, and s5's unscoped
   .sp-mb-scb-meta / .sp-mb-scb-links rows. The Payments and Earlier tables
   are the live .sp-table inside .sp-tablewrap (it stacks on a phone, one
   labelled block per row, from data-label); the Earlier fold is the live
   .sp-acc. This block adds only the lede, the paid message's spacing inside
   the panel, right-aligned money columns, the empty-table line, the
   references under an Earlier course name, and a white ground behind the
   fold, which sits on the page rather than in a card.

   NO transform / filter / will-change / perspective / contain (the welcome
   guide's ring is position:fixed), no !important, not .sp-tabs / .sp-tab.
   ========================================================================== */

/* "Holiday courses and mock exams are paid separately ..." --sp-ink-soft. */
.sp-inv-sc-lede { margin: 0 0 14px; font-size: 14px; line-height: 1.5; color: var(--sp-ink-soft); }

/* s5 scopes the paid message's spacing to My Bookings; the same inside this panel. */
.sp-inv-sc .sp-mb-scb-paid { margin: 14px 0 0; }

/* ---------- Payments and Earlier tables ------------------------------------ */

/* Money columns right-aligned, figures lined up; the last column meets the
   card's padding rather than the table's 12px cell gutter. */
.sp-table th.sp-inv-sc-num,
.sp-table td.sp-inv-sc-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.sp-inv-sc .sp-table th:last-child,
.sp-inv-sc .sp-table td:last-child { padding-right: 0; }

/* "No payments yet." --sp-ink-soft, not muted. */
.sp-table td.sp-inv-sc-empty { font-size: 13.5px; color: var(--sp-ink-soft); }

/* Earlier special courses: the fold is not inside a card, so it gets a card's white. */
.sp-inv-sc-earlier { background: var(--sp-surface); border-radius: var(--sp-radius-sm); }
/* The booking reference(s) under a course's full name. */
.sp-inv-sc-refs { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 500; color: var(--sp-ink-soft); }

/* ---------- phone ---------------------------------------------------------- */

@media (max-width: 560px) {
  .sp-inv-sc-lede { font-size: 13.5px; margin-bottom: 12px; }
}
/* ======== end student-portal-special-courses s6 ========================== */

/* ======== student-portal-special-courses: card v2 (2026-09-15) ============
   philip: what was booked comes first -- Dates (with the days), Times, Where
   and the subjects -- and one booking reference, small, at the bottom. Used by
   My Bookings and Invoice. */

/* Dates / Times / Where: three quiet tiles; one column on a phone. */
.sp-scf { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 0 0 14px; }
.sp-scf > div { min-width: 0; background: var(--sp-ground); border-radius: var(--sp-radius-sm); padding: 10px 12px; }
.sp-scf dt { margin: 0 0 3px; font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--sp-ink-soft); }
.sp-scf dd { margin: 0; font-size: 15.5px; font-weight: 650; line-height: 1.35; color: var(--sp-ink); overflow-wrap: anywhere; }
.sp-scf dd small { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 400; color: var(--sp-ink-soft); }

/* Subjects booked: large chips in the course's light shade (ink on soft
   >= 11.38:1 on every course colour, Preflight decisions). */
.sp-scs { margin: 0 0 14px; }
.sp-scs-h { margin: 0 0 6px; font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--sp-ink-soft); }
.sp-scs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.sp-scs-list li { background: var(--c-soft, var(--sp-brand-soft)); border: 1px solid var(--c-strong, var(--sp-line-strong)); color: var(--sp-ink);
                  border-radius: 999px; padding: 6px 12px; font-size: 14.5px; font-weight: 600; line-height: 1.3; }
.sp-scs-list li span { font-weight: 400; color: var(--sp-ink-soft); }
.sp-scs-note { margin: 8px 0 0; font-size: 13px; line-height: 1.5; color: var(--sp-ink-soft); }

/* Invoice: the one-line facts, then "What you booked" with a cost per subject. */
.sp-scf-line { margin: 0 0 12px; font-size: 14.5px; line-height: 1.5; color: var(--sp-ink); }
.sp-scf-line b { font-weight: 650; }
.sp-sc-costs { width: 100%; border-collapse: collapse; margin: 0 0 14px; font-size: 14.5px; font-variant-numeric: tabular-nums; }
.sp-sc-costs th,
.sp-sc-costs td { padding: 8px 0; border-bottom: 1px solid var(--sp-line); vertical-align: top; text-align: left; font-weight: 400; }
.sp-sc-costs td:last-child { text-align: right; white-space: nowrap; padding-left: 12px; }
.sp-sc-costs-det { display: block; font-size: 12.5px; color: var(--sp-ink-soft); }
.sp-sc-costs tr.sp-sc-costs-total th,
.sp-sc-costs tr.sp-sc-costs-total td { font-weight: 700; border-bottom: 0; border-top: 2px solid var(--sp-line-strong); }

/* The course name is the card's heading (Verify design-sage F1): an h2 inside
   the band, so no heading margin; size and weight stay .sp-sc-band-name's. */
h2.sp-sc-band-name { margin: 0; line-height: inherit; }

/* Payments: an older reference is marked beside the payment made with it. */
.sp-sc-oldref { display: block; font-size: 12px; color: var(--sp-ink-soft); }
/* One item in the phone flex cell, so the earlier-reference line sits under the method. */
.sp-sc-method { display: block; }

/* One booking reference, small, at the bottom of the card. */
.sp-sc-reffoot { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--sp-line); font-size: 12.5px; color: var(--sp-ink-soft); }

@media (max-width: 560px) {
  .sp-scf { grid-template-columns: 1fr; gap: 8px; }
  .sp-scf > div { display: flex; align-items: baseline; gap: 10px; padding: 8px 12px; }
  .sp-scf dt { flex: 0 0 58px; margin: 0; }
}
/* ======== end card v2 ====================================================== */

/* ---------------------------------------------------------------------------
   The Year 7-11 Maths plan — textbook pages, homework pages, and the books.

   Two rules and no new colour. A plan line is the cell's own content, not an
   annotation on something else, so it takes the table's ink at the weight the
   Week column already uses; everything secondary to it (the topic, the syllabus
   area, what each book is for) reuses .sp-sub2 rather than inventing a second
   muted style. Both are block so a cell can stack page, topic and button
   without a wrapper.
   ------------------------------------------------------------------------ */
.sp-plan { display: block; font-weight: 600; color: var(--sp-ink); }
/* Against the table's 14px, and against the card's 15px where the books sit. */
.sp-table .sp-plan { font-size: 14px; }
.sp-table td .sp-plan + .sp-btn,
.sp-table td .sp-plan-row + .sp-btn { margin-top: 6px; }

/* Classwork and Homework, one labelled line each.

   The label runs INLINE with the value rather than sitting in a fixed column
   beside it. Written out in full ("Classwork", not "CW") the label is wide
   enough that a fixed column would push the pages a third of the way across a
   table cell and leave Year 7's eighty-character ranges wrapping in a gutter.
   Inline, the pair reads as one phrase and wraps as one. */
.sp-plan-row { display: block; }
.sp-plan-row + .sp-plan-row { margin-top: 3px; }
.sp-plan-lab {
  font-size: 11.5px; font-weight: 600; color: var(--sp-muted); margin-right: 6px;
}
/* .sp-plan is display:block by default so it can stack under a button; beside a
   label it has to sit on the same line. */
.sp-plan-row .sp-plan { display: inline; }

/* The books, once at the top of a subject. A row on a wide screen, a stack on a
   narrow one - two titles side by side below about 560px would each wrap to
   three lines and read as one run-on sentence. */
.sp-books { margin: 0 0 14px; display: flex; flex-wrap: wrap; gap: 6px 22px; align-items: flex-start; }
.sp-books-lead {
  flex: 0 0 100%; font-size: 12px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--sp-muted);
}
.sp-book { flex: 1 1 220px; min-width: 0; }
.sp-book .sp-sub2 { margin-top: 1px; }
@media (max-width: 560px) {
  .sp-book { flex: 0 0 100%; }
}

/* The plan on the dashboard. Both sit inside cards that already control their
   own spacing, so these only claim the gap above themselves. */
/* Inside the session row's INFO column, under the tutor line. The info column
   mutes every span it contains at 12.5px, which is right for the tutor line and
   wrong for a statement of what to bring, so both halves are restated here. */
.sp-session-plan { margin-top: 6px; }
.sp-session-info .sp-session-plan .sp-plan { font-size: 13px; color: var(--sp-ink); display: inline; margin-top: 0; }
.sp-session-info .sp-session-plan .sp-plan-lab { font-size: 11.5px; color: var(--sp-muted); display: inline; margin-top: 0; }
.sp-session-info .sp-session-plan .sp-plan-row { display: block; margin-top: 2px; }
/* On the homework card, between the week line and the deadline. */
.sp-due-plan { margin-top: 4px; }
