*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --bg: #f7f7f5;
  --surface: #ffffff;
  --surface2: #f2f2ef;
  --border: rgba(0,0,0,0.09);
  --border-mid: rgba(0,0,0,0.16);
  --text: #1a1a18;
  --text2: #6b6b67;
  --text3: #9e9e9a;
  --green: #1B6B4A;
  --green-light: #E8F5EE;
  --green-mid: #1D9E75;
  --amber: #854F0B;
  --amber-light: #FAEEDA;
  --amber-mid: #EF9F27;
  --red: #A32D2D;
  --red-light: #FCEBEB;
  --red-mid: #E24B4A;
  --blue: #185FA5;
  --blue-light: #E6F1FB;
  --purple: #6B3FA0;
  --purple-light: #F1EAFB;
  --purple-mid: #9B6FD1;
  --radius: 8px;
  --radius-lg: 12px;
  --sidebar-w: 220px;
  --panel-w: 380px;

  /* ---- TYPE SCALE ----
     Six sizes, and every piece of screen text is one of them. Before this
     there were 17 sizes in here plus 414 inline font-size declarations in
     the JS, which is what made the app read as unresolved rather than wrong.
     Every merge rounded UP, never down — staff read these screens all day.

     Two things deliberately keep their own sizes and are NOT on this scale:
     @media print (a table has to fit a sheet of paper, so 8.5/9.5px there
     is correct), and icon glyph sizes, which are a separate concern from
     text and sit at 14px and above. */
  --t-title: 22px;   /* page titles, one per screen */
  --t-head: 15px;    /* section headings, modal titles, card headers */
  --t-body: 13px;    /* the default — table cells, form fields */
  --t-meta: 12px;    /* supporting detail beside body text */
  --t-small: 11px;   /* pills, buttons, captions, helper text */
  --t-label: 10px;   /* table column headers. Nothing else. */

  /* ---- SPACING ----
     Five steps on a 4px grid, replacing 12 gap values and 10 margins. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;

  /* ---- MOTION ----
     Two durations and one curve, deliberately. --dur-1 is a state change
     (hover, tint), --dur-2 is something entering or leaving. Anything that
     needs a third duration is probably an animation we agreed not to add. */
  --dur-1: 120ms;
  --dur-2: 200ms;
  --ease: cubic-bezier(.2, 0, .2, 1);
}

/* Honour the OS setting. Without this the six animations below are
   imposed on people who have asked the system for no motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
/* The base size stays 14px and deliberately off the scale: it is what every
   element without its own font-size inherits, not a size anyone chose. The
   scale describes text we style; moving this moves everything we don't. */
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }

/* ---- AUTH ---- */
.auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); }
.auth-card { background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 40px 36px; width: 360px; }
.auth-logo { font-size: 26px; font-weight: 600; color: var(--green); letter-spacing: -0.5px; margin-bottom: 4px; }
.auth-logo-img { display: block; width: 172px; max-width: 100%; height: auto; margin-bottom: 10px; }
.auth-sub { font-size: var(--t-meta); color: var(--text3); margin-bottom: 28px; }
.auth-label { display: block; font-size: var(--t-meta); color: var(--text2); margin-bottom: 5px; font-weight: 500; }
.auth-input { width: 100%; padding: 9px 12px; border: 0.5px solid var(--border-mid); border-radius: var(--radius); font-size: var(--t-head); color: var(--text); background: var(--surface); margin-bottom: 14px; outline: none; }
.auth-input:focus { border-color: var(--green-mid); box-shadow: 0 0 0 3px rgba(29,158,117,0.1); }
.auth-btn { width: 100%; padding: 10px; background: var(--green); color: white; border: none; border-radius: var(--radius); font-size: var(--t-head); font-weight: 500; cursor: pointer; margin-top: 4px; }
.auth-btn:hover { background: #155a3c; }
.auth-error { background: var(--red-light); color: var(--red); font-size: var(--t-meta); padding: 8px 12px; border-radius: var(--radius); margin-bottom: 12px; display: none; }
.auth-error.show { display: block; }

/* ---- APP SHELL ---- */
.app { display: flex; height: 100vh; overflow: hidden; }
.hidden { display: none !important; }

/* ---- SIDEBAR ---- */
.sidebar { width: var(--sidebar-w); background: var(--surface); border-right: 0.5px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-logo { padding: 22px 18px 16px; border-bottom: 0.5px solid var(--border); }
.logo-img { display: block; width: 128px; max-width: 100%; height: auto; }
.logo-name { font-size: 20px; font-weight: 600; color: var(--green); letter-spacing: -0.3px; }
.logo-sub { font-size: var(--t-label); color: var(--text3); margin-top: 2px; letter-spacing: 0.3px; }
/* Which branch this login works for, always in view: a desk must never have
   to guess whose children and money it is looking at. Neutral, not a colour
   (colour means "needs you"). */
.branch-badge { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; padding: 3px 9px; border-radius: 20px; background: var(--surface2); border: 0.5px solid var(--border); font-size: var(--t-small); font-weight: 600; color: var(--text); max-width: 100%; }
.branch-badge .ti { font-size: 13px; color: var(--text3); }
.nav { padding: 12px 8px; flex: 1; overflow-y: auto; }
/* Nav groups are spacing and a rule, not headings — see the comment in the
   markup. A block hidden by role collapses entirely so no stray rule is left
   behind. */
.nav-block { padding: 9px 0; border-top: 0.5px solid var(--border); }
.nav-block:first-child { padding-top: 0; border-top: none; }
.nav-block.hidden { display: none; }
/* Real <button>s, not divs: as divs these could not be reached by Tab at
   all, so the whole sidebar was mouse-only. */
.nav-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px; border: 0; background: none; text-align: left; font-family: inherit; border-radius: var(--radius); cursor: pointer; font-size: var(--t-body); color: var(--text2); transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease); margin-bottom: 1px; user-select: none; }
.nav-item:hover { background: var(--surface2); color: var(--text); }
.nav-item:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--green-mid); }
.nav-item.active { background: var(--green-light); color: var(--green); font-weight: 500; }
.nav-item i { font-size: 16px; }
.nav-item.active i { color: var(--green); }
.sidebar-user { padding: 12px 16px; border-top: 0.5px solid var(--border); }
.user-name { font-size: var(--t-meta); font-weight: 500; color: var(--text); }
.user-role { font-size: var(--t-small); color: var(--text3); margin-bottom: 6px; }
.logout-btn { font-size: var(--t-small); color: var(--text3); cursor: pointer; background: none; border: none; padding: 0; display: flex; align-items: center; gap: 4px; }
.logout-btn:hover { color: var(--red); }

/* ---- MAIN ---- */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
.topbar { padding: 12px 20px; background: var(--surface); border-bottom: 0.5px solid var(--border); display: flex; align-items: center; justify-content: space-between; min-height: 52px; flex-shrink: 0; }
.page-title { font-size: var(--t-title); font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.topbar-right { display: flex; align-items: center; gap: 8px; }
.date-nav { display: flex; align-items: center; gap: 5px; }
.date-btn { background: none; border: 0.5px solid var(--border-mid); border-radius: var(--radius); padding: 5px 8px; cursor: pointer; color: var(--text2); font-size: var(--t-body); display: flex; align-items: center; }
.date-btn:hover { background: var(--surface2); }
.date-label { font-size: var(--t-meta); font-weight: 500; color: var(--text); min-width: 160px; text-align: center; background: var(--surface2); padding: 5px 10px; border-radius: var(--radius); }
.content-area { flex: 1; overflow-y: auto; padding: 18px 20px; }

/* ---- PAGE VIEWS ---- */
.page-view { display: none; }
.page-view.active { display: block; }

/* ---- STAT CARDS ---- */
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px; }
.stat-card { background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; }
.stat-label { font-size: var(--t-label); color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 5px; }
/* Qualifier under a headline figure — the thing that stops the number being
   read as something it is not. */
.stat-sub { font-size: var(--t-label); color: var(--text3); margin-top: 3px; }
.stat-value { font-size: 26px; font-weight: 500; color: var(--text); line-height: 1; }

/* ---- CLASS CARDS ---- */
.classes-grid { display: flex; flex-direction: column; gap: 12px; }
/* Physical / Online split on the day view (078: online classes are shared). */
.class-section-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding: 8px 2px 0; color: var(--text2); }
.class-section-head:first-child { padding-top: 0; }
.class-section-head i { font-size: 16px; color: var(--text3); align-self: center; }
.class-section-title { font-weight: 600; font-size: var(--t-body, 14px); color: var(--text); }
.class-section-count { font-size: var(--t-meta, 13px); color: var(--text3); }
.class-section-sub { font-size: var(--t-small, 12px); color: var(--text3); margin-left: auto; }
/* Another branch's desk on a shared online class: who takes the register,
   in the spot the attendance control would be (079). Quiet, not a warning. */
.class-run-by { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border: 0.5px solid var(--border); border-radius: var(--radius-md, 8px); font-size: var(--t-small, 12px); color: var(--text3); white-space: nowrap; }
/* position: relative so the is-now progress bar can pin to the card's top edge. */
.class-card { background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; position: relative; }
/* Stacked, not side by side. The two children ARE the two lines of the card
   — laying them out in a row put the meta strip beside the title instead of
   under it, which is the layout this round set out to fix. */
.class-header { padding: 14px 18px; display: flex; flex-direction: column; align-items: stretch; border-bottom: 0.5px solid var(--border); }
/* The divider separates the header from a body. A single class card has no
   body, so without this it ends on a rule hanging off its own bottom edge. */
.class-card > .class-header:last-child { border-bottom: none; }
.class-left { display: flex; flex-direction: column; gap: 6px; }
.class-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.badge { font-size: var(--t-small); font-weight: 600; padding: 3px 8px; border-radius: 20px; }
/* Mode, room, session type and Single/Bundled are all *classifications*: true
   of a class permanently, never something needing attention. They used to be
   coloured (green physical / blue online / indigo session), which meant five
   colours were lit on a card before anything had actually happened — and the
   amber and red that do need a human had to shout over them. They read as
   neutral now; the words already say "Room A" or "Online". Colour on a class
   card means either "this is that course" (the course tag below) or
   "something needs you" (amber/red). */
.badge-physical, .badge-online, .badge-room,
.badge-session-a, .badge-session-b, .badge-classkind {
  background: var(--surface2); color: var(--text2);
}

/* ---- COURSE COLOUR FAMILIES ---- */
/* Brand colours, sampled from the website: pink Mommy & Me #D83B79, green
   Happee Early Literacy #62B254, blue Happee Literacy #57A8DE. Levels within
   a family share a colour, and an online course wears its family's colour
   rather than anything mode-derived.
   The backgrounds are 10% tints of those hues. The text tones are darkened
   deliberately: the brand colours are display colours meant for large bold
   headings, and at this size they fail contrast badly on white (#62B254 is
   about 2.6:1, #57A8DE about 2.3:1, against the 4.5:1 small text needs).
   These pairings measure 4.8-5.3:1. */
.course-tag { display: inline-block; font-size: var(--t-body); font-weight: 600; padding: 3px 10px; border-radius: 20px; }
.cf-mommy_and_me   { background: #FBEBF2; color: #B32A61; }
.cf-early_literacy { background: #EFF7EE; color: #3D7A33; }
.cf-literacy       { background: #EEF6FC; color: #1E6FA8; }
/* Foundation (087): purple, the one hue not already meaning something here
   (amber = needs you, red = full, blue = online/Literacy). 6.0:1. */
.cf-foundation     { background: #F3EFFA; color: #6B46A8; }
/* No family set: plain text, exactly as it looked before any of this. A course
   with no family must never be given a colour it might not own. */
.course-plain { font-size: var(--t-body); font-weight: 500; color: var(--text); }
.class-meta { display: flex; align-items: center; gap: 14px; }
.class-meta-item { display: flex; align-items: center; gap: 4px; font-size: var(--t-meta); color: var(--text2); }
.class-meta-item i { font-size: 13px; color: var(--text3); }
.class-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.teacher-pill { display: flex; align-items: center; gap: 6px; background: var(--surface2); border-radius: 20px; padding: 4px 10px 4px 5px; font-size: var(--t-meta); color: var(--text2); }
.student-list { padding: 10px 16px 12px; display: flex; flex-wrap: wrap; gap: 6px; min-height: 44px; }
.student-chip { display: flex; align-items: center; gap: 6px; border-radius: 20px; padding: 4px 10px 4px 6px; font-size: var(--t-meta); cursor: pointer; transition: opacity 0.1s; border: 0.5px solid var(--border); background: var(--surface2); color: var(--text); }
.student-chip:hover { opacity: 0.8; }
/* A makeup chip is a plain chip: it already prints the word "makeup" as a tag,
   so the amber tint (and its amber border) was a second encoding of a fact
   already stated in words. Both are dropped so the chip inherits the standard
   surface and border — a tinted body with a neutral border read as an
   unfinished half-measure. The .makeup-tag below still carries the amber. */
.student-chip.static { cursor: default; }
.student-chip.static:hover { opacity: 1; }
.makeup-tag { font-size: var(--t-label); background: var(--amber-light); color: var(--amber); padding: 1px 5px; border-radius: 3px; font-weight: 500; }
.session-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.dot-ok { background: var(--green-mid); }
.dot-low { background: var(--amber-mid); }
.dot-none { background: var(--red-mid); }
.capacity-pill { font-size: var(--t-small); padding: 2px 8px; border-radius: 20px; }
/* "Room to spare" is the normal case and was spending colour to say nothing.
   Only the two states that change what you'd do — nearly full, full — are
   coloured now. */
.cap-ok { background: var(--surface2); color: var(--text2); }
.cap-warn { background: var(--amber-light); color: var(--amber); }
.cap-full { background: var(--red-light); color: var(--red); }
.empty-day { text-align: center; padding: 60px 20px; color: var(--text3); }
.empty-day i { font-size: 32px; display: block; margin-bottom: 10px; opacity: 0.5; }
/* "Now" used to be a 2px blue border. Blue now means online on the badge and
   Happee Literacy on the course tag, so the border was the same ink as two
   things it isn't — and it beat the course colour outright on the one card
   people look at most. It reads as a near-black progress bar across the top
   of the card instead: no hue at all, in space nothing else uses, and it
   answers "how much longer" as well as "which one". */
.class-card.is-now { border: 0.5px solid var(--border-mid); }
.lesson-progress { position: absolute; top: 0; left: 0; right: 0; height: 3px; background: rgba(26,26,24,0.09); }
.lesson-progress > span { display: block; height: 100%; background: var(--text); }
.pill-now { background: var(--text); color: #fff; font-weight: 600; }
.holiday-row { background: var(--surface2); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 10px 16px; display: flex; align-items: center; gap: 12px; color: var(--text3); }
/* Attendance mark on a student chip. The topbar legend that decoded the old
   coloured dot is gone, so each state now differs in SHAPE as well as colour
   and carries its own [data-tip]. The webfont's tick and cross are hairlines
   at 16px, so they get a stroke: 1.1px off a 0.7/1.1/1.5 ladder — 1.5 closes
   up the notch inside the tick. */
.chip-mark { font-size: 16px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; flex: none; }
.chip-mark.present { color: var(--green-mid); -webkit-text-stroke: 1.1px currentColor; }
.chip-mark.absent { color: var(--red-mid); -webkit-text-stroke: 1.1px currentColor; }
.chip-mark.unmarked { color: var(--text3); }
/* Filled badge, not a glyph: a bare "L" next to the avatar would read as an
   initial. White on purple makes it unmistakably a status. */
.chip-mark.leave { width: 16px; height: 16px; border-radius: 50%; background: var(--purple-mid); color: #fff; font-size: var(--t-label); font-weight: 600; }
.day-progress { display: flex; align-items: center; gap: 12px; background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 10px 16px; margin-bottom: 12px; }

/* One segment per class instead of one continuous bar. On live, 19 of 30
   days have only one or two classes, so a percentage-width fill was a
   scale pretending to be finer than the data it drew: "1 of 2" is a half
   slab that reads like a measurement. Segments count instead. */
/* DEAD, DELIBERATELY KEPT until the Cloudflare cache window has fully turned
   over. Deleting these in the SAME deploy that changed the JS produced a live
   broken window: browsers ran the cached OLD dashboard.js, which still emits
   .day-progress-track, against the NEW stylesheet, where the rules no longer
   existed — so the bar rendered as invisible unstyled divs and the row showed
   text with a gap where the bar should be. See reference_pages_cache_trap.
   RULE: when JS stops emitting a class, remove the JS first and the CSS a
   deploy later. Old markup with new CSS must still render something. */
.day-progress-track { flex: 1; height: 6px; background: var(--surface2); border-radius: 20px; overflow: hidden; }
.day-progress-fill { height: 100%; background: var(--text); }
.day-progress-segs { flex: 1; display: flex; align-items: center; gap: 3px; }
.day-progress-seg { flex: 1; height: 6px; background: var(--surface2); border-radius: 20px; }
.day-progress-seg.done { background: var(--text); }
/* A fully-marked day used to render as a full-width slab of the heaviest
   ink on the page — the loudest possible way to say "there is nothing to
   do here". Complete means done, so the whole row recedes. */
.day-progress.complete .day-progress-seg.done { background: var(--text3); }
.day-progress-label { font-size: var(--t-meta); color: var(--text2); white-space: nowrap; }
.day-progress-count { font-size: var(--t-meta); font-weight: 500; white-space: nowrap; }
/* Both spans are classed rather than inline-styled so this can reach them:
   an inline colour beats any class rule, which left a finished day greying
   only half its own row. */
.day-progress.complete .day-progress-label,
.day-progress.complete .day-progress-count { color: var(--text3); }
.search-dimmed { opacity: 0.35; }

/* ---- AVATAR ---- */
.avatar { border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 500; flex-shrink: 0; }
.av-green { background: #9FE1CB; color: #085041; }
.av-blue { background: #B5D4F4; color: #0C447C; }
.av-amber { background: #FAC775; color: #633806; }
.av-pink { background: #F4C0D1; color: #72243E; }
.av-purple { background: #CECBF6; color: #3C3489; }

/* ---- SIDE PANEL ---- */
.side-panel { width: var(--panel-w); background: var(--surface); border-left: 0.5px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; transition: width 0.2s; }
.side-panel.collapsed { width: 0; }
.panel-header { padding: 13px 16px; border-bottom: 0.5px solid var(--border); display: flex; align-items: center; justify-content: space-between; min-height: 52px; flex-shrink: 0; }
.panel-title { font-size: var(--t-body); font-weight: 500; color: var(--text); }
.panel-close { background: none; border: none; cursor: pointer; color: var(--text3); font-size: 18px; padding: 2px 4px; border-radius: 4px; }
.panel-close:hover { color: var(--text); background: var(--surface2); }
.panel-body { flex: 1; overflow-y: auto; padding: 16px 18px; }
.panel-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; color: var(--text3); font-size: var(--t-body); text-align: center; gap: 8px; }
.panel-empty i { font-size: 28px; }
.sd-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.sd-name { font-size: var(--t-head); font-weight: 500; color: var(--text); }
.sd-sub { font-size: var(--t-meta); color: var(--text2); margin-top: 1px; }
.credit-box { background: var(--surface2); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 12px; }
.credit-row { display: flex; justify-content: space-between; font-size: var(--t-meta); padding: 3px 0; }
.credit-label { color: var(--text2); }
.credit-val { font-weight: 500; color: var(--text); }
.detail-section { margin-bottom: 14px; }
.detail-section-title { font-size: var(--t-label); text-transform: uppercase; letter-spacing: 0.5px; color: var(--text3); margin-bottom: 7px; }
.detail-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 0.5px solid var(--border); font-size: var(--t-meta); }
.detail-row:last-child { border-bottom: none; }
.detail-label { color: var(--text2); }
.detail-val { color: var(--text); font-weight: 500; }
.action-btn { width: 100%; padding: 8px 12px; background: none; border: 0.5px solid var(--border-mid); border-radius: var(--radius); font-size: var(--t-meta); color: var(--text2); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 6px; transition: background 0.1s, color 0.1s; }
.action-btn:hover { background: var(--surface2); color: var(--text); }
.action-btn.primary { background: var(--green-light); color: var(--green); border-color: transparent; }
.action-btn.primary:hover { background: #c5eed8; }
.attendance-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 0.5px solid var(--border); font-size: var(--t-meta); }
.attendance-item:last-child { border-bottom: none; }
.att-date { color: var(--text2); }
.att-status { font-weight: 500; }
.att-present { color: var(--green); }
.att-absent { color: var(--red); }
.att-makeup { color: var(--amber); font-size: var(--t-label); margin-left: 4px; }

/* ---- TABLES ---- */
.table-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.search-wrap { display: flex; align-items: center; gap: 7px; background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius); padding: 7px 11px; min-width: 240px; }
.search-wrap i { font-size: 15px; color: var(--text3); flex-shrink: 0; }
.search-wrap input { border: none; outline: none; background: none; font-size: var(--t-body); color: var(--text); width: 100%; }
.search-wrap input::placeholder { color: var(--text3); }
/* Mirrors the magnifier on the left. `visibility` rather than `display`
   so the button always occupies its slot: toggling display would shrink
   and re-grow the input as the first character is typed. */
.search-clear { visibility: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 50%; background: none; color: var(--text3); cursor: pointer; }
.search-clear.is-visible { visibility: visible; }
.search-clear i { font-size: 13px; }
.search-clear:hover { background: var(--surface2); }
/* `.search-wrap i` sets the glyph colour directly, so it beats a colour on
   the button — the hover has to reach the icon itself or only the circle
   would react. */
.search-clear:hover i { color: var(--text); }
.count-label { font-size: var(--t-meta); color: var(--text3); }
.btn { display: inline-flex; align-items: center; gap: 5px; padding: 7px 12px; border-radius: var(--radius); font-size: var(--t-meta); cursor: pointer; border: 0.5px solid var(--border-mid); background: none; color: var(--text2); transition: background 0.1s; }
.btn:hover { background: var(--surface2); color: var(--text); }
.btn.btn-primary { background: var(--green); color: white; border-color: transparent; }
.btn.btn-primary:hover { background: #155a3c; }
.btn i { font-size: 14px; }
.data-table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: var(--radius-lg); overflow: hidden; border: 0.5px solid var(--border); font-size: var(--t-body); }
.data-table th { text-align: left; padding: 11px 16px; font-size: var(--t-label); text-transform: uppercase; letter-spacing: 0.5px; color: var(--text3); font-weight: 500; background: var(--surface2); border-bottom: 0.5px solid var(--border); }
.data-table td { padding: 12px 16px; border-bottom: 0.5px solid var(--border); color: var(--text); vertical-align: middle; }
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--surface2); cursor: pointer; }
.no-data { text-align: center; padding: 40px; color: var(--text3); font-size: var(--t-body); }
.pill { display: inline-flex; align-items: center; gap: 4px; font-size: var(--t-small); padding: 2px 8px; border-radius: 20px; font-weight: 500; }
.pill-green { background: var(--green-light); color: var(--green); }
.pill-amber { background: var(--amber-light); color: var(--amber); }
.pill-red { background: var(--red-light); color: var(--red); }
.pill-blue { background: var(--blue-light); color: var(--blue); }
.pill-gray { background: var(--surface2); color: var(--text2); }
.pill-purple { background: var(--purple-light); color: var(--purple); }

/* ---- MODAL ---- */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal-overlay.hidden { display: none; }
.modal { background: var(--surface); border-radius: var(--radius-lg); width: 480px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 8px 40px rgba(0,0,0,0.15); }
.modal-wide { width: 720px; }
.modal-header { padding: 16px 20px; border-bottom: 0.5px solid var(--border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.modal-title { font-size: var(--t-head); font-weight: 500; color: var(--text); }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.modal-footer { padding: 14px 20px; border-top: 0.5px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0; }
.form-group { margin-bottom: 14px; }
.form-label { display: block; font-size: var(--t-meta); color: var(--text2); margin-bottom: 5px; font-weight: 500; }
.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 10px; border: 0.5px solid var(--border-mid); border-radius: var(--radius); font-size: var(--t-body); color: var(--text); background: var(--surface); outline: none; font-family: inherit; }
.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--green-mid); box-shadow: 0 0 0 3px rgba(29,158,117,0.1); }
.form-textarea { min-height: 70px; resize: vertical; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-hint { font-size: var(--t-small); color: var(--text3); margin-top: 3px; }
.form-error { font-size: var(--t-small); color: var(--red); margin-top: 3px; display: none; }
.form-error.show { display: block; }

/* ---- MANAGEMENT SUMMARY ---- */
.summary-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 16px; }
.summary-card { background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
.summary-card-title { font-size: var(--t-small); color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.summary-card-value { font-size: 32px; font-weight: 500; color: var(--text); line-height: 1; }
.summary-card-sub { font-size: var(--t-small); color: var(--text3); margin-top: 3px; }
.utilization-bar { height: 5px; background: var(--surface2); border-radius: 3px; overflow: hidden; margin-top: 6px; }
.utilization-fill { height: 100%; border-radius: 3px; }

/* ---- CHANGE LOG ---- */
.log-entry { padding: 10px 0; border-bottom: 0.5px solid var(--border); font-size: var(--t-meta); }
.log-entry:last-child { border-bottom: none; }
.log-meta { display: flex; justify-content: space-between; color: var(--text3); margin-bottom: 3px; font-size: var(--t-small); }
.log-change { color: var(--text); }
.log-reason { color: var(--text2); font-style: italic; margin-top: 2px; }

/* ---- SCROLLBAR ---- */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 3px; }

/* ---- TOAST ---- */
.toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
.toast { background: var(--text); color: white; padding: 10px 16px; border-radius: var(--radius); font-size: var(--t-body); display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); animation: slideIn 0.2s ease; }
.toast.success { background: var(--green); }
.toast.error { background: var(--red); }
.toast-close { background: none; border: none; color: white; opacity: 0.7; cursor: pointer; padding: 0; display: flex; align-items: center; font-size: var(--t-head); }
.toast-close:hover { opacity: 1; }
.toast-action { background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.35); color: white; font: inherit; font-size: var(--t-meta); font-weight: 500; padding: 3px 10px; border-radius: 5px; cursor: pointer; white-space: nowrap; }
.toast-action:hover { background: rgba(255,255,255,0.3); }
@keyframes slideIn { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

/* ---- LOADING ---- */
/* Translucent, not opaque. It used to be a solid fill, so every load blanked
   the whole app and "loading" was indistinguishable from "it crashed".
   Keeping the page visible underneath makes a slow connection read as the app
   working rather than the app disappearing. Still covers input, so a
   half-loaded screen can't be clicked. */
.loading-overlay { position: fixed; inset: 0; background: color-mix(in srgb, var(--surface) 72%, transparent); backdrop-filter: blur(1.5px); display: flex; align-items: center; justify-content: center; z-index: 300; }
.loading-overlay.hidden { display: none; }
.spinner { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--green); border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.btn-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.4); border-top-color: currentColor; border-radius: 50%; animation: spin 0.6s linear infinite; vertical-align: -2px; margin-right: 5px; }
.btn:disabled, .btn.is-loading { opacity: 0.75; cursor: default; pointer-events: none; }

/* ============================================================
   RESPONSIVE — one set of rules for the whole app
   ============================================================
   Previously this section was two lines, so every page invented its own
   behaviour and none of them agreed. The rules below are deliberately
   few and applied everywhere:

     1. A wide table SCROLLS, never crushes. Squeezing columns until
        "Recorded by" clipped mid-word was the single worst offender.
     2. The side panel OVERLAYS, never pushes. Opening a detail panel
        must not reflow the page behind it.
     3. Grids step down at fixed widths: 4 -> 2 -> 1.
     4. Nothing wraps mid-phrase. Titles get their own line rather than
        sharing a flex row with five other things.

   Breakpoints: 1200 (panel overlays / cards stack), 900 (tablet),
   640 (phone).
   ============================================================ */

/* ---- 1. TABLES SCROLL ---- */
/* The wrapper owns the scrolling; the table keeps a readable minimum so
   columns stop compressing before they become unreadable. */
.table-wrap { width: 100%; overflow-x: auto; border-radius: var(--radius-lg); }
/* overflow:hidden on the table (it exists to clip the rounded corners) sits
   between the sticky cells and the scrollport, which silently disables
   position:sticky. The wrapper owns the rounding now, so the table lets
   its cells stick. */
.table-wrap .data-table { min-width: 860px; border-radius: 0; overflow: visible; }

/* Pinned identity columns: while scrolling right you must never lose
   track of WHOSE row you are reading. --pin1 is the first column's width,
   set per table because a date column and a name column differ. */
/* border-collapse:collapse makes cells unpositionable, so position:sticky
   silently does nothing and the cell renders outside the scroll container.
   Pinned tables therefore need `separate`; border-spacing:0 keeps them
   looking identical to the collapsed ones. */
.data-table.pin-two { --pin1: 92px; border-collapse: separate; border-spacing: 0; }
.data-table.pin-two th:nth-child(1), .data-table.pin-two td:nth-child(1) {
  position: sticky; left: 0; z-index: 2; width: var(--pin1); min-width: var(--pin1);
}
.data-table.pin-two th:nth-child(2), .data-table.pin-two td:nth-child(2) {
  position: sticky; left: var(--pin1); z-index: 2;
}
/* Sticky cells need their own opaque background or rows show through. */
.data-table.pin-two th:nth-child(-n+2) { background: var(--surface2); }
.data-table.pin-two td:nth-child(-n+2) { background: var(--surface); }
.data-table.pin-two tr:hover td:nth-child(-n+2) { background: var(--surface2); }
.data-table.pin-two td:nth-child(2)::after,
.data-table.pin-two th:nth-child(2)::after {
  content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 1px; background: var(--border);
}

/* ---- 2. SIDE PANEL OVERLAYS ---- */
/* Always an overlay, at every width. Two behaviours (push on wide, hide
   on narrow) is what made the app feel inconsistent — and pushing meant
   opening a payment reflowed the ledger behind it. */
.side-panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 60;
  width: var(--panel-w); max-width: 100vw;
  border-left: 0.5px solid var(--border);
  box-shadow: -8px 0 24px rgba(0,0,0,0.08);
  transform: translateX(0); transition: transform 0.2s ease;
}
.side-panel.collapsed { width: var(--panel-w); transform: translateX(100%); box-shadow: none; }
.panel-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.18); z-index: 55;
  opacity: 1; transition: opacity 0.2s ease;
}
.panel-backdrop.hidden { display: none !important; }

/* ---- 3. GRIDS STEP DOWN ---- */
@media (max-width: 1200px) { .stats-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px)  { .stats-row { grid-template-columns: 1fr; } }

/* ---- 4. CARDS STACK INSTEAD OF CRUSHING ---- */
/* The class card's header is one flex row of title + time + meta +
   buttons. Below this width the title takes its own line and the actions
   drop to a row of equal-width buttons, so nothing wraps mid-word. */
@media (max-width: 1200px) {
  .class-header { flex-direction: column; align-items: stretch; gap: 10px; }
  .class-right { width: 100%; }
  /* Only labelled buttons share the width. An icon-only button stretched
     to 300px is exactly the "buttons are huge" problem this section is
     meant to fix. */
  .class-right .btn:not(.btn-icon) { flex: 1; justify-content: center; }
  .class-right .btn.btn-icon { flex: 0 0 auto; }
  .class-meta { flex-wrap: wrap; gap: 8px 12px; }
}

/* ---- 5. TOOLBARS ---- */
/* Search grows to fill; everything else keeps its size and wraps as whole
   controls rather than squeezing. */
.table-toolbar > .form-input[type="search"], .table-toolbar > .search-wrap { flex: 1 1 200px; min-width: 160px; }
@media (max-width: 900px) {
  .table-toolbar { gap: 8px; }
  .table-toolbar .form-select, .table-toolbar .form-input { flex: 1 1 140px; }
}

/* ---- 6. SIDEBAR ---- */
@media (max-width: 900px) {
  .sidebar { width: 64px; }
  .sidebar-logo { display: none; }
  .nav-block { padding: 4px 0; }
  /* The nav labels are bare text nodes rather than elements, so there is
     nothing to hide directly. Collapsing the font size removes them and
     the icon restores its own — otherwise the labels merely get clipped,
     which looks broken rather than deliberate. */
  .nav-item { justify-content: center; padding-left: 0; padding-right: 0; gap: 0; font-size: 0; }
  .nav-item i { font-size: 18px; }
  /* These carry their own font-size, so collapsing the parent's does
     nothing — they have to be hidden outright. */
  .user-name, .user-role { display: none; }
  .sidebar-user { padding: 10px 8px; text-align: center; }
  .logout-btn { font-size: 0; gap: 0; justify-content: center; width: 100%; }
  .logout-btn i { font-size: 16px !important; }
}

/* ---- 7. ICON-BUTTON TOOLTIPS ---- */
/* Icon-only buttons are unlabelled until hovered. One shared tooltip node
   lives at the end of <body> and is positioned by script (see initTooltips
   in utils.js), rather than a ::after on each element. A pseudo-element is
   clipped by any ancestor with overflow:hidden and cannot measure itself
   against the viewport, which is why this file used to carry a growing list
   of per-context exceptions (.tip-left, .class-right, .modal-footer,
   .action-grid even children). Measuring once, in one place, replaces all of
   them and works inside cards, tables and modals alike. */
[data-tip] { position: relative; }
.tipbox {
  position: fixed; z-index: 200; pointer-events: none;
  background: var(--text); color: #fff; font-size: var(--t-small); line-height: 1.3;
  padding: 5px 8px; border-radius: 6px; max-width: 280px;
  opacity: 0; transition: opacity 90ms ease;
}
.tipbox.on { opacity: 1; }
.tipbox::after {
  content: ''; position: absolute; border: 5px solid transparent;
}
/* The arrow is centred on the bubble by default. When the bubble has been
   clamped away from the viewport edge the script overrides --tip-arrow so the
   arrow keeps pointing at its anchor instead of drifting with the bubble. */
.tipbox[data-side="top"]::after,
.tipbox[data-side="bottom"]::after { left: var(--tip-arrow, 50%); margin-left: -5px; }
.tipbox[data-side="top"]::after    { top: 100%;  border-top-color: var(--text); }
.tipbox[data-side="bottom"]::after { bottom: 100%; border-bottom-color: var(--text); }
.tipbox[data-side="left"]::after   { left: 100%; top: 50%; margin-top: -5px; border-left-color: var(--text); }
.tipbox[data-side="right"]::after  { right: 100%; top: 50%; margin-top: -5px; border-right-color: var(--text); }

/* The card clips its own rounded corners, which also clipped any tooltip
   rising above a button inside it. The corners are preserved by rounding
   the header instead. */
.class-card { overflow: visible; }
.class-card .class-header { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.class-card .student-list { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }

/* ---- 8. TOPBAR ---- */
/* The topbar packs a title, a week badge, Day/Week and four date controls
   into one row. It has to shed things in priority order rather than wrap
   "Week 2 · Term 1" down three lines. */
.topbar { gap: 12px; flex-wrap: wrap; }
.topbar-right { flex-wrap: wrap; justify-content: flex-end; }
.date-nav { flex-shrink: 0; }
#dashboard-week-badge { white-space: nowrap; }
.date-label { white-space: nowrap; }
@media (max-width: 640px) {
  .date-label { min-width: 0; }
  .page-title { font-size: var(--t-head); }
}

/* ---- 9. CALCULATED FIELDS ---- */
/* Every payment modal now reads Gross -> Discount -> Net -> Amount paid.
   Gross and Net are derived, so they look like fields but are plainly not
   typeable — same shape in Purchase Credit, Materials, Registration and
   Multiple payment, so staff learn one pattern. */
/* Reads as TEXT, not as a field. Giving computed values the same box, border
   and background as the inputs beside them made the purchase modal look like
   it had twelve things to fill in when it has about five — which is most of
   why it felt long and confusing. Only what you can change looks changeable. */
.calc-field {
  padding: 7px 0; font-size: var(--t-body); font-weight: 500;
  font-variant-numeric: tabular-nums; color: var(--text);
}

/* ---- 10. SEARCHABLE PICKER ---- */
.picker-wrap { position: relative; }
.picker-results {
  position: absolute; top: calc(100% + 2px); left: 0; right: 0; z-index: 20;
  background: var(--surface); border: 0.5px solid var(--border-mid);
  border-radius: var(--radius); box-shadow: 0 4px 14px rgba(0,0,0,0.08);
  max-height: 220px; overflow-y: auto;
}
.picker-option:last-child { border-bottom: none !important; }
.picker-option:hover { background: var(--surface2); }

/* ---- 11. MODAL FOOTER BUTTONS ---- */
/* A three-button confirm ("Cancel / Reserve seat instead / Purchase Credit")
   did not fit a 400px modal, so the labels wrapped and those buttons became
   two lines tall — visibly thicker than the ones on the modal behind them.
   Buttons keep one line; the dialog widens instead. */
.modal-footer .btn { white-space: nowrap; flex-shrink: 0; }
/* No wrapping: a footer that breaks onto two rows (two buttons, then one)
   looks like a mistake. The dialog widens to fit its buttons instead, and
   only when it actually has three — a plain yes/no confirm stays 400px. */
.modal-footer { flex-wrap: nowrap; }
#confirm-modal .modal { width: 400px; max-width: 92vw; }
#confirm-modal .modal.has-secondary { width: 560px; }
@media (max-width: 620px) {
  /* Too narrow for three side by side: stack them full-width, which reads
     as deliberate rather than as an accidental break. */
  #confirm-modal .modal.has-secondary .modal-footer { flex-wrap: wrap; }
  #confirm-modal .modal.has-secondary .modal-footer .btn { flex: 1 1 100%; justify-content: center; }
}

/* ---- 12. ONE BUTTON SCALE ---- */
/* Buttons were sized ad hoc with inline font-size: var(--t-small) in some places and
   not others, which is why button rows looked ragged. */
.btn.btn-sm { padding: 5px 9px; font-size: var(--t-small); }
.btn.btn-sm i { font-size: 13px; }
.btn.btn-icon { padding: 6px 8px; }
.btn.btn-icon i { font-size: 15px; }

.enroll-option { background: var(--surface2); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 12px 14px; cursor: pointer; transition: border-color 0.15s; }
.enroll-option:hover { border-color: var(--green-mid); background: var(--green-light); }
.enroll-option-selected { border-color: var(--green-mid) !important; background: var(--green-light) !important; }
.enroll-option-full { opacity: 0.5; cursor: not-allowed; }
.enroll-option-full:hover { border-color: var(--border); background: var(--surface2); }

/* ---- 13. PRINT ---- */
/* Printing produced exactly one clipped page, and the reason is structural
   rather than cosmetic: `.app` is `height:100vh; overflow:hidden`, `.main` is
   `overflow:hidden` and `.content-area` is `overflow-y:auto`. A browser prints
   the visible box of a scroll container and stops — everything below the fold
   was never in the print tree at all. So the first job here is to undo the
   app-shell layout and let the document flow to its natural height; the rest
   is deciding what a paper copy should contain.

   Note that print still only shows the rows currently loaded into the table.
   Pages that paginate re-render in full on `beforeprint` (see the
   `beforeprint` handlers in the page modules). For the complete data set,
   Export Excel is the reliable path. */
@media print {
  /* Landscape: the wide tables declare `min-width:860px`, which does not fit
     A4 portrait at any sane font size. */
  @page { size: A4 landscape; margin: 11mm 10mm 13mm; }

  /* 1 — unwind the app shell so content flows onto as many pages as it needs */
  html, body { height: auto !important; overflow: visible !important; background: #fff !important; }
  .app, .main { display: block !important; height: auto !important; overflow: visible !important; }
  .content-area { overflow: visible !important; height: auto !important; padding: 0 !important; flex: none !important; }
  .page-view.active { display: block !important; }

  /* 2 — chrome, controls and anything transient */
  .sidebar, .topbar, .side-panel, .modal-overlay, .toast, .toast-container,
  .no-print, .table-toolbar, .search-wrap, .pagination, .load-more,
  #payments-pending-section, .panel-close, .fab { display: none !important; }
  /* Every button in the app is an action, and actions do not print. The cells
     that hold row actions stay — emptying them keeps the header and body
     columns lined up, where removing them would not. */
  button, .btn { display: none !important; }

  /* 3 — tables: no horizontal scroller, repeat the header on every page,
     never split a row across a page break */
  .table-wrap { overflow: visible !important; width: auto !important; }
  .table-wrap .data-table { min-width: 0 !important; width: 100% !important; }
  .data-table { font-size: 9.5px !important; border: 0.5px solid #999 !important; page-break-inside: auto; }
  .data-table thead { display: table-header-group; }
  .data-table tfoot { display: table-footer-group; }
  .data-table tr { page-break-inside: avoid; break-inside: avoid; }
  .data-table th { padding: 6px 7px !important; font-size: 8.5px !important; color: #000 !important; background: #eee !important; border-bottom: 0.5px solid #999 !important; }
  .data-table td { padding: 5px 7px !important; border-bottom: 0.5px solid #ccc !important; color: #000 !important; }
  .data-table tr:hover td { background: transparent !important; }

  /* 4 — colour. Pills and status colours carry meaning ("this needs you"),
     so they must survive the printer's default of dropping backgrounds. */
  * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
  .stats-row { grid-template-columns: repeat(4, 1fr) !important; page-break-inside: avoid; margin-bottom: 10px !important; }
  .stat-card, .class-card, .data-table { box-shadow: none !important; }

  /* 5 — a paper copy has to say what it is; on screen the page title lives in
     the topbar, which is the first thing hidden above. #print-header is filled
     in by utils.js on beforeprint. */
  .print-only { display: block !important; }
  #print-header {
    display: block !important;
    margin: 0 0 10px; padding: 0 0 7px;
    border-bottom: 1px solid #333;
    font-size: 11px; color: #000;
  }
  #print-header .print-title { font-size: 15px; font-weight: 600; }
  #print-header .print-meta { color: #444; margin-top: 3px; }

  a { text-decoration: none !important; color: #000 !important; }
}

/* Hidden on screen; only @media print reveals it. */
.print-only, #print-header { display: none; }

/* Trial children on a class card. Amber, like the makeup tag, because both
   mean "this person is here today but is not part of the normal roster" —
   and unlike a makeup, a trial is not marked on the register at all. */
.student-chip.trial { background: var(--amber-light); border-color: rgba(239,159,39,0.3); }
.trial-tag { font-size: var(--t-label); background: var(--amber); color: #fff; padding: 1px 5px; border-radius: 3px; font-weight: 500; }

/* A control that is deliberately unavailable, as opposed to merely disabled.
   `.btn:disabled` sets pointer-events:none, which also suppresses the
   [data-tip] tooltip — leaving a dead button with no way to find out why it
   is dead. This keeps hover alive so the tooltip can explain itself. */
.btn-inert { opacity: 0.5; cursor: not-allowed; }
.btn-inert:hover { background: var(--surface); border-color: var(--border); }

/* ============================================================
   FILTER / SELECT DROPDOWN
   One component replacing the look of all 80 native <select>s.
   The real <select> stays in the DOM, hidden, so every existing
   id, .value read and change handler keeps working untouched —
   this is a skin over the control, not a replacement for it.

   Three dresses, same machinery:
     .hf            filter, in a table toolbar (grey key + value)
     .hf.hf-form    form field inside a modal (full width, no key)
     .hf-toggle     a filter with exactly two states, so a menu
                    would be sillier than a switch
   ============================================================ */
.hf { position: relative; display: inline-flex; }
.hf-btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  background: var(--surface); border: 1px solid var(--border-mid);
  border-radius: var(--radius); padding: var(--sp-2) 11px;
  font-size: var(--t-body); font-family: inherit; color: var(--text);
  box-shadow: 0 1px 2px rgba(0,0,0,0.03); cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
.hf-btn:hover { border-color: var(--text3); }
.hf-btn:focus-visible { outline: none; border-color: var(--green-mid); box-shadow: 0 0 0 3px rgba(29,158,117,0.14); }
.hf-k { color: var(--text3); font-size: var(--t-meta); }
.hf-v { font-weight: 500; }
.hf-chev {
  width: 8px; height: 8px; flex: none; margin-left: 2px;
  border-right: 1.7px solid var(--text3); border-bottom: 1.7px solid var(--text3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-2) var(--ease);
}
.hf-btn[aria-expanded="true"] .hf-chev { transform: rotate(225deg) translate(-2px, -2px); }
.hf-cnt {
  background: var(--green); color: #fff; font-size: var(--t-label); font-weight: 600;
  border-radius: 20px; padding: 1px 6px; font-variant-numeric: tabular-nums;
}
/* A filter that is doing something must look different from one that is not —
   otherwise you leave one on and misread the table. */
.hf-btn.on { border-color: var(--green); background: var(--green-light); color: var(--green); }
.hf-btn.on .hf-k { color: var(--green); opacity: 0.75; }
.hf-btn.on .hf-chev { border-color: var(--green); }
.hf-btn:disabled { background: var(--surface2); color: var(--text3); box-shadow: none; cursor: default; }
.hf-btn:disabled .hf-chev { opacity: 0.4; }
.hf-btn.err { border-color: var(--red); }

.hf-panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
  background: var(--surface); border: 1px solid var(--border-mid);
  border-radius: var(--radius); min-width: 100%; padding: 5px;
  box-shadow: 0 10px 28px rgba(0,0,0,0.14), 0 2px 6px rgba(0,0,0,0.06);
  animation: hfIn var(--dur-1) var(--ease);
}
/* Flip at the right edge and inside the side panel, the same way the
   existing [data-tip] tooltips already do. */
.hf-panel.flip { left: auto; right: 0; }
@keyframes hfIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

.hf-opt {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 9px; border: 0; background: none; border-radius: 6px;
  font-size: var(--t-body); font-family: inherit; color: var(--text);
  cursor: pointer; text-align: left;
  transition: background var(--dur-1) var(--ease);
}
.hf-opt:hover, .hf-opt.cursor { background: var(--surface2); }
.hf-opt .tick { width: 13px; flex: none; color: var(--green); font-size: var(--t-small); }
.hf-opt .tail { margin-left: auto; font-size: var(--t-small); color: var(--text3); font-variant-numeric: tabular-nums; }
.hf-opt.sel { background: var(--green-light); color: var(--green); font-weight: 500; }
/* An option that would return nothing should say so before you click it. */
.hf-opt.zero { color: var(--text3); }
.hf-opt.zero .tail { opacity: 0.6; }
.hf-opt:focus-visible { outline: none; background: var(--surface2); box-shadow: inset 0 0 0 2px rgba(29,158,117,0.3); }

/* Form dress: label sits above, control fills the row. */
.hf.hf-form { display: flex; width: 100%; }
.hf.hf-form .hf-btn { width: 100%; justify-content: space-between; }
.hf.hf-form .hf-chev { margin-left: auto; }
.hf-placeholder { color: var(--text3); font-weight: 400; }

/* Two-state filter. A dropdown to choose between on and off is worse
   than the buttons it replaced. */
.hf-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--border-mid);
  border-radius: var(--radius); padding: var(--sp-2) 11px;
  font-size: var(--t-body); font-family: inherit; color: var(--text);
  box-shadow: 0 1px 2px rgba(0,0,0,0.03); cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.hf-toggle:hover { border-color: var(--text3); }
.hf-toggle:focus-visible { outline: none; border-color: var(--green-mid); box-shadow: 0 0 0 3px rgba(29,158,117,0.14); }
.hf-toggle.on { border-color: var(--red); background: var(--red-light); color: var(--red); }
.hf-toggle .hf-cnt { background: var(--red); }

/* Below this width the controls are too small for a thumb, so the menu
   becomes a sheet at the bottom of the screen with bigger rows. Phones
   only — a tablet has room for the anchored popover, and a full-width
   sheet for a three-option menu reads as a mistake. */
@media (max-width: 600px) {
  .hf-panel {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: var(--sp-2) var(--sp-2) var(--sp-4);
    max-height: 70vh; overflow-y: auto;
    animation: hfSheet var(--dur-2) var(--ease);
  }
  .hf-panel.flip { right: 0; }
  .hf-opt { padding: 12px 11px; font-size: var(--t-head); }
  .hf-opt .tail { font-size: var(--t-meta); }
}
@keyframes hfSheet { from { transform: translateY(100%); } to { transform: none; } }
/* A full-bleed sheet with nothing behind it gives no hint that tapping
   away dismisses it. */
@media (max-width: 600px) {
  .hf-scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.28); z-index: 59;
              animation: hfFade var(--dur-2) var(--ease); }
}
@keyframes hfFade { from { opacity: 0; } to { opacity: 1; } }

/* ---- STUDENT PANEL ACTIONS ----
   Grouped rather than a flat column of ten. Two per row halves the
   panel height, so the contact details and the actions fit together. */
.panel-actions { display: flex; flex-direction: column; }
.action-group-label {
  font-size: var(--t-label); text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--text3); font-weight: 600; margin: var(--sp-3) 0 7px;
}
.action-group-label:first-child { margin-top: 0; }
/* Four across, matching the full page's sd-action-grid, so the same eight
   actions sit in the same places in both views. */
.action-grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-bottom: 5px; }
.action-grid-4 .action-btn { justify-content: center; padding: 6px 4px; font-size: var(--t-small); }

.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
/* The grouped buttons sit in a grid, so the stacked-button margin and the
   centred text of the old flat list both have to go. */
.action-grid .action-btn { margin-top: 0; justify-content: flex-start; }
.action-more {
  display: flex; align-items: center; gap: 6px; background: none; border: 0;
  padding: var(--sp-2) 2px; margin-top: var(--sp-1);
  font-size: var(--t-meta); font-family: inherit; color: var(--text3); cursor: pointer;
}
.action-more:hover { color: var(--text2); }
.action-more i { transition: transform var(--dur-2) var(--ease); }
.action-more.open i { transform: rotate(90deg); }
.action-more-body { margin-top: var(--sp-1); }
.panel-fullpage {
  display: inline-flex; align-items: center; gap: 4px; flex: none;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  font-size: var(--t-meta); font-family: inherit; color: var(--blue);
  transition: opacity var(--dur-1) var(--ease);
}
.panel-fullpage:hover { opacity: 0.72; }

/* A disabled action must LOOK disabled. This class was being set in
   panel.js and student-detail.js but had no rule anywhere, so "Move class"
   for an unenrolled student looked identical to a live button and did
   nothing when clicked. Hover stays alive so the [data-tip] still explains
   why. */
.action-btn.disabled-action {
  color: var(--text3);
  background: var(--surface2);
  border-color: var(--border);
  cursor: not-allowed;
}
.action-btn.disabled-action:hover { background: var(--surface2); color: var(--text3); }
.action-btn.disabled-action i { opacity: 0.5; }



/* ---- TOOLBAR CONTROL HEIGHT ----
   A toolbar row mixes .btn (was 29px), .form-select (34px), .search-wrap
   and the new .hf controls. Three heights on one baseline is exactly the
   kind of thing that reads as unfinished, so they are pinned together.
   Scoped to toolbars so modal fields are untouched. */
.table-toolbar > .btn,
.table-toolbar > .form-select,
.table-toolbar > .search-wrap,
.table-toolbar > .hf > .hf-btn,
.table-toolbar > .hf-toggle {
  min-height: 34px;
}
.table-toolbar > .btn { align-items: center; }

/* ---- MOTION: the last two of the six ----
   Hover, dropdowns and the More group are already done above. These are
   the two remaining jolts: a modal appearing at full size out of nothing,
   and a table snapping from 180 rows to 12 when a filter changes.
   Both are short enough to feel instant and long enough to follow.
   The prefers-reduced-motion block near the top disables all of it. */
.modal-overlay:not(.hidden) { animation: mOverlay var(--dur-1) var(--ease); }
.modal-overlay:not(.hidden) > .modal { animation: mBox var(--dur-2) var(--ease); }
@keyframes mOverlay { from { opacity: 0; } to { opacity: 1; } }
@keyframes mBox { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* The tbody is replaced wholesale on every filter change, so animating the
   new content in is a one-line win — no per-row stagger, which we agreed
   against. */
.data-table tbody { animation: tFade 100ms var(--ease); }
@keyframes tFade { from { opacity: 0.35; } to { opacity: 1; } }

/* Row action icons sat in a right-aligned flex row, so a row with two
   actions put its icons under a row with three and the columns went
   ragged down the table. A fixed slot grid keeps them aligned. */
.row-actions { display: grid; justify-content: end; gap: var(--sp-1); align-items: center; }
.row-actions-3 { grid-template-columns: repeat(3, 30px); }
.row-actions .btn { justify-content: center; padding-left: 0; padding-right: 0; }

/* Change-log CHANGE pills wrapped to two lines ("Security deposit taken"),
   making rows uneven next to single-line pills. */
.pill { white-space: nowrap; }

/* Accounting section headers had no rule and inherited the base size, so
   they were one body-size change away from silently resizing. */
.card-title { font-size: var(--t-head); font-weight: 600; color: var(--text); }

/* The last action column sits close enough to the window edge that its
   tooltip clips on a narrower screen. Same right-pinning the panel's
   two-up grid already uses. */
.row-actions > :last-child[data-tip]:hover::after,
.row-actions > :last-child[data-tip]:focus-visible::after {
  left: auto; right: 0; transform: none;
}
.row-actions > :last-child[data-tip]:hover::before,
.row-actions > :last-child[data-tip]:focus-visible::before {
  left: auto; right: 14px; transform: none;
}
/* aria-disabled rather than the disabled attribute: a truly disabled button
   suppresses hover, which would hide the tooltip explaining why it is off. */
.action-btn[aria-disabled="true"] { pointer-events: auto; }

/* ============================================================
   SHARED PATTERNS — 2026-09-06 consistency round
   One definition each, so pages stop inventing their own.
   ============================================================ */

/* ---- Computed values are not inputs ----
   Net total, Total due, Collecting now and the document-set Net were all
   styled exactly like the fields you type into, so a form with five real
   inputs looked like it had twelve. A figure the app worked out reads as
   text; only things you can change look changeable. */
.readout { font-size: var(--t-body); color: var(--text); padding: 8px 0; }
.readout-strong { font-weight: 500; }

/* A totals block: itemised lines, then the totals, then what is left. */
.sumbox { background: var(--surface2); border-radius: var(--radius); padding: 12px 14px; }
.sumbox .sum-row { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; font-size: var(--t-body); }
.sumbox .sum-row .sum-label { color: var(--text2); }
.sumbox .sum-rule { border-top: 0.5px solid var(--border); margin-top: 4px; padding-top: 8px; }
.sumbox .sum-total { font-weight: 500; }
/* Colour only when there is something to act on — a settled balance is not
   news, an outstanding one is. */
.sumbox .sum-owing { color: var(--red); font-weight: 500; }

/* ---- Muted meta line ----
   The single grey line of supporting facts under a title (room, lesson,
   student count). Replaces per-item icons and separate columns. */
.meta-line { font-size: var(--t-meta); color: var(--text2); }
.meta-line .sep { margin: 0 6px; color: var(--text3); }

/* ---- Date group heading ----
   Lists that repeated the same date on every row now print it once. */
.date-group {
  font-size: var(--t-label); letter-spacing: 0.05em; color: var(--text3);
  text-transform: uppercase; padding: 10px 0 4px;
}
tr.date-group-row > td {
  background: var(--surface2); font-size: var(--t-label); letter-spacing: 0.05em;
  color: var(--text3); text-transform: uppercase; padding: 6px 10px;
}

/* ---- Hover-revealed row actions ----
   Action buttons sit quiet until the row or card is hovered. Kept visible
   on touch, where there is no hover and the buttons would be unreachable,
   and whenever the row holds keyboard focus. */
.hover-actions { opacity: 0; transition: opacity 90ms ease; }
tr:hover .hover-actions,
tr:focus-within .hover-actions,
.card:hover .hover-actions,
.card:focus-within .hover-actions,
.hover-host:hover .hover-actions,
.hover-host:focus-within .hover-actions { opacity: 1; }
@media (hover: none) { .hover-actions { opacity: 1; } }

/* ---- Voided rows ----
   A void is shown on the thing it voided, not as a second row alongside it. */
.is-void { opacity: 0.5; }
.is-void .void-subject { text-decoration: line-through; }

/* ---- School fee tiles (Courses page) ----
   Four fees in one even grid. They used to sit in a 1-then-2-then-1 layout
   with four outlined Edit buttons above the course list — no grid, so the
   eye could not find a column, and set-once values outweighed the list the
   page exists for. */
/* Dashboard: search and (whole-school users only) the branch filter on one
   row, wrapping on a phone. The branch mount is empty for everyone else. */
.dashboard-filters { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.dashboard-filters .search-wrap { flex: 1 1 220px; }
.dashboard-filters #dashboard-branch-wrap:empty { display: none !important; }
.fee-strip { margin-top: 20px; }
/* Heading row: the label, and for a whole-school user the branch picker the
   tiles below belong to. Wraps under the label on a phone. */
.fee-strip-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; margin-bottom: 8px; }
.fee-strip-head .fee-strip-label { margin-bottom: 0; }
.fee-strip-label {
  font-size: var(--t-label); letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text3); margin-bottom: 8px;
}
.fee-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; }
.fee-tile { background: var(--surface2); border-radius: var(--radius); padding: 11px 13px; }
.fee-tile-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.fee-tile-title { font-size: var(--t-meta); color: var(--text2); }
.fee-tile-value { font-size: 19px; font-weight: 500; line-height: 1.2; margin: 6px 0 4px; }
/* The rules that stop a fee being set wrongly — "Held, not income" is what
   keeps a deposit off the revenue line — so they stay visible rather than
   hiding behind a hover. */
.fee-tile-desc { font-size: var(--t-meta); color: var(--text3); line-height: 1.45; }
.fee-editor { display: flex; flex-direction: column; gap: 8px; }

/* Numeric columns right-align: amounts and counts are far easier to compare
   down a shared right edge than a ragged left one. */
.num-cell { text-align: right; }

/* ---- Dashboard class card, two-line layout ----
   Was a single row carrying eight competing elements, with the class name in
   a pill that wrapped on long names and pushed everything beside it out of
   alignment. Line 1 identifies the class, line 2 is one muted meta strip plus
   the actions. */
.class-line1 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.class-line2 { display: flex; align-items: center; gap: 12px; margin-top: 7px; }
.class-line2 .meta-line { flex: 1; min-width: 0; }
/* One line always: the full name is on the tooltip. */
.class-name-pill { min-width: 0; max-width: 340px; overflow: hidden; }
.class-name-pill .course-tag, .class-name-pill .course-plain {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.class-time { font-size: var(--t-body); white-space: nowrap; }
.class-who { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.class-room { font-size: var(--t-meta); color: var(--text2); white-space: nowrap; }
.class-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border-mid); }
.class-actions { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
/* A written note is never hidden behind a hover — that is the whole point of
   writing one. Dark rather than tinted: colour stays for things needing action,
   and a standing note is not an alert. */
.class-actions .btn.has-note { color: var(--text); border-color: var(--border-mid); }
@media (max-width: 900px) {
  .class-line2 { flex-direction: column; align-items: stretch; gap: 8px; }
  .class-who { margin-left: 0; }
}

/* ---- Student detail: lifetime left, selected term right ----
   Was seven full-width cards in a single vertical stack, so on a wide screen
   every card held a narrow list against the left edge and three separate
   chronological lists competed. The split has one rule behind it: the left
   column is lifetime — who this student is and what they have bought — and
   the right column is whatever the term selector is pointing at. */
.sd-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 16px; align-items: start; }
.sd-card { background: var(--surface); border: 0.5px solid var(--border); border-radius: var(--radius-lg); padding: 16px; margin-bottom: 14px; }
.sd-card:last-child { margin-bottom: 0; }
.sd-label {
  font-size: var(--t-small); text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--text3); margin-bottom: 10px;
}
.sd-actions { margin-top: 14px; padding-top: 12px; border-top: 0.5px solid var(--border); display: flex; flex-direction: column; gap: 6px; }
/* Even columns, so the two rows line up instead of ragging by label length. */
.sd-action-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.sd-action-grid .btn { justify-content: center; }
.sd-tabs { display: flex; gap: 16px; border-bottom: 0.5px solid var(--border); margin: -2px 0 0; }
.sd-tab {
  background: none; border: none; cursor: pointer; padding: 6px 2px;
  font-size: var(--t-body); color: var(--text2); font-family: inherit;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.sd-tab.on { color: var(--text); border-bottom-color: var(--text); }
@media (max-width: 1100px) {
  .sd-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Headline figures above a table — count, total, share. Flat grey, no border:
   they are a summary line, not cards competing with the table below. */
.tile-stat { background: var(--surface2); border-radius: var(--radius); padding: 11px 13px; }
.tile-stat-label { font-size: var(--t-label); letter-spacing: 0.05em; color: var(--text3); }
.tile-stat-value { font-size: 19px; font-weight: 500; margin-top: 3px; }
.tile-stat-sub { font-size: var(--t-label); color: var(--text3); margin-top: 2px; }

/* ---- PHONE WIDTH POLISH (2026-09-25) ---- */
/* An empty branch-filter slot (one-branch user) still took a flex gap, so
   the toolbar showed a double space where the filter would be. */
[id$="-branch-wrap"]:empty { display: none !important; }
@media (max-width: 640px) {
  /* The date controls were one unbreakable row wider than a phone, so the
     date itself was cut off at the right edge. */
  .topbar-right { justify-content: flex-start; }
  .date-nav { flex-wrap: wrap; flex-shrink: 1; row-gap: 8px; }
  /* Break after Day/Week so prev / date / next / refresh stay together. */
  .date-nav::after { content: ''; flex-basis: 100%; order: 1; }
  .date-nav .date-btn, .date-nav .date-label { order: 2; }
  /* Toolbar filters fill each row evenly instead of leaving ragged gaps. */
  .table-toolbar > .hf, .table-toolbar > [id$="-branch-wrap"],
  .table-toolbar > .hf-toggle { flex: 1 1 140px; }
  .table-toolbar .hf, .table-toolbar .hf-btn { width: 100%; }
  .table-toolbar .hf-chev { margin-left: auto; }
  /* Course rows: name and details on top, controls on their own line,
     instead of the name squeezed into a column four words wide. */
  .course-row { flex-wrap: wrap; }
  .course-row-actions { margin-left: auto; }
}
