/* ==========================================================================
   Generation Swimming Academy - design system
   Palette sampled from the Generation letterhead logo and the level
   certificates. Mascot accents come from data/curriculum.json.
   ========================================================================== */

:root {
  /* ---- Official Generation Schools CI 2025 ----------------------------
     Primary      #00729A  Pantone 7469C
     Secondary    #95C8DB  Pantone 636C
     Alternative  #00AEEF  Pantone Cyan C
     Alternative  #F47A20  Pantone 158C
     Everything else below is a tint or shade derived from these four.
     The certificate artwork keeps its own navy: it was supplied that way
     by the school and is not ours to restyle.                          */
  --brand:       #00729A;
  --brand-mid:   #0091C2;   /* derived: primary lifted toward the cyan */
  --brand-deep:  #00556F;   /* derived: primary darkened, for footers */
  --cyan:        #00AEEF;
  --sky:         #95C8DB;
  --orange:      #F47A20;   /* used sparingly, for genuine attention only */

  /* Legacy names, kept so existing rules keep working. They predate the
     brand guide; --navy is now the primary teal, not a navy. */
  --navy:        var(--brand);
  --navy-deep:   var(--brand-deep);
  --blue:        var(--brand-mid);
  --foam:        #E9F4FA;

  /* Surface */
  --bg:          #F3FAFD;
  --card:        #FFFFFF;
  --line:        #D8E9F0;
  --ink:         #0E2A36;
  --ink-soft:    #4C6C79;
  --ink-faint:   #8AA6B2;

  /* Status */
  --ok:          #1FA971;
  --ok-soft:     #E4F7EF;
  --warn:        #E9922B;
  --warn-soft:   #FDF1E2;
  --danger:      #E04B4B;
  --danger-soft: #FCEAEA;

  /* Mascot accents */
  --lv1: #4CAF50;  --lv2: #F5A623;  --lv3: #8E5BC4;  --lv4: #26A69A;
  --lv5: #78909C;  --lv6: #2196F3;  --lv7: #546E7A;  --lv8: #263238;

  --radius:    18px;
  --radius-lg: 28px;
  --radius-sm: 12px;

  --shadow:    0 4px 18px rgba(0, 85, 111, .09);
  --shadow-lg: 0 14px 40px rgba(0, 85, 111, .16);

  /* The brand guide specifies Museo Sans, which is commercially licensed and
     cannot be bundled here. It is named first so it is picked up wherever the
     school has a licence installed; Nunito is the fallback and is a close
     match in spirit - geometric, humanist, slightly rounded, and friendly
     enough for a children's product. */
  --font: "Museo Sans", "Museo Sans Rounded", "Nunito", "Segoe UI",
          system-ui, -apple-system, sans-serif;
}

* { box-sizing: border-box; }

/* The hidden attribute is only `display: none` from the user agent sheet, which
   any class selector outranks. The stacked table rules set display on every tr
   and silently un-hid the inline edit forms, so the toggle stopped working.
   Nothing should be able to beat hidden. */
[hidden] { display: none !important; }

/* The canvas, not just the body.
   Only `body` carried a background, and the canvas behind it is painted before
   any element is - so every navigation flashed the browser's default canvas
   first. On a phone set to dark mode that default is near-black, which is why
   moving between screens flickered. `color-scheme` is the other half: without
   it a dark-mode browser is entitled to darken the canvas and the form
   controls of an app that only has a light theme. */
html {
  background: var(--bg);
  color-scheme: light;
}

body {
  margin: 0;
  font-family: var(--font);
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  /* The footer sits at the bottom of the screen on a page too short to fill
     it, instead of stopping wherever the content ran out and leaving a band of
     background beneath it. Visible on the sign-in screen the moment it stopped
     needing to scroll, and on any short page in the installed app.

     dvh, not vh: vh on a phone is the viewport with the browser's bars hidden,
     which is taller than what you can actually see, so a vh-based page is
     short by exactly the height of the address bar. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
main { flex: 1 0 auto; }
footer { flex: none; }

h1, h2, h3, h4 { margin: 0 0 .4em; line-height: 1.15; color: var(--navy); font-weight: 800; }
h1 { font-size: clamp(1.7rem, 3.4vw, 2.5rem); }
h2 { font-size: clamp(1.3rem, 2.4vw, 1.7rem); }
h3 { font-size: 1.1rem; }
p  { line-height: 1.6; color: var(--ink-soft); margin: 0 0 1em; }
a  { color: var(--blue); }

/* ---------- Water backdrop ------------------------------------------------ */

.water-top {
  position: relative;
  background: linear-gradient(160deg, var(--cyan) 0%, var(--blue) 55%, var(--navy) 100%);
  color: #fff;
  overflow: hidden;
}
.water-top h1, .water-top h2 { color: #fff; }
.water-top p  { color: rgba(255, 255, 255, .88); }

/* Soft bubbles drifting in the header. Purely decorative. */
.water-top::before,
.water-top::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  background: rgba(255, 255, 255, .13);
  pointer-events: none;
}
.water-top::before { width: 240px; height: 240px; top: -90px;  right: -60px; }
.water-top::after  { width: 150px; height: 150px; bottom: -70px; left: 8%; }

.wave {
  display: block;
  width: 100%;
  height: 52px;
  margin-bottom: -1px;
}

/* ---------- Layout -------------------------------------------------------- */

/* Wide enough to use a modern laptop screen without the content sitting in a
   narrow column, capped so line lengths stay readable on very wide monitors.
   The clamp lets it breathe on mid-size screens rather than jumping. */
.wrap  { width: 100%; max-width: 1320px; margin: 0 auto; padding: 0 clamp(20px, 4vw, 48px); }
.wrap-narrow { width: 100%; max-width: 640px; margin: 0 auto; padding: 0 20px; }

.stack   > * + * { margin-top: 18px; }
.stack-s > * + * { margin-top: 10px; }

.grid { display: grid; gap: 20px; }
/* Grid children default to min-width:auto, so a single item that refuses to
   shrink - an input carrying its default size=20, a nowrap button - sets a
   floor for the whole column and pushes the page sideways on a phone. */
.grid > * { min-width: 0; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

.row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.spacer { flex: 1; }

section { padding: 44px 0; }

/* ---------- Cards --------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
  box-shadow: var(--shadow);
}
.card-flush { padding: 0; overflow: hidden; }
.card-hero {
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 30px;
}

.card-head {
  padding: 18px 22px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-body { padding: 22px; }

/* ---------- Buttons ------------------------------------------------------- */

/* Android paints a translucent blue rectangle over whatever you touch, and it
   ignores the border radius, so pressing a rounded chip flashed a square block
   behind it. Turned off everywhere and replaced by the deliberate :active
   states below, which follow the shape of the thing you actually pressed.
   Focus rings are untouched: this is the touch flash, not the outline. */
a, button, summary, label, [role="button"], .pick, .slot, .cls, .lrow, .vfile {
  -webkit-tap-highlight-color: transparent;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 13px 26px;
  border: none;
  border-radius: 999px;
  font: inherit;
  font-weight: 800;
  font-size: .98rem;
  cursor: pointer;
  text-decoration: none;
  transition: transform .15s ease, box-shadow .15s ease, background .15s ease;
  white-space: nowrap;
}
/* The lift is for a pointer that can hover. On a touch screen Chrome applies
   :hover on tap and leaves it applied until you tap something else, so a
   button rose 2px as you pressed it and then stayed risen - which is the
   jitter you see rather than a press. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { transform: translateY(-2px); }
}
.btn:active { transform: scale(.97); }
.btn:focus-visible { outline: 3px solid var(--cyan); outline-offset: 3px; }

.btn-primary {
  background: linear-gradient(135deg, var(--cyan), var(--blue));
  color: #fff;
  box-shadow: 0 6px 18px rgba(30, 123, 224, .35);
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { box-shadow: 0 10px 26px rgba(30, 123, 224, .45); }
  .btn-ghost:hover { border-color: var(--cyan); }
}

.btn-ghost {
  background: #fff;
  color: var(--navy);
  border: 2px solid var(--line);
}

.btn-on-dark { background: #fff; color: var(--navy); }
.btn-quiet   { background: var(--foam); color: var(--navy); }
.btn-sm      { padding: 9px 18px; font-size: .87rem; }
/* A full-width button may wrap its label. Without this the nowrap above sets a
   min-content floor that a narrow grid column cannot shrink below, and the
   whole column pushes the page sideways. */
.btn-block   { display: flex; width: 100%; white-space: normal; }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }

/* ---------- Forms --------------------------------------------------------- */

.field { display: block; margin-bottom: 16px; }
.field > span {
  display: block;
  font-weight: 700;
  font-size: .88rem;
  color: var(--navy);
  margin-bottom: 6px;
}
.field .hint { font-weight: 500; color: var(--ink-faint); font-size: .82rem; }

.input, .select, .textarea {
  width: 100%;
  min-width: 0;          /* the default size=20 must not set a layout floor */
  padding: 13px 15px;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--ink);
  background: #fff;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--cyan);
  box-shadow: 0 0 0 4px rgba(41, 171, 226, .16);
}
.textarea { min-height: 110px; resize: vertical; }

/* Dates and times.
   The native control stays. On a phone it is a proper wheel or calendar that
   knows the locale and the keyboard, and nothing hand-built here would be an
   improvement on it - it would only be ours. What is styled is the field it
   sits in and the little calendar button, which by default is a grey glyph in
   a font nothing else on the page uses.

   Height is forced because Safari sizes a date input from its own text and
   ends up shorter than every other field beside it, which is visible the
   moment a date sits next to a text box in the same row. */
input[type="date"].input,
input[type="time"].input,
input[type="datetime-local"].input {
  min-height: 52px;
  font-variant-numeric: tabular-nums;
  /* iOS otherwise centres the text and leaves the field looking half empty. */
  -webkit-appearance: none;
  appearance: none;
}
@media (pointer: coarse) {
  input[type="date"].input,
  input[type="time"].input { min-height: 54px; }
}

/* The calendar button. WebKit gives us a handle on it; Firefox does not, and
   there its own indicator is already drawn in the field's colour. */
input[type="date"].input::-webkit-calendar-picker-indicator,
input[type="time"].input::-webkit-calendar-picker-indicator,
input[type="datetime-local"].input::-webkit-calendar-picker-indicator {
  /* The brand's own glyph rather than the browser's grey one, in the primary
     teal. A data URI, because a stylesheet cannot reach url_for and an extra
     request for 400 bytes of icon is not worth a round trip. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2300729A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4.5' width='18' height='16' rx='2.5'/%3E%3Cpath d='M3 9.5h18M8 2.5v4M16 2.5v4'/%3E%3C/svg%3E");
  background-size: 18px 18px;
  background-repeat: no-repeat;
  background-position: center;
  /* Sized to the text beside it, not larger. The indicator is an inline item
     inside the field, so a 30px button made the whole control 7px taller than
     every other input in the same row. */
  width: 22px;
  height: 22px;
  padding: 0;
  margin: 0;
  border-radius: 8px;
  cursor: pointer;
  opacity: .75;
  transition: opacity .15s, background-color .15s;
}
input[type="date"].input::-webkit-calendar-picker-indicator:hover,
input[type="time"].input::-webkit-calendar-picker-indicator:hover {
  opacity: 1;
  background-color: var(--foam);
}
/* The field's own focus ring is the one that matters; a second ring on the
   button inside it reads as two separate controls. */
input[type="date"].input::-webkit-calendar-picker-indicator:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 1px;
}
/* Chrome greys the placeholder segments (dd/mm/yyyy) so far down they read as
   disabled rather than as waiting to be filled in. */
input[type="date"].input::-webkit-datetime-edit-text,
input[type="date"].input::-webkit-datetime-edit-month-field,
input[type="date"].input::-webkit-datetime-edit-day-field,
input[type="date"].input::-webkit-datetime-edit-year-field {
  color: var(--ink);
}
input[type="date"].input:invalid::-webkit-datetime-edit { color: var(--ink-faint); }

.check {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 16px;
  background: var(--warn-soft);
  border: 2px solid #F5DDBA;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.check input { margin-top: 3px; width: 20px; height: 20px; accent-color: var(--blue); flex: none; }
.check.is-done { background: var(--ok-soft); border-color: #B6E7D2; }

/* ---------- Pills & badges ------------------------------------------------ */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 13px;
  border-radius: 999px;
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .01em;
}
.pill-ok      { background: var(--ok-soft);     color: #147850; }
.pill-warn    { background: var(--warn-soft);   color: #9A5C10; }
.pill-danger  { background: var(--danger-soft); color: #A82F2F; }
.pill-info    { background: var(--foam);        color: var(--navy); }

.eyebrow {
  text-transform: uppercase;
  letter-spacing: .09em;
  font-size: .74rem;
  font-weight: 800;
  color: var(--cyan);
  margin-bottom: 8px;
}

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

.wheel { display: grid; place-items: center; position: relative; }
.wheel svg { transform: rotate(-90deg); display: block; }
.wheel-track { stroke: var(--foam); }
.wheel-fill  { stroke-linecap: round; transition: stroke-dashoffset .9s cubic-bezier(.22,1,.36,1); }
.wheel-label {
  position: absolute;
  display: grid;
  place-items: center;
  text-align: center;
  line-height: 1.1;
}
.wheel-label b { display: block; font-size: 1.5rem; color: var(--navy); }
.wheel-label small { display: block; font-size: .68rem; color: var(--ink-faint); font-weight: 700; }

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

.level {
  position: relative;
  border-radius: var(--radius);
  padding: 20px;
  background: #fff;
  border: 2px solid var(--line);
  transition: transform .15s ease, box-shadow .15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .level:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
}
.level::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 6px;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--accent, var(--cyan));
}
.level.is-locked { opacity: .55; }
.level.is-current { border-color: var(--cyan); box-shadow: 0 0 0 4px rgba(41,171,226,.14); }

.level-mascot {
  width: 62px; height: 62px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--foam);
  border: 3px solid #fff;
  box-shadow: var(--shadow);
  flex: none;
}

/* ---------- Skill rows ---------------------------------------------------- */

.skill { padding: 12px 0; border-bottom: 1px dashed var(--line); }
.skill:last-child { border-bottom: none; }
.skill-text { font-weight: 600; font-size: .94rem; color: var(--ink); }

/* Wording on the left, score pinned to the right of the first line. Baseline
   rather than centre, so a two-line skill keeps its score level with the words
   it belongs to instead of floating halfway down the row. min-width:0 lets the
   wording column shrink and wrap; without it a long unbroken run would push
   the score off the edge. */
.skill-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.skill-head .skill-text {
  flex: 1;
  min-width: 0;
  /* Two columns: the mark, then the words. A hanging indent, so every line of
     a wrapped skill starts under the first one rather than under the tick. */
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
/* Fixed width so the ticks and targets form a column of their own down the
   card, whatever the emoji happens to measure on a given platform. */
.skill-ic { flex: none; width: 20px; text-align: center; line-height: 1.45; }
.skill-words {
  flex: 1;
  min-width: 0;
  /* Where a skill does need two lines, split them evenly rather than filling
     the first and leaving one word alone on the second. "...out of the pool /
     safely" is what read as crooked. Ignored by anything that does not know
     the property, which simply wraps as before. */
  text-wrap: balance;
}
.skill-head .skill-pct {
  flex: none;
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}
/* A touch smaller on a phone, which is enough to keep most of these on one
   line - "I can walk along the side of the pool" fits, where before it was
   a word away from not fitting. */
@media (max-width: 560px) {
  .skill-text { font-size: .88rem; }
}

.meter { height: 9px; background: var(--foam); border-radius: 999px; overflow: hidden; }
.meter > i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--cyan), var(--blue));
  transition: width .8s cubic-bezier(.22,1,.36,1);
}

/* ---------- Stepper ------------------------------------------------------- */

.steps { display: flex; gap: 6px; margin-bottom: 26px; }
.steps > li {
  flex: 1;
  list-style: none;
  font-size: .74rem;
  font-weight: 800;
  color: var(--ink-faint);
  text-align: center;
  padding-top: 12px;
  border-top: 4px solid var(--line);
  transition: color .2s, border-color .2s;
}
.steps > li.is-done    { color: var(--ok);   border-color: var(--ok); }
.steps > li.is-current { color: var(--blue); border-color: var(--blue); }

/* ---------- Tables -------------------------------------------------------- */

.table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.table th {
  text-align: left;
  padding: 12px 14px;
  font-size: .76rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-faint);
  border-bottom: 2px solid var(--line);
  white-space: nowrap;
}
.table td { padding: 14px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.table tr:last-child td { border-bottom: none; }

/* A wide table scrolls sideways inside this, which is invisible on a phone:
   the card that holds it has overflow:hidden, so the columns past the fold
   just look absent. These are Lea Verou's scrolling shadows - the two white
   panels are anchored to the content (background-attachment:local) and slide
   away at each end, uncovering a shadow that says "there is more this way".
   No JS and no markup change, so every existing .table-wrap gets it. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background:
    linear-gradient(to right, var(--card) 30%, rgba(255, 255, 255, 0))
      left center / 28px 100% no-repeat,
    linear-gradient(to left, var(--card) 30%, rgba(255, 255, 255, 0))
      right center / 28px 100% no-repeat,
    radial-gradient(farthest-side at 0 50%, rgba(0, 85, 111, .16), transparent)
      left center / 12px 100% no-repeat,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 85, 111, .16), transparent)
      right center / 12px 100% no-repeat;
  background-attachment: local, local, scroll, scroll;
}

/* ---------- Avatars ------------------------------------------------------- */

.avatar {
  width: 44px; height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, var(--cyan), var(--blue));
  flex: none;
  font-size: .95rem;
}
.avatar-lg { width: 68px; height: 68px; font-size: 1.4rem; }

/* ---------- Mobile shell (instructor / parent PWA) ------------------------ */

.phone {
  max-width: 430px;
  margin: 0 auto;
  min-height: 100vh;
  background: var(--bg);
  box-shadow: 0 0 60px rgba(22, 53, 127, .12);
  padding-bottom: 86px;
  position: relative;
}

.tabbar {
  position: fixed;
  bottom: 0; left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 430px;
  display: flex;
  background: #fff;
  border-top: 1px solid var(--line);
  padding: 8px 4px calc(8px + env(safe-area-inset-bottom));
  z-index: 20;
}
.tabbar a {
  flex: 1;
  text-align: center;
  text-decoration: none;
  color: var(--ink-faint);
  font-size: .68rem;
  font-weight: 800;
  padding: 6px 2px;
  border-radius: var(--radius-sm);
}
.tabbar a.is-active { color: var(--blue); background: var(--foam); }
.tabbar .ic { display: block; font-size: 1.25rem; margin-bottom: 2px; }

/* ---------- Data tables as cards (phones) ---------------------------------
   A seven column table in a 320px viewport is 900px wide. It scrolls, and the
   shadows above say so, but an office admin still had to swipe three screens
   sideways to reach Approve. Below 700px each row becomes a card with its
   column headings as labels.

   Only tables with a <thead> are treated this way, because those are the data
   grids. A key/value table - the application detail, an invoice - is already
   two columns and reads fine as it is.

   The labels come from JS in base.html, which copies the header row onto each
   cell. Without JS nothing here applies and the table simply scrolls, which is
   the behaviour this replaces.
   ------------------------------------------------------------------------- */

@media (max-width: 700px) {
  table.is-stacked, table.is-stacked tbody,
  table.is-stacked tr, table.is-stacked tr.is-row td { display: block; width: 100%; }

  /* Hidden from view, still announced to a screen reader in row order. */
  table.is-stacked thead {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  table.is-stacked tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: #fff;
    margin-bottom: 10px;
    padding: 4px 0;
  }
  table.is-stacked tr:last-child { margin-bottom: 0; }

  /* A row the script skipped: an empty state, or the inline class edit form.
     Plain block, so the form inside lays itself out as it does on desktop. */
  table.is-stacked tr:not(.is-row) { border: none; background: none; padding: 0; }
  table.is-stacked tr:not(.is-row) td { display: block; width: 100%; padding: 14px 0; }

  table.is-stacked tr.is-row td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    padding: 7px 14px;
    border: none;
    text-align: left;
  }
  table.is-stacked tr.is-row td::before {
    content: attr(data-label);
    flex: none;
    max-width: 42%;
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  /* The value, wrapped by the script so a cell with several children stays a
     single flex item rather than spreading across the row. */
  table.is-stacked tr.is-row td > .v { text-align: right; min-width: 0; overflow-wrap: break-word; }

  /* A cell under a blank heading is an action cell: buttons, full width, on
     their own line under a rule. */
  table.is-stacked tr.is-row td.is-actions {
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 4px;
    padding-top: 11px;
    border-top: 1px solid var(--line);
  }
  table.is-stacked tr.is-row td.is-actions::before { content: none; }
  table.is-stacked tr.is-row td.is-actions > .v { text-align: left; width: 100%; }
  table.is-stacked tr.is-row td.is-blank { display: none; }

  /* Form controls get the whole width with the heading above them, rather than
     being crushed into the value half of the row. */
  table.is-stacked tr.is-row td.is-controls {
    display: block;
    margin-top: 4px;
    padding-top: 11px;
    border-top: 1px solid var(--line);
  }
  table.is-stacked tr.is-row td.is-controls::before { display: block; margin-bottom: 8px; }
  table.is-stacked tr.is-row td.is-controls > .v { display: block; width: 100%; text-align: left; }
  table.is-stacked tr.is-row td.is-controls form { display: flex; flex-wrap: wrap; gap: 8px; }
  table.is-stacked tr.is-row td.is-controls .input,
  table.is-stacked tr.is-row td.is-controls .select { flex: 1 1 130px; }

  /* Nothing scrolls sideways any more, so the scroll shadows would be a lie. */
  .table-wrap.is-stacked {
    overflow-x: visible;
    background: none;
  }
}

/* ---------- Bottom navigation (phones) ------------------------------------
   The coach view has been a bottom tab bar since it was built, because that is
   what works on pool deck with one hand. Every other role was left with a top
   bar that ran out of width and wrapped. This is that same idea, generalised:
   below 560px each role gets its primary destinations in the thumb zone and
   everything else behind More, and the top bar drops to brand plus bell.
   ------------------------------------------------------------------------- */

.navbar {
  display: none;                 /* phones only; the top bar serves desktop */
  /* The coach shell is a phone column at every width, so it opts in always. */
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 40;
  padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, .88);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-top: 1px solid var(--line);
}
.navbar-inner {
  display: flex;
  max-width: 560px;
  margin: 0 auto;
}

.navbar a, .navbar button {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 52px;
  /* The extra bottom padding is for the active dot, which is positioned into
     it. Without it the dot crowds the label and reads as a descender. */
  padding: 5px 2px 12px;
  border: none;
  background: none;
  font: inherit;
  font-size: .67rem;
  font-weight: 800;
  letter-spacing: .01em;
  color: var(--ink-faint);
  text-decoration: none;
  border-radius: 14px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.navbar svg { width: 23px; height: 23px; stroke-width: 1.9; flex: none; }
.navbar a:active, .navbar button:active { background: var(--foam); }

.navbar .is-active { color: var(--brand); }
/* The dot carries the active state as well as the colour, so it does not rely
   on hue alone. */
.navbar .is-active::after {
  content: "";
  position: absolute;
  bottom: 4px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--brand);
}
.navbar a, .navbar button { position: relative; }

/* Unread count on the bell, mirroring the header badge. */
.navbar .ct {
  position: absolute;
  top: 3px;
  left: 50%;
  margin-left: 4px;
  min-width: 17px; height: 17px;
  padding: 0 4px;
  background: var(--orange);
  color: #fff;
  border-radius: 999px;
  font-size: .6rem;
  font-weight: 900;
  display: grid;
  place-items: center;
}

/* ---------- The More sheet ------------------------------------------------ */

.sheet {
  width: 100%;
  max-width: 560px;
  margin: auto auto 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 -10px 50px rgba(0, 85, 111, .28);
}
.sheet::backdrop { background: rgba(4, 38, 51, .45); }
.sheet[open] { animation: sheet-up .24s cubic-bezier(.22, 1, .36, 1); }
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }

.sheet-body { padding: 10px 16px calc(20px + env(safe-area-inset-bottom)); }
/* Grab handle, so it reads as a sheet that can be dismissed. */
.sheet-body::before {
  content: "";
  display: block;
  width: 40px; height: 4px;
  margin: 0 auto 14px;
  border-radius: 999px;
  background: var(--line);
}
/* Whose sheet this is. Centred and in the brand colour rather than a faint
   grey caption in the corner: it is the only thing on the panel that says
   which account you are signed in as, which matters on a phone that a family
   shares. */
.sheet h4 {
  margin: 2px 0 12px;
  font-size: .82rem;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: .11em;
  font-weight: 800;
  color: var(--brand);
}
.sheet-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.sheet-grid a, .sheet-grid button {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 52px;
  padding: 0 14px;
  /* Tinted rather than white on white. A panel of identical white boxes gives
     the eye nothing to aim at, so each destination carries a wash of its own
     colour and the icon sits on a disc of it. --tint and --key are set per
     item below; anything unlabelled falls back to the house blue. */
  --tint: var(--foam);
  --key: var(--brand);
  border: 1px solid color-mix(in srgb, var(--key) 22%, transparent);
  border-radius: var(--radius-sm);
  background: var(--tint);
  font: inherit;
  font-size: .87rem;
  font-weight: 700;
  color: var(--key);
  text-decoration: none;
  text-align: left;
  cursor: pointer;
}
.sheet-grid a:active, .sheet-grid button:active {
  background: color-mix(in srgb, var(--key) 16%, #fff);
}
/* Opening the sheet focuses its first item, so this ring is on screen every
   time somebody opens More. Left to the browser it was a black rectangle in
   the middle of a coloured panel. */
.sheet-grid a:focus-visible, .sheet-grid button:focus-visible {
  outline: 3px solid var(--cyan);
  outline-offset: 2px;
}
.sheet-grid svg {
  width: 19px;
  height: 19px;
  stroke-width: 1.9;
  flex: none;
  color: var(--key);
}
.sheet-grid .wide { grid-column: 1 / -1; }
/* The density control is a label and a segmented switch, not a destination, so
   it does not take the tinted-box treatment the links do. */
.sheet-density {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 4px 2px 2px;
}
.sheet-density .tiny { color: var(--ink-faint); letter-spacing: .06em;
                       text-transform: uppercase; }

/* One colour per kind of destination, from the palette rather than invented:
   where you are going, what you are looking at, what is yours, and the two
   that leave. Sign out keeps the muted red it had; Close is deliberately the
   plainest thing on the sheet. */
.sheet-grid .t-plan  { --key: var(--brand);     --tint: #E9F4FA; }
.sheet-grid .t-read  { --key: #7A4FBF;          --tint: #F1EBFB; }
.sheet-grid .t-money { --key: #1B7F5C;          --tint: #E6F6EF; }
.sheet-grid .t-you   { --key: var(--brand-mid); --tint: #E4F5FC; }
.sheet-grid .t-admin { --key: #8A5A12;          --tint: #FBF2E3; }
.sheet-grid .danger  { --key: #A82F2F;          --tint: #FCEDED; }
/* Close is the way out of the sheet rather than a destination, so it is the
   plainest thing here - and it carries no icon, which left its label sitting
   at the left edge of a full-width button as though an icon were missing.
   Centred, which is what a lone label on a wide button wants. */
.sheet-grid .quiet {
  --key: var(--ink-soft);
  --tint: #fff;
  justify-content: center;
  text-align: center;
}

.navbar-always { display: block; }
body:has(.navbar-always) { padding-bottom: calc(74px + env(safe-area-inset-bottom)); }

@media (max-width: 560px) {
  .navbar { display: block; }
  /* Clear the bar so nothing sits under it, including a long page's last row.
     Only where there is a bar to clear: signed out there is none, and the
     padding was 74px of empty page under the footer on the sign-in, register
     and install screens. :has means the page asks the question rather than
     each template having to answer it. */
  body:has(.navbar) { padding-bottom: calc(74px + env(safe-area-inset-bottom)); }
  /* These duplicate what the bar now provides. */
  .pagenav, .navdup { display: none !important; }
}

/* ---------- Utilities ----------------------------------------------------- */

/* An amount must never break between the R and the figure, and figures in a
   column should line up, which proportional digits do not do. */
.money   { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* One fact, kept whole. A line of facts separated by middots should break
   between them and never inside one: "Any level" split over two lines reads as
   a mistake, and the reader has to reassemble it. */
.nw      { white-space: nowrap; }
.muted   { color: var(--ink-soft); }
.faint   { color: var(--ink-faint); }
.small   { font-size: .84rem; }
.tiny    { font-size: .75rem; }
.b       { font-weight: 800; }
.center  { text-align: center; }
.mt0 { margin-top: 0; } .mb0 { margin-bottom: 0; }

.note {
  padding: 14px 16px;
  border-left: 4px solid var(--cyan);
  background: var(--foam);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: .88rem;
  color: var(--navy);
}

/* Prototype-only banner so nobody mistakes this for the live product. */
.proto-flag {
  background: #12233F;
  color: #9FD7F0;
  font-size: .76rem;
  font-weight: 700;
  text-align: center;
  padding: 7px 16px;
  letter-spacing: .02em;
}
.proto-flag a { color: #fff; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ---------- Phones ------------------------------------------------------- */

@media (max-width: 560px) {
  /* Tighter cells and breakable values, so a table needs less sideways travel
     and a long email address cannot push a key/value table off its card. */
  .table th, .table td { padding: 10px 9px; }
  /* break-word, not anywhere: `anywhere` is counted when the browser works out
     a column's min-content width, so every cell collapses to its narrowest and
     text shreds one character per line inside a scrolling table. `break-word`
     only breaks a word that genuinely cannot fit, which is all this needs. */
  .table td { overflow-wrap: break-word; }

  /* A row of figures pairs up rather than stacking. At 180px only one column
     fits a phone, so four cards each carrying a single number cost four
     full-width rows and most of a screen - in either density, which is why
     this is a phone rule and not a compact one.

     :has(.stat) matters. Unscoped it caught the week-at-a-glance grid too and
     folded seven days of classes into two narrow columns, where a class name
     wrapped over three lines. A grid of one-line figures takes the squeeze; a
     grid of timetables does not. */
  .grid-4:has(.stat) { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

  /* The generous desktop padding costs a phone a third of its width.
     :not(.card-flush) matters. A flush card carries no padding of its own on
     purpose - its rows and its card-body supply their own - and this rule sits
     later in the file, so without the exclusion it handed every flush card
     17px back and everything inside was inset twice: 31px of margin each side
     of a 390px screen, which is where the amount and the buttons on the
     invoice list were losing their breathing room. */
  .card:not(.card-flush) { padding: 17px; }
  .card-hero { padding: 20px; }
  .card-head { padding: 15px 17px; }
  .card-body { padding: 17px; }
}

/* Going back, on a light page. The coach shell has .coach-back for the dark
   hero; this is its counterpart for anything below the wave. An arrow rather
   than a bare destination name, because "Today" alone reads as a place you are
   going to rather than one you are returning to. */
.backlink {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 38px;
  padding: 0 14px;
  margin-bottom: 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  color: var(--brand);
  font-weight: 800;
  font-size: .84rem;
  text-decoration: none;
}
.backlink:hover { border-color: var(--cyan); background: var(--foam); }
@media (pointer: coarse) { .backlink { min-height: 44px; } }

/* A reversible action that still deserves a second's thought: deactivating an
   account, retiring a class. Tinted rather than solid red, because a loud
   button for something routine and undoable just makes people hesitate. */
.btn-warn-soft {
  background: #FDEDED;
  color: #A82F2F;
  border: 1px solid #F3C9C9;
}
.btn-warn-soft:hover { background: #FADCDC; border-color: #E8AEAE; }

/* A control that only earns its place when there is no JavaScript to update
   the page: the "Update the cost" button under a live filter. Hidden by class
   on <html>, set in the document head, so it never appears and then vanishes.
   Without the script the class is never added and the button is simply there,
   which is the whole point of it. */
.js .nojs { display: none; }

/* ==========================================================================
   Compact density
   A preference, not a breakpoint. Type is set in rem throughout, so a single
   percentage on the root takes a notch off every size at once; the paddings
   are px and are listed here explicitly. On a phone this is the difference
   between three classes on screen and five.

   Deliberately a notch and not a squeeze: the 44px touch floor is untouched,
   because a control nobody can hit reliably is not denser, it is broken.
   ========================================================================== */
html[data-density="compact"] { font-size: 93.75%; }          /* 16px -> 15px */
[data-density="compact"] .card { padding: 15px; }
[data-density="compact"] .card-hero { padding: 20px; }
[data-density="compact"] .card-head { padding: 12px 15px; }
[data-density="compact"] .card-body { padding: 14px 15px; }
[data-density="compact"] .lrow { padding: 10px 13px; }
[data-density="compact"] .skill { padding: 9px 0; }
/* Compact takes a notch off, not the air out of everything. A timetable slot
   carries two lines of text and a capacity bar in about 40px, so the same
   reduction that suits a paragraph leaves it airless - and this is the screen
   somebody scans fastest. It keeps most of its room. */
[data-density="compact"] .slot { padding: 8px 10px; gap: 8px; }
[data-density="compact"] .card-body.stack-s { padding: 12px 13px; }
[data-density="compact"] .stack-s > * + * { margin-top: 8px; }
[data-density="compact"] .stack > * + * { margin-top: 11px; }
[data-density="compact"] .grid { gap: 12px; }
/* Left in place for the compact rule below to build on. */
@media (max-width: 560px) {
  [data-density="compact"] .card:not(.card-flush) { padding: 13px; }
  [data-density="compact"] .card-body { padding: 13px; }
  [data-density="compact"] .card-head { padding: 11px 13px; }
}

/* The footer's list of facts. Separators are drawn between items rather than
   typed between them, so a wrap can never leave a line ending in a lone dot,
   and on a phone they are dropped and each fact takes its own line. */
/* Sized to be read, not to be got out of the way. It was .tiny at 70% white,
   which is legible on a monitor a foot away and not much else. */
.footmeta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  font-size: .82rem;
  font-weight: 600;
  color: rgba(255, 255, 255, .82);
  /* Baseline, not the default stretch. On a touch device the rule further down
     gives the links padding and makes them inline-block, which makes those three
     items taller than the two plain ones - and with stretch every item's text
     starts at the top of its own box, so the phone number and the address sat
     visibly lower than the company name between them. Aligning on the baseline
     is what makes five facts read as one line whether or not the links are
     carrying a tap target. It only applies in the row direction; the phone
     layout below sets align-items itself. */
  align-items: baseline;
}
.footmeta span { white-space: nowrap; }
/* The links read as part of the same line of facts rather than as three
   underlined things among five plain ones. The resting rule under each one was
   doing more harm than good: five items, three of them ruled, made the footer
   look like a row of form fields with two of them missing.

   So the affordance moves to hover and focus, where it is asked for, rather than
   sitting on the page permanently. Nothing here is a destination anybody hunts
   for - it is a phone number, an address and the privacy notice, and a person
   who wants one already knows what they are looking at. */
.footmeta a {
  color: inherit;
  text-decoration: none;
}
.footmeta a:hover { text-decoration: underline; }
/* Keyboard users get the browser outline, which needs somewhere to sit. */
.footmeta a:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
  border-radius: 3px;
}
@media (pointer: coarse) {
  /* A phone number and an address in a footer are things people tap. */
  .footmeta a { display: inline-block; padding: 6px 0; }
}
.footmeta span + span::before {
  content: "\00B7";
  padding: 0 8px;
  opacity: .55;
}
@media (max-width: 620px) {
  .footmeta { flex-direction: column; align-items: center; gap: 3px; }
  .footmeta span + span::before { content: none; }
}

/* ==========================================================================
   Two components that had drifted into several copies
   .stat was defined in five templates, identical but for a tenth of a rem, and
   .pick in two, identical outright. Both are one component; kept per-template
   they were five and two chances to change one and not the others.
   ========================================================================== */
.stat {
  font-size: 2rem;
  font-weight: 900;
  color: var(--brand);
  line-height: 1;
  margin-bottom: 5px;
}

/* A choosable row: a class in the picker, a way to pay. Radio or not, the
   whole row is the target. */
.pick {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 15px;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  background: #fff;
  cursor: pointer;
  transition: border-color .15s, box-shadow .15s;
}
.pick.is-picked { border-color: var(--cyan); box-shadow: 0 0 0 4px rgba(0, 174, 239, .13); }
.pick.is-full { opacity: .55; cursor: not-allowed; }
.pick input { accent-color: var(--brand); width: 18px; height: 18px; flex: none; }
.pick b { display: block; color: var(--brand); font-size: .95rem; }
.pick span.tiny { display: block; }
@media (hover: hover) and (pointer: fine) {
  .pick:hover { border-color: var(--cyan); }
}

/* Arriving at a particular row from somewhere else. Without this, following a
   link to an anchor scrolls the row into view and leaves the reader hunting
   for which of twenty identical rows they were sent to. */
.table tbody tr:target,
.lrow:target {
  background: var(--foam);
  box-shadow: inset 3px 0 0 var(--cyan);
}

/* ==========================================================================
   Installed
   What a browser tab needs and an app does not.

   Scrolling is not the thing that makes something feel like a website - every
   native app scrolls. What does is chrome: a masthead sized to introduce the
   academy to a stranger, and a footer carrying a company registration number
   under your thumb on the way to signing in. A stranger is not who is holding
   an installed app.

   So the footer becomes one quiet line: 134px of it down to 58. With the
   bottom padding no longer reserved for a navigation bar that is not there
   when signed out, a sign-in screen measures 712px against an 844px phone and
   712px against a 720px one - it fits either way, without anything being
   crammed.
   ========================================================================== */
.standalone .water-top h1 { font-size: 1.62rem; }
.standalone footer { margin-top: 22px; padding: 12px 0; }
.standalone .footmeta { font-size: .66rem; opacity: .8; }
/* Back to one line: the stacked version exists so a wrapped separator never
   ends a line, and at this size there is nothing to wrap. */
@media (max-width: 620px) {
  .standalone .footmeta { flex-direction: row; flex-wrap: wrap; gap: 0 2px; }
  .standalone .footmeta span + span::before { content: "\00B7"; padding: 0 6px; }
  /* Contact only, which fits on one line - and a line is what this has to be,
     because putting the separators back means a wrap can leave one stranded at
     the start of the next line. The company name and registration number are a
     website's obligation: they are on the public site, on every invoice and on
     every receipt, which is where they belong. */
  .standalone .footmeta span:nth-child(2),
  .standalone .footmeta span:nth-child(3) { display: none; }
  /* The privacy notice is first and stays visible, so it leads the line and
     nothing is left wearing a separator with nothing in front of it. */
}

/* ==========================================================================
   Welcome
   The introduction shown on a first sign-in. A sheet from the bottom on a
   phone, a centred card on a laptop - the same shape each platform uses for
   everything else here, rather than one modal pretending both are the same
   thing.
   ========================================================================== */
.welcome {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(4, 38, 51, .5);
  animation: welcome-in .2s ease both;
}
.welcome.is-going { opacity: 0; transition: opacity .18s ease; }

.welcome-card {
  width: 100%;
  max-width: 560px;
  max-height: 92vh;
  overflow-y: auto;
  background: #fff;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-lg);
  animation: welcome-up .26s cubic-bezier(.22, 1, .36, 1) both;
  padding-bottom: env(safe-area-inset-bottom);
}

.welcome-hero {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 20px 22px 18px;
  background: linear-gradient(135deg, var(--cyan) 0%, var(--brand-mid) 55%, var(--brand) 100%);
  color: #fff;
}
.welcome-hero img {
  width: 76px;
  height: 76px;
  flex: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  padding: 4px;
}
.welcome-hero h2 { margin: 2px 0 0; color: #fff; font-size: 1.28rem; line-height: 1.2; }

.welcome-body { padding: 18px 22px 4px; }
.welcome-item { display: flex; gap: 12px; align-items: flex-start; padding: 9px 0; }
.welcome-item .wi-ic {
  flex: none;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: var(--foam);
  font-size: 1.15rem;
}
.welcome-item b { display: block; color: var(--brand); font-size: .95rem; }
.welcome-item .small { color: var(--ink-soft); }

.welcome-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 22px 20px;
  border-top: 1px solid var(--line);
  margin-top: 10px;
}
.welcome-again { display: flex; align-items: center; gap: 9px; cursor: pointer; }
.welcome-again input { width: 19px; height: 19px; accent-color: var(--brand); flex: none; }
@media (pointer: coarse) { .welcome-again { min-height: 44px; } }

/* On a narrow phone the row wraps, and a wrapped row is three things left
   aligned at three different widths. Stacked deliberately instead: the tick
   centred over two buttons of equal width, which is the arrangement the eye
   reads as one decision followed by two ways out. */
@media (max-width: 480px) {
  .welcome-foot { flex-direction: column; align-items: stretch; gap: 12px; }
  .welcome-again { justify-content: center; }
  .welcome-foot .row { width: 100%; }
  .welcome-foot .row .btn { flex: 1; }
}

/* On anything with room, it is a card in the middle rather than a sheet. */
@media (min-width: 620px) {
  .welcome { align-items: center; padding: 24px; }
  .welcome-card { border-radius: var(--radius-lg); }
  .welcome-hero { padding: 24px 26px 22px; }
  .welcome-body { padding: 20px 26px 4px; }
  .welcome-foot { padding: 16px 26px 22px; }
}

@keyframes welcome-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes welcome-up { from { transform: translateY(14%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .welcome, .welcome-card { animation: none; }
}

/* ==========================================================================
   Filter chips
   A row of chips, not a select. Where there are half a dozen options and they
   are the whole vocabulary of the screen, a dropdown hides five of them behind
   a tap and saves no space worth having. Used by the notification list and the
   activity report, which is why they live here and not in either template.
   ========================================================================== */
.notetools {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}
.notefilters { display: flex; gap: 7px; flex-wrap: wrap; flex: 1; min-width: 0; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: #fff;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: .8rem;
  font-weight: 800;
  white-space: nowrap;
}
.chip .n {
  font-size: .72rem;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--foam);
  color: var(--brand);
}
.chip.is-on { background: var(--brand); border-color: var(--brand); color: #fff; }
.chip.is-on .n { background: rgba(255, 255, 255, .22); color: #fff; }
@media (pointer: coarse) { .chip { min-height: 40px; } }
/* On a phone the filters are ONE line that scrolls sideways, and the action
   beside them drops onto its own row.
   ---------------------------------------------------------------------------
   What this replaces: .notetools is a flex row and the action button was a flex
   sibling of .notefilters, so the button reserved a column on the right and the
   chips wrapped inside the narrow gutter that was left. Four filters therefore
   arrived as three ragged rows with the right half of the screen empty on two
   of them - cramped and misaligned, and worse the more notification kinds a
   family accumulates, because the chip list only ever grows.

   The fades are the same idiom as .table-wrap above: painted as backgrounds
   with background-attachment local, local, scroll, scroll, so they appear only
   while there is something to scroll to and vanish at each end. No JS and no
   markup change, which is why this reaches all five screens using these
   classes - notifications, the privacy queue, the access log, the activity log
   and the applications filters - rather than only the one it was reported on.

   The colour is --bg rather than --card because these rows sit on the page, not
   in a card. rgba() of that exact colour rather than `transparent`, or the
   gradient interpolates through black and greys the edge.

   A consequence worth knowing: chips no longer have to fit. The old note here
   said four had to fit across a 360px phone or the set stopped reading as one
   row. That constraint is gone, which is the point - but it means an active
   filter can now start off-screen, so notifications.html scrolls it into view. */
@media (max-width: 560px) {
  .chip { padding: 7px 10px; font-size: .76rem; gap: 5px; }
  .chip .n { padding: 1px 5px; font-size: .68rem; }

  /* nowrap is load-bearing, not tidiness. .notetools carries flex-wrap: wrap
     from the base rule, and in a MULTI-LINE flex container align-items:
     stretch sizes an item to its flex LINE rather than to the container - so
     the chip strip took its max-content width, never clipped, never scrolled,
     and pushed the whole document sideways instead. Measured: the strip was
     wider than its own container and the action button sat 92px outside it. */
  .notetools { flex-direction: column; flex-wrap: nowrap; align-items: stretch;
               gap: 10px; }
  .notetools > form { align-self: flex-end; }

  .notefilters {
    flex: none;
    flex-wrap: nowrap;
    gap: 6px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 2px 0;
    background:
      linear-gradient(to right, var(--bg) 30%, rgba(243, 250, 253, 0))
        left center / 24px 100% no-repeat,
      linear-gradient(to left, var(--bg) 30%, rgba(243, 250, 253, 0))
        right center / 24px 100% no-repeat,
      radial-gradient(farthest-side at 0 50%, rgba(0, 85, 111, .14), transparent)
        left center / 10px 100% no-repeat,
      radial-gradient(farthest-side at 100% 50%, rgba(0, 85, 111, .14), transparent)
        right center / 10px 100% no-repeat;
    background-attachment: local, local, scroll, scroll;
  }
  .notefilters::-webkit-scrollbar { display: none; }
  /* Without this a long chip is squeezed to fit rather than scrolling. */
  .notefilters .chip { flex: none; }
}

/* A key/value table stacks on a phone, unlike a data grid.
   ---------------------------------------------------------------------------
   The stacking script in base.html deliberately skips tables with no <thead>,
   because a two column key/value table already reads fine.  That holds right up
   until a value is a single unbreakable token wider than the column: the
   contact block on the privacy notice carries
   POPIAComplaints@inforegulator.org.za at 36 characters, and an email address
   has nowhere to wrap.  A table sizes its columns from min-content, so the
   whole table grew wider than the card it sits in and the text ran out of the
   white box.  Reported in the campus round.

   overflow-wrap stays break-word rather than anywhere, for the reason recorded
   in CLAUDE.md: anywhere is counted when a column is measured and shreds every
   cell to one character per line.  Once each value has the full width it does
   not need it. */
@media (max-width: 560px) {
  .table-kv, .table-kv tbody, .table-kv tr, .table-kv td { display: block; }
  .table-kv tr { padding: 10px 0; }
  .table-kv tr + tr { border-top: 1px solid var(--line); }
  .table-kv td { border: 0; padding: 0; }
  .table-kv td:first-child {
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--ink-faint);
    margin-bottom: 3px;
  }
  .table-kv td:first-child b { font-weight: 800; }
}

/* Every live region fades while it is being replaced. Mild on purpose: at this
   speed a spinner would flash on and off and read as a fault. */
[data-live-region] { transition: opacity .12s; }
[data-live-region].is-busy { opacity: .55; }

/* ==========================================================================
   Rows or cards
   A segmented control, and the compact row it switches to. The row exists
   because a data table stacked into one card per record on a phone, and an
   invoice with six short facts was taking a whole screen.
   ========================================================================== */
.viewtoggle {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--foam);
  border-radius: 999px;
}
.viewtoggle a,
.viewtoggle span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: .8rem;
  font-weight: 800;
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
}
.viewtoggle .is-on {
  background: #fff;
  color: var(--brand);
  box-shadow: 0 1px 4px rgba(0, 85, 111, .16);
}
.viewtoggle svg { width: 15px; height: 15px; flex: none; }
@media (pointer: coarse) {
  .viewtoggle a, .viewtoggle span { min-height: 40px; }
}

/* One record per row. Named areas rather than a column count, so the phone
   layout is a different arrangement of the same cells instead of a second set
   of markup. */
.lrow {
  display: grid;
  gap: 2px 14px;
  align-items: center;
  padding: 13px 18px;
  border-bottom: 1px solid var(--line);
  grid-template-columns: minmax(120px, auto) minmax(0, 1fr) auto auto auto;
  grid-template-areas: "ref for amt state do";
}
.lrow:last-child { border-bottom: none; }
.lrow:hover { background: var(--bg); }
/* Unpaid is the one state that wants finding in a list. A left edge rather
   than a tinted row: the pill already says which it is, and a coloured row
   would fight with the amount for attention. */
.lrow.is-unpaid { box-shadow: inset 3px 0 0 var(--orange); }

.l-ref   { grid-area: ref; font-weight: 800; color: var(--brand); font-size: .88rem; }
.l-for   { grid-area: for; font-size: .86rem; color: var(--ink); min-width: 0;
           overflow-wrap: break-word; }
.l-when  { grid-area: when; }
.l-amt   { grid-area: amt; font-weight: 800; font-size: .95rem; text-align: right; }
.l-state { grid-area: state; }
.l-do    { grid-area: do; }

/* Wide enough for the dates as well, without squeezing anything. */
@media (min-width: 1000px) {
  .lrow {
    grid-template-columns: minmax(120px, auto) minmax(0, 1fr) auto auto auto auto;
    grid-template-areas: "ref for when amt state do";
  }
}

/* Two lines on a phone: what it is and what it costs on the first, the detail
   and the actions under it. Still one record, still compact. */
@media (max-width: 700px) {
  .lrow {
    padding: 12px 14px;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "ref   amt"
      "for   state"
      "when  do";
  }
  .l-state { justify-self: end; }
  .l-do { justify-self: end; margin-top: 4px; }
  .l-when { align-self: center; }
}

.icard.is-unpaid { border-left: 4px solid var(--orange); }
.ibig { font-size: 1.3rem; font-weight: 900; color: var(--brand); line-height: 1.1; }

/* ==========================================================================
   Capacity meter
   How full a class is, drawn left to right: a quarter of the width at 1/4, the
   whole width at 4/4. Two forms, one set of colours:

     .cap     the fill sits behind an existing box, so a timetable slot is its
              own meter and the week reads as a heat map at a glance.
     .capbar  a standalone track with the count beside it, for table rows and
              anywhere there is no box to fill.

   Colour runs along the brand's own scale, cyan through to the primary teal,
   deepening as the class fills. It is a share of a capacity we already know
   rather than a category, so the hue is one family and not a traffic light -
   and the count is printed next to every bar, so nothing here depends on
   reading a colour, seeing a colour, or on the fill being measured by eye.

   --fill and data-cap come from classes.fill(); no template does the
   arithmetic. */
[data-cap] { --cap-1: var(--sky);       --cap-2: var(--sky); }
[data-cap="roomy"]   { --cap-1: #6FD3F2; --cap-2: var(--cyan); }
[data-cap="filling"] { --cap-1: var(--cyan); --cap-2: var(--brand-mid); }
[data-cap="nearly"]  { --cap-1: var(--brand-mid); --cap-2: var(--brand); }
[data-cap="full"]    { --cap-1: var(--brand); --cap-2: var(--brand-deep); }

/* Full is a state worth spotting from across the week, not just the far end of
   a gradient, so it also carries a texture. That texture is the part that does
   not rely on telling two blues apart. */
[data-cap="full"] {
  --cap-texture: repeating-linear-gradient(135deg,
    rgba(255, 255, 255, .18) 0 6px, rgba(255, 255, 255, 0) 6px 14px);
}

.cap { position: relative; overflow: hidden; isolation: isolate; }
.cap::before {
  content: "";
  position: absolute;
  inset: 0;
  width: var(--fill, 0%);
  z-index: -1;
  /* Tinted rather than solid: the fill passes under the class name, and text
     that dims as its class fills up would be a strange thing to have built. */
  background:
    var(--cap-texture, none),
    linear-gradient(90deg,
      color-mix(in srgb, var(--cap-1) 18%, transparent),
      color-mix(in srgb, var(--cap-2) 32%, transparent));
  /* A defined edge, so a half-full box reads as a measurement rather than as a
     gradient that faded out somewhere around the middle. */
  box-shadow: inset -2px 0 0 color-mix(in srgb, var(--cap-2) 55%, transparent);
  animation: cap-grow .75s cubic-bezier(.22, 1, .36, 1);
}
.cap[data-cap="empty"]::before { box-shadow: none; }
.cap[data-cap="full"] { border-color: var(--brand); }

/* Tall rows fill along the bottom edge instead of end to end. A coach's list
   is 60px high and full width, and a tint across all of that stops being a
   measurement and becomes the background colour of the row - two of them side
   by side looked like a rendering fault rather than two busy classes. The bar
   is drawn at full strength precisely because it is thin. */
.cap-strip::before {
  inset: auto 0 0 0;
  height: 5px;
  background:
    var(--cap-texture, none),
    linear-gradient(90deg, var(--cap-1), var(--cap-2));
  box-shadow: none;
  border-radius: 0 3px 3px 0;
}

.capbar { display: flex; align-items: center; gap: 9px; }
.capbar .track {
  flex: 1;
  min-width: 46px;
  max-width: 130px;
  height: 9px;
  border-radius: 999px;
  background: var(--foam);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}
.capbar .track i {
  display: block;
  height: 100%;
  width: var(--fill, 0%);
  border-radius: 999px;
  background:
    var(--cap-texture, none),
    linear-gradient(90deg, var(--cap-1), var(--cap-2));
  animation: cap-grow .75s cubic-bezier(.22, 1, .36, 1);
}
.capbar .n {
  font-weight: 800;
  font-size: .82rem;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.capbar .n.is-full { color: var(--brand-deep); }

/* Grows from nothing on arrival, which is what makes the row read as a level
   rather than as a coloured box. Only on arrival: it is a page load, not a
   loop.

   No fill mode, deliberately. The real width is declared on the element, and
   the animation only overrides it while it is running - so a browser that
   never runs the animation still draws a correct bar, rather than holding the
   opening frame of nothing at all. */
@keyframes cap-grow { from { width: 0; } to { width: var(--fill, 0%); } }

@media (prefers-reduced-motion: reduce) {
  .cap::before, .capbar .track i { animation: none; }
}

/* Pager, shared by any list long enough to need one. */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 0 0 26px;
}

/* Header links that are controls rather than body text.

   Help and Sign out stayed anchors on purpose: they navigate, so middle-click
   and "open in new tab" should work and a screen reader should announce them
   as links, none of which a <button> gives you. What they lacked was looking
   like controls - two words of underlined text against a gradient reads as an
   afterthought, and the target was whatever the text happened to measure.

   So: a ghost pill. A border and a real hit area, no fill, so they sit below
   anything primary instead of competing with it. */
.navlink {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px 13px;
  /* Set above the surrounding .small, which these carry for spacing reasons.
     At .small they read as body text that happens to be in the header. */
  font-size: .875rem;
  font-weight: 800;
  border: 1px solid rgba(255, 255, 255, .34);
  border-radius: 999px;
  color: rgba(255, 255, 255, .92);
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s, border-color .15s;
}
.navlink:hover,
.navlink:focus-visible {
  background: rgba(255, 255, 255, .15);
  border-color: rgba(255, 255, 255, .62);
}
/* Sign out is quieter than Help: always available, never inviting. Someone
   hunting for it still finds it; nobody lands on it by accident. */
.navlink-quiet {
  border-color: rgba(255, 255, 255, .2);
  color: rgba(255, 255, 255, .74);
}
.navlink-quiet:hover,
.navlink-quiet:focus-visible {
  border-color: rgba(255, 255, 255, .42);
  color: rgba(255, 255, 255, .95);
}

/* Touch devices only, so nothing here changes the look on a desktop mouse.
   44px is the smallest target Apple and Google both consider reliable; the
   small buttons and header links sit under it by default. */
@media (pointer: coarse) {
  .btn-sm { min-height: 44px; }
  .navlink { min-height: 44px; padding-left: 15px; padding-right: 15px; }
  /* A pill used as a control, not a label: the publish toggle on the resources
     screen is 28px tall, which is well under a reliable target. */
  button.pill { min-height: 44px; }

  /* A select carrying its own inline padding - the method picker in the
     payments table - lands at 38px, and compact takes another two off it.
     The touch floor is not a thing density gets to negotiate with. */
  select.select { min-height: 44px; }

  /* Whole rows that are links: a timetable slot, a class in the coach's week,
     a lesson on the overview. They are the primary way into those screens and
     were landing at 38px, which is a tap that misses about as often as it hits
     on a wet poolside. */
  .slot, .wslot, .cls, .lesson, .vfile { min-height: 44px; }

  /* A phone number is there to be dialled. At 11px of text it was a 15px
     target inside a table cell. */
  a[href^="tel:"] {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
  }

  /* Chrome on Android raises "Tap to see search results" on a tap anywhere in
     selectable text, so reading a class list kept popping a Google bar over
     the page. Only text that is selectable triggers it, and an opt-out list
     could never cover every label, so the policy is inverted: an installed app
     is not selectable by default, and the things genuinely worth copying opt
     back in below. Desktop is untouched - this is inside pointer: coarse. */
  body {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  /* Figures, form values, and anything a template marks as worth copying. */
  .selectable, .table td, .input, .textarea, .select, input, textarea, select {
    -webkit-user-select: text;
    user-select: text;
    -webkit-touch-callout: default;
  }

  /* Header links read as buttons rather than underlined text, and clear the
     44px target floor that padding alone cannot reach. */
  .water-top a.small,
  .water-top p a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 13px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .16);
    text-decoration: none;
    font-weight: 800;
    /* These are 44px tall inside a paragraph whose line height was set for
       text, so once the row wraps - and eleven of them on an office screen
       always wraps - the second line's pills touch the first line's. An
       atomic inline box does contribute its vertical margins to the line
       box, unlike a plain inline, so this is what separates the rows.
       Horizontal spacing already comes from the middots between them. */
    margin: 4px 0;
  }
  .water-top a.small:active,
  .water-top p a:active { background: rgba(255, 255, 255, .3); }
}
