/* ==========================================================================
   MDV Kids — the back office.
   ==========================================================================

   Tokens come from kids-tokens.css. This is the staff-facing side, so it is
   quieter than the television: the same palette and the same squircles, but
   read at arm's length on a laptop rather than from across a room, and it is
   somewhere people do work rather than somewhere they look.

   No seasons here. The classroom screen dresses for December because a room of
   children notices; a table of display tokens does not need to.
   ========================================================================== */

* { box-sizing: border-box; }

html, body { height: 100%; margin: 0; }

body {
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  color: var(--ink);
  background: var(--sky-bottom);
  -webkit-font-smoothing: antialiased;
  font-size: 15px;
  line-height: 1.5;
}

a { color: var(--accent); }

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

/*
 * THE SIDEBAR IS HUB'S, MEASURED RATHER THAN RECALLED.
 *
 * Ian, 2026-11-09: "fix the sidebar copy the one for the hub and make sure
 * it identical in size, style and layout please".
 *
 * Every number below was read off Hub's own markup — templates/layout/head.php
 * and the `[data-sidebar]` / `.nav-group-*` blocks in its app.css — not chosen
 * here. Hub writes its sidebar as Tailwind utilities, which this app does not
 * have, so the utilities are resolved into their values:
 *
 *     column        w-56        224px   (was 232)
 *     rail          3.25rem      52px   (was 64)
 *     brand band    h-14         56px   (was a 22px flex gap and no band)
 *     band rule     border-b     1px of --hairline
 *     nav padding   p-2           8px   (was 18/14 on the aside)
 *     row           h-9          36px   gap-2.5 = 10px, px-2 = 8px, text-sm
 *     row radius    rounded-lg    8px   (was 10)
 *     row gap       space-y-px    1px   (was 2)
 *     group head    1.75rem      28px   11px/600, .06em, uppercase
 *     group gap     0.375rem      6px   (was 14)
 *     brand mark    h-6 w-6      24px   rounded-md = 6px (was 34px / 11px)
 *
 * WHAT DID NOT COME ACROSS, and why it is not an oversight:
 *   - Hub's command palette. It is a live `/search` endpoint; this app has
 *     none, and a search box that cannot search is worse than no search box.
 *   - Hub's "MyMDV Hub" footer line. See the note on `.bo-who` below.
 * -------------------------------------------------------------------------- */

.bo {
  /* The width lives in a custom property so the rail is one declaration
     rather than a second grid definition. */
  --bo-side: 224px;
  display: grid;
  grid-template-columns: var(--bo-side) minmax(0, 1fr);
  min-height: 100%;
}

/*
 * AND THE ROWS ARE NAMED HERE TOO, WHICH IS NOT COSMETIC.
 *
 * Below 861px `.bo` has exactly two items in flow — `.bo-top` and
 * `.bo-main`; the scrim and the drawer are both `position: fixed` and take
 * no track. The rows were implicit, so both were `auto`, and an `auto` row
 * in a grid with `min-height: 100%` STRETCHES: on a page shorter than the
 * window the leftover was split between them and HALF OF IT WENT INTO THE
 * HEADER BAR.
 *
 * Measured before this line, on the refused-cards page: the 49px bar came
 * out 165px at 375 and 342px at 834 — a third of an iPad's screen of empty
 * grey with the wordmark floating in the middle of it. Estado read 112 and
 * the inscripciones queue 119 on the same width. It is the same shape of
 * fault as the grey column down the left edge: a box nobody drew, made out
 * of space a grid had nowhere else to put.
 *
 * `auto 1fr` gives the bar its content height and hands every spare pixel to
 * the page. Stated only here, because above 861px `.bo-top` is
 * `display: none` and `.bo-main` is the only item in flow — naming two rows
 * there would reserve one for nothing.
 */
@media (max-width: 860px) {
  .bo { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
}

/* --------------------------------------------------------------------------
   EXPAND / MINIMISE.

   Ian, 2026-11-09, pointing at shadcn's default dashboard: "be able to
   expand/minimize depending on screen size".

   Minimised, the sidebar is an ICON RAIL at Hub's 52px: the marks stay, the
   words go. Nothing is re-rendered — `.bo-copy` fades and the fixed inner
   column is simply clipped by the narrowing aside. That is Hub's trick and
   the reason neither sidebar judders: a label inside a column that is itself
   being animated re-wraps on every single frame.

   -------------------------------------------------------------------------
    EVERY RULE BELOW IS BEHIND `min-width: 861px`, AND THAT IS A FIX
   -------------------------------------------------------------------------

   The rail is a DESKTOP idea. Below 861px there is no sidebar column at all:
   the sidebar is an off-canvas drawer and the navigation is the bar at the
   bottom of the screen.

   kids-admin.js chooses the rail from `window.innerWidth < 1100` with no
   lower bound, so a phone gets `is-rail` on the shell — and these rules then
   fade every nav label, the brand words and the account block to opacity 0
   INSIDE THE DRAWER, hide the group headings and hide the Inscripciones
   badge. A drawer of unlabelled icons.

   It is latent today only because the drawer cannot be opened: the hamburger
   went when the bottom bar arrived, and `kids-admin.js` guards that whole
   module on `[data-menu-open]`, which no longer exists. Put a hamburger back
   and the bug is live the same minute. Found by the 2026-11-09 sweep.

   SCOPED IN CSS RATHER THAN GUARDED IN JS, deliberately. The script does not
   re-evaluate on resize — on purpose, because a sidebar that reopens itself
   when a window is dragged wider is a sidebar arguing with the person using
   it — so a width check there would still leave a stale class behind after a
   desktop-to-phone resize or a restore from localStorage. Behind a media
   query the class can be present and simply mean nothing where there is no
   rail to speak of.
   -------------------------------------------------------------------------- */

@media (min-width: 861px) {

.bo.is-rail { --bo-side: 52px; }

/* COPY FADES, IT IS NEVER RE-WRAPPED. `.bo-copy` is on the brand words, every
   nav label and the account block's text — one class, so there is one rule. */
.bo-copy {
  transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1),
              transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.bo.is-rail .bo-copy {
  opacity: 0;
  transform: translateX(-8px);
  pointer-events: none;
}

/*
 * THE GROUP HEADINGS GO, AND THE GROUPS STAND OPEN.
 *
 * At 52px there is no room for a heading and nothing to read if there were.
 * Hiding the summary alone would be a bug, not a style: a shut <details> hides
 * its links too, so a collapsed group in the rail is a section that has simply
 * vanished. kids-admin.js forces every group open on the way in and puts each
 * one back the way it was on the way out — the same dance as Hub's
 * `railGroups()`, and the reason this is script and not CSS is that forcing it
 * with `display` fights the element's own state and leaves it wrong when the
 * sidebar expands again.
 */
.bo.is-rail .bo-group-title { display: none; }
.bo.is-rail .bo-group + .bo-group { margin-top: 1px; }

/*
 * THE ROWS ARE NOT TOUCHED, AND THAT IS THE POINT OF 52.
 *
 * This used to say `justify-content: center; padding-left: 0; padding-right: 0`
 * on the rail's rows, which looks like the obvious thing and is wrong: the row
 * is still 224px wide — only the <aside> narrowed — so centring put the glyph
 * at x=112, eighty pixels past the clip. Measured at 18px of a 52px rail
 * against Hub's 27.
 *
 * Left alone, the arithmetic centres it by itself, which is why Hub picked
 * this width: 8px of nav padding + 8px of row padding + half of a 20px glyph
 * is 26, and half of 52 is 26. The account's face below lands on the same
 * centre the same way — 12px of padding + half of 28.
 *
 * The badge goes, because there is no label left for it to sit after.
 */
.bo.is-rail .bo-badge { display: none; }

/* The brand has nowhere to go at 52px; the expand chevron takes its place.
   The swap itself is with the buttons, below, so each class is declared
   once — see the note on clashcheck in bin/checks/clashcheck.py. */
.bo.is-rail .bo-rail-out { opacity: 1; pointer-events: auto; }
.bo.is-rail .bo-rail-in { opacity: 0; pointer-events: none; }
.bo.is-rail .bo-brand { opacity: 0; pointer-events: none; }

}  /* min-width: 861px — the rail */

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

/*
 * PINNED, NOT SCROLLED.
 *
 * Ian, 2026-11-09: "the log out button the sidebar is scrollable when it
 * should be the same non changing height throughout all the pages."
 *
 * `.bo` is a grid with `min-height: 100%`, so the row GROWS with the page —
 * on a long page the sidebar column became as tall as the document and its
 * foot, which holds the account and the only way to sign out, sat at the
 * bottom of the DOCUMENT rather than the bottom of the window. Every page
 * put it somewhere different, which is exactly what was reported.
 *
 * FIXED, AND NOT STICKY, and the difference is measurable.
 *
 * Sticky was the first attempt and it is what Hub uses, but Hub's shell is a
 * flex row and this one is a grid. A sticky element stops travelling at the
 * bottom of its CONTAINING BLOCK — here `.bo` — and on these pages the
 * document is consistently 22px taller than `.bo` is. Measured on the
 * children's page: document 2137, `.bo` 2115. Nothing visible accounts for
 * it; no element's box extends past the grid. So the last 22px of scroll
 * pulled the sidebar up off the top of the window, and the account block
 * went with it. Measured again after the fix: 0.
 *
 * Fixed has no containing block to run out of. The grid's first column still
 * RESERVES `--bo-side`, so `.bo-main` lands exactly where it did and the rail
 * animation still drives both from one custom property.
 *
 * `svh` rather than `vh`: on iOS `vh` is measured against the chrome-HIDDEN
 * height, so a 100vh column is taller than the window while the address bar
 * is showing and the account block sits just under the fold — the same trap
 * that pushed the kiosk bubbles off screen. `svh` is the small viewport,
 * which is the one that is always visible.
 *
 * Below 861px this is a drawer and sets its own `position: fixed` with
 * `inset: 0 auto 0 0`; see that block.
 */
.bo-side {
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  width: var(--bo-side);
  height: 100svh;
  z-index: 20;
  border-right: 1px solid var(--hairline);
  /* zinc-900, which is Hub's `bg-zinc-900`. It was --surface-2 (zinc-800) —
     Hub's BORDER colour — so the sidebar sat one step lighter than Hub's and
     read as a raised panel rather than a column. */
  background: var(--surface);
  /* The aside clips; the column inside it keeps its width. */
  overflow: hidden;
}

/* A browser without `svh` gets `vh`, which is right everywhere except an iOS
   address bar. Declared BEFORE the svh rule would be the usual shape, but
   this file is one-declaration-per-class, so the fallback is its own rule. */
@supports not (height: 100svh) {
  .bo-side { height: 100vh; }
}

/*
 * The fixed inner column. 224px, `flex: none`, and it never reflows.
 */
.bo-side-col {
  width: 224px;
  flex: none;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

@media (max-width: 860px) {
  .bo-side { border-right: 0; border-bottom: 1px solid var(--hairline); }

  /* The drawer sets its OWN width, so the column fills it rather than
     leaving 66px of bare sidebar beside a 224px column. Nothing is clipped
     on a phone: there is no rail there. */
  .bo-side-col { width: 100%; }
}

/* --------------------------------------------------------------------------
   The brand band — Hub's `h-14 border-b`.

   56px with a rule under it, and everything in it is POSITIONED rather than
   in flow. That is what lets the brand and the expand chevron occupy the same
   spot without either of them moving when the other appears.
   -------------------------------------------------------------------------- */

.bo-band {
  position: relative;
  height: 56px;
  flex: none;
  border-bottom: 1px solid var(--hairline);
}

.bo-brand {
  position: absolute;
  left: 12px;
  top: 12px;
  height: 32px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 4px;
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
  transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1);
}

/*
 * The wordmark, in a square tinted chip — Hub's 24px `rounded-md` app icon.
 *
 * SCOPED TO THE BRAND on purpose. `.bo-mark` is also the phone bar's mark
 * (30px), the sign-in card's (46px) and the PIN screen's, and this app has a
 * standing note that retokenising the base class changes all four. One
 * selector here changes the one that had to change.
 *
 * `color: var(--accent-ink)` reaches the drawing because the file is
 * `fill="currentColor"`. Nothing here needs to know what colour the theme is.
 */
.bo-brand .bo-mark {
  width: 24px;
  height: 24px;
  border-radius: 6px;
}

.bo-mark {
  width: 34px;
  height: 34px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 11px;
  background: var(--accent);
  color: var(--accent-ink);
}
.bo-mark svg { width: 74%; height: auto; display: block; }

/*
 * Hub's pair: 14px/600 zinc-100 over 11px zinc-500, both on a tight line.
 * It was 15px/800 with -0.02em of tracking, which is a heading, not a brand
 * line — and it made this sidebar a size bigger than Hub's at a glance.
 */
.bo-brand-words { min-width: 0; }
.bo-brand-words b {
  display: block;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0;
  color: var(--ink);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/*
 * The subtitle under "MDV Kids" — and ONLY that.
 *
 * It used to be `.bo-brand span`, which also matched the mark's own span, so
 * the logo inherited `--ink-faint` and came out grey on green. Scoped through
 * `.bo-brand-words` now, which is a class on the text block: the mark is left
 * alone whatever order the rules end up in.
 */
.bo-brand-words span {
  display: block;
  font-size: 11px;
  line-height: 1.25;
  font-weight: 400;
  color: var(--ink-faint);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Hub's two band buttons: 32px, 8px radius, zinc-500 going to zinc-100 on a
   zinc-800 ground. */
.bo-rail-toggle {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border: 0; border-radius: 8px;
  background: none; color: var(--ink-faint);
  cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease, opacity 180ms ease;
}
.bo-rail-in  { right: 8px; top: 12px; }
/*
 * The expand chevron sits exactly where the BRAND sits, not beside it, and
 * only one of the two is ever reachable: opacity and pointer-events rather
 * than `display`, so the swap does not reflow the band under the pointer.
 * `scaleX(-1)` is why there is one drawing and not two — the expand button
 * is the mirror image of the collapse one, which is Hub's trick as well.
 */
.bo-rail-out {
  left: 12px;
  top: 12px;
  opacity: 0;
  pointer-events: none;
}
.bo-rail-out svg { transform: scaleX(-1); }
.bo-rail-toggle:hover { background: var(--surface-2); color: var(--ink); }
.bo-rail-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.bo-rail-toggle svg { width: 18px; height: 18px; }
/* On a phone the sidebar is already a drawer with its own close button; two
   ways to put the same thing away is one too many. */
@media (max-width: 860px) { .bo-rail-toggle { display: none; } }

/* --------------------------------------------------------------------------
   The rows — Hub's `p-2` nav, `h-9` rows, `space-y-px`.
   -------------------------------------------------------------------------- */

.bo-nav {
  position: relative;      /* the glide measures offsetTop against this */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px;
}

/*
 *  A sidebar row is 36px, which is Hub's `h-9`.
 *
 *  Stated as a HEIGHT now rather than built out of padding. It was
 *  `padding: 8px 10px` with a 20px line box, which comes to the same 36 —
 *  until a label wraps, and then one row in the list is 56 and the glide
 *  measures a different box from the one you see. Hub sets the height.
 *
 *  Measure this at a DESKTOP width. The first measurement here read 44.5 and
 *  was taken in a headless window defaulting to 800px — under the 860px
 *  breakpoint, so it was the phone drawer's deliberately fatter touch target
 *  being compared against Hub's desktop row. The drawer is still 42px and
 *  should be.
 *
 *  The 8px radius is Hub's `rounded-lg`. This file used to argue for 10,
 *  "this app's own language, shared with the buttons and the fields". Ian
 *  asked for identical, and identical is what the sidebar now is; the
 *  buttons and fields keep their 12px `--squircle-sm`.
 */
.bo-nav a {
  position: relative;
  z-index: 1;              /* above the glide, which is painted behind */
  display: flex;
  align-items: center;
  gap: 10px;
  height: 36px;
  padding: 0 8px;
  font-size: 14px;
  line-height: 20px;
  border-radius: 8px;
  color: var(--ink-soft);
  text-decoration: none;
  font-weight: 400;
  transition: color 140ms ease;
}

/*
 * NO BACKGROUND ON HOVER. The glide paints it — see `.bo-glide`. A row that
 * also lit its own background put two blocks on the same row during the
 * slide, which reads as a flicker rather than a highlight.
 */
.bo-nav a:hover { color: var(--ink); }
.bo-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Hub's active row: violet-500/10 behind violet-200, medium rather than
   semibold. It was a zinc-800 fill with plain ink on it, which is the same
   block Hub uses for HOVER — so the current section and a hovered one looked
   alike. */
.bo-nav a[aria-current='page'] {
  background: rgb(139 92 246 / 0.1);
  color: #ddd6fe;
  font-weight: 500;
}

/* --------------------------------------------------------------------------
   THE GLIDING HIGHLIGHT.

   One absolutely-positioned block per nav that moves to whichever row the
   pointer is over, rather than each row painting its own hover. Transform and
   height only — both compositor properties — so it slides at any refresh
   rate. Copied from Hub's `.sidebar-glide`; the alternative blinks, because
   two rows are briefly both lit as the pointer crosses and nothing connects
   them.
   -------------------------------------------------------------------------- */

.bo-glide {
  position: absolute;
  left: 8px;
  right: 8px;
  top: 0;
  height: 0;
  border-radius: 8px;
  background: var(--surface-2);          /* zinc-800 */
  opacity: 0;
  pointer-events: none;
  transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1),
              height 180ms cubic-bezier(0.16, 1, 0.3, 1),
              opacity 140ms ease;
}
[data-glide][data-hovering] .bo-glide { opacity: 1; }

/* The active row keeps its tint but yields to the glide, so the two never
   stack into a brighter block. */
[data-glide][data-hovering] .bo-nav a[aria-current='page'] { background-color: transparent; }

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

/* --------------------------------------------------------------------------
   The account, at the foot of the column.

   HUB HAS NO SUCH BLOCK — its sidebar ends in one line of 11px grey and the
   account lives behind the avatar in its top bar. This back office has no top
   bar above 860px, so this is the only place to sign out; removing it would
   have matched the picture and lost the control.

   It keeps Hub's footer METRICS instead: a top hairline rather than a
   bordered tile, 4px/14px of padding, and the 11px `--ink-faint` Hub uses for
   the line it has here.
   -------------------------------------------------------------------------- */

.bo-who {
  margin-top: auto;
  flex: none;
  padding: 10px 12px;
  border-top: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  gap: 10px;
}

.bo-face {
  width: 28px; height: 28px; flex: none;
  border-radius: 50%;
  background: var(--accent-wash) center/cover;
  color: var(--accent);
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700;
}

.bo-who-words { min-width: 0; }
.bo-who b {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.bo-who a { font-size: 11px; color: var(--ink-faint); text-decoration: none; }
.bo-who a:hover { color: var(--danger); }

/*
 * ROOM AT THE TOP.
 *
 * Ian, 2026-11-09, having just deployed: "we need space at the top of the
 * site its too close to the edge i need proper web spacing. especially
 * because this operates like a PWA."
 *
 * 40px, not the 26 this was. Two measurements, both taken rather than
 * guessed:
 *
 *  * The reference (shadcn's default dashboard) insets its content column a
 *    FLAT 24px on every side — the same 24 at 390px as at 1440px, not a
 *    clamp. Our side inset tops out at 40px, so a 26px top was 14px tighter
 *    than our own sides: the page read cropped at the top edge and roomy at
 *    the left, which is the shape of "too close to the edge".
 *  * On the reference that 24px sits UNDER a 56px sticky toolbar, so its
 *    heading lands 88px below the window top (80px at 390px). Above 860px
 *    this app has no toolbar at all — `.bo-top` is display:none — so this
 *    padding is the only thing between the window edge and a 24px/700
 *    heading. 40px does not buy back the whole 56, and deliberately: 88px
 *    of blank space is what a toolbar's worth of room looks like on a page
 *    that has no toolbar in it.
 */
.bo-main { padding: 40px clamp(16px, 3vw, 40px) 60px; min-width: 0; }

/*
 * SECOND COLUMN, SAID EXPLICITLY — AND ONLY ON A DESKTOP.
 *
 * `.bo-side` is `position: fixed`, which takes it out of the grid's flow, so
 * auto-placement put `.bo-main` in the FIRST column, 224px wide, directly
 * underneath the sidebar. Measured: main's left edge at 0 instead of 224.
 * The column still exists and still reserves the width; nothing is in it any
 * more, so the content has to be told where it goes.
 *
 * IN A MIN-WIDTH QUERY, NOT A BARE RULE WITH A PHONE RESET AFTER IT. That is
 * what shipped first and it put a grey column down the left of every phone —
 * Ian, 2026-11-09: "weird gray box on the left in mobile". The reset lived in
 * the `@media (max-width: 860px)` block near the top of this file, and this
 * rule sat four hundred lines below it at the same specificity, so the LATER
 * one won and `grid-column: 2` applied on phones as well. The grid has one
 * column there, so an implicit second column was created, `.bo-main` went
 * into it, and `.bo-top` — the phone's own header bar, a grid item like
 * everything else — was left alone in column 1 and stretched down the side
 * as a tall grey strip.
 *
 * Scoped to the width where column 2 exists, it cannot leak downward however
 * the file is later reordered.
 */
@media (min-width: 861px) {
  .bo-main { grid-column: 2; }
}

/*
 * AND LESS OF IT ON A PHONE, because there the bar is already up there.
 *
 * Below 861px `.bo-top` is sticky against the top edge, so this padding
 * stacks on a bar rather than on the window. Measured at 390px: the bar is
 * 49px, so 32px here puts the heading 81px down — one pixel off the 82px
 * the reference puts its own heading at on the same width. 40px would have
 * made it 89.
 *
 * A clamp cannot say this. The bar appears and disappears at a breakpoint,
 * which is a step, not a ramp, and the top inset has to step with it.
 *
 * MEASURE THIS ON A PAGE THAT SCROLLS. `.bo` is a grid with `min-height:
 * 100%` and two implicit auto rows, so on a page shorter than the viewport
 * the leftover is split between them and the bar reads 75px instead of 49 —
 * a real behaviour, not a harness artefact, but not the one to design the
 * ordinary case against.
 */
@media (max-width: 860px) {
  .bo-main { padding-top: 32px; }
}

/*
 * INSTALLED, ABOVE 860px: the page is what sits against the status bar.
 *
 * `View::pwa()` emits `apple-mobile-web-app-status-bar-style:
 * black-translucent`, which is what gives the installed app an edge-to-edge
 * window instead of a grey band, and the price is that the page has to inset
 * its own content or iOS draws the clock over the top of it.
 *
 * A FLOOR, not the raw inset — the fix kids-control.css already had to
 * learn. `env(safe-area-inset-top)` is honest on a notched iPhone and close
 * to nothing on an iPad, which still has a translucent clock over the page;
 * padding by a number the system reports as zero pads by zero. 30px is the
 * floor the console settled on against a real iPad, and one floor for the
 * whole app beats two. The admin's viewport meta has no `viewport-fit=cover`
 * either, which is a second way `env()` can come back 0 and a second reason
 * the floor is the part that does the work.
 *
 * `min-width: 861px` IS LOAD-BEARING. Below it `.bo-top` is a sticky bar
 * against the top edge with its own inset, and this would pad the page a
 * second time underneath it — an iPad in portrait is 810–834pt, so it is on
 * the bar's side of that line and only lands here in landscape.
 */
@media (display-mode: standalone) and (min-width: 861px) {
  .bo-main { padding-top: calc(40px + max(env(safe-area-inset-top, 0px), 30px)); }
}

/*
 * The page: a measure, and ONE rhythm.
 *
 * Ian, 2026-10-15: the back office is "a little all over the place" beside
 * Hub. Two things were doing it. There was no measure, so on a 1440 laptop a
 * page ran the full width and a card's title sat a hand's width from its own
 * button; and the space between blocks was whatever each page had typed —
 * `.card + .card` gave 14, `margin-top:22px` gave 22, a bare div gave 0.
 *
 * Hub's answer, copied here because Ian is comparing them directly: an inner
 * container that is centred and capped, and a single `space-y` between every
 * top-level block. One number, one place.
 *
 * `is-narrow` is for pages that are a form and nothing else, which Hub caps
 * far tighter than a list — see `max-w-xl` on its own people form. Set from
 * the CONTROLLER: `View::admin()` renders the body to a string BEFORE the
 * layout runs, so a variable assigned in a template body is written after the
 * layout has already read it and silently does nothing.
 */
/*
 * 1600, WHICH IS HUB'S NUMBER.
 *
 * Ian, 2026-11-09, on a 2560 display: "i have a bunch of room on both sides
 * and your making me scroll a bunch".
 *
 * It was 1240 — chosen when the cap was being introduced at all and never
 * checked against the thing it was copying. Hub's own container is
 * `max-w-[1600px]` (templates/layout/head.php), with `max-w-none` for the
 * handful of pages that want the whole width. At 1240 on a 2560 screen the
 * sidebar takes 224 and the page leaves about 1050px of empty desk on either
 * side — and every pixel of width a dashboard does not take, it takes in
 * height instead.
 *
 * `is-narrow` is for pages that are a form and nothing else, which Hub caps
 * far tighter than a list — see `max-w-xl` on its own people form. Set from
 * the CONTROLLER: `View::admin()` renders the body to a string BEFORE the
 * layout runs, so a variable assigned in a template body is written after the
 * layout has already read it and silently does nothing.
 */
.bo-page { max-width: 1600px; margin-inline: auto; }
.bo-page.is-narrow { max-width: 680px; }
/*
 * NO CAP, for the dashboard.
 *
 * Ian, 2026-11-09, on a 2560 display: "i have a bunch of room on both sides".
 * At 1600 he still had 347px of empty desk on either side of it.
 *
 * Hub has this escape hatch and writes it `max-w-none`; it is for pages that
 * are cards and tables rather than paragraphs. A measure exists so that a
 * LINE OF TEXT does not run the width of a cinema screen — a grid of figures
 * has no such problem, and the width it does not take it takes in height
 * instead, which is the thing being complained about.
 *
 * Set from the controller, like `is-narrow`.
 */
.bo-page.is-wide { max-width: none; }
/*
 * 24px BETWEEN THE BANDS OF A PAGE.
 *
 * Ian, 2026-11-09, pointing at shadcn's default dashboard: "apply the same
 * design style and layout". Its cards sit 24px apart, measured off the
 * reference rather than guessed, and the room is most of what makes that page
 * read as calm. 20px was close enough to look like a smaller copy of the same
 * thing, which is the worst place to land.
 */
.bo-page > * + * { margin-top: 24px; }
/* The old per-block spacing, now that the container owns the rhythm. `.gap`
   was here too; it is gone — see the note where its 32px copy used to be. */
.bo-page > .card + .card { margin-top: 24px; }

.bo-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px; }
/* 24px/700 — the reference's own heading. 800 was a shade heavier than
   anything else on these pages needed to be. */
/* 24px/600 at -0.025em — Hub's `text-2xl font-semibold tracking-tight`,
   which is what 29 of its page headings carry. This was 700, a weight
   heavier than the app it is meant to match. */
.bo-head h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.025em; }
.bo-head p { margin: 3px 0 0; color: var(--ink-soft); font-size: 13.5px; max-width: 62ch; }

/* --------------------------------------------------------------------------
   Pieces
   -------------------------------------------------------------------------- */

/*
 * A CARD, SHADED THE WAY HUB SHADES ONE.
 *
 * Ian, 2026-11-09: "it needs to look identical in style shading etc."
 *
 * Three things came across from Hub's `.dash-card`, and the first is the one
 * that does the work:
 *
 *   - A TOP-DOWN LIGHTENING. `linear-gradient(180deg, white 4%, transparent
 *     45%)` over the fill. This is what makes a dark card read as a surface
 *     catching light from above rather than as a rectangle of slightly
 *     different grey. Without it a dark card is an outline.
 *   - A REAL SHADOW. Hub's is `0 1px 2px black/.4, 0 12px 28px -16px
 *     black/.75`. This file's was `black/.04` and `black/.03`, which is
 *     invisible on a near-black ground — a shadow written for a white page
 *     and left in place when the page went dark.
 *   - 8px, not 16. `.sk-card` on the dashboard keeps the big radius, which
 *     is what Hub's own dashboard cards use.
 */
.card {
  /*
   * THE MEASURING BOX for anything inside a card that wants to lay itself
   * out by the room it HAS rather than by the size of the window — see
   * `.pk-grid`, which got narrower as the window got wider until this
   * existed. `inline-size` only: a `size` container must have its block size
   * constrained too, which would make every card here a fixed height.
   */
  container-type: inline-size;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface);
  background-image: linear-gradient(180deg, rgb(250 250 250 / 0.04), rgb(250 250 250 / 0) 45%);
  overflow: hidden;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.4), 0 12px 28px -16px rgb(0 0 0 / 0.75);
}

/*
 * `.card + .card` IS GONE, and this comment is the third attempt at it.
 *
 * It said `margin-top: 20px`, which also catches every card that is a GRID
 * ITEM — so the second and third card of a row sat exactly 20px lower than
 * the first, title line and all. Invisible until measured; at a glance it
 * reads as "the cards are slightly off" rather than as a margin.
 *
 *   attempt 1  `:not(.sk-card)`. Named the one grid that had the problem, so
 *              the next grid had it again the same day — the phone tiles.
 *              It also cost three points of specificity and quietly overrode
 *              `.bo-page > .card + .card`'s 24px with its own 20.
 *   attempt 2  plain again, with `.sk-row > .card, .sk-tiles > .card
 *              { margin-top: 0 }` beside those grids. Then `.ro-figs` and
 *              `.bk-figs` were written and neither was on the list. Ian,
 *              2026-11-09: "they are not aligned".
 *   attempt 3  delete it.
 *
 * MEASURED FIRST, on every rendered page: the parents that actually hold two
 * or more sibling cards are `sk-tiles`, `bk-figs`, `ro-figs`, `sk-row*`,
 * `ro-views` and `sk-phone` — all grids, where `gap` does the spacing — plus
 * `kid-panel`, which carries its own `> * + * { margin-top: 18px }`, and
 * `bo-page`, which carries `> * + * { margin-top: 24px }`. Not one of them
 * needed this rule. A rule that spaces things from the child's side cannot
 * know what the parent is doing, and that is the whole bug; spacing belongs
 * to the container.
 */
.card-body { padding: 16px 18px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 11px 16px;
  border-radius: var(--squircle-sm);
  /* zinc-700, which is what Hub outlines a control with — see
     `--hairline-strong`. The card's own hairline is a step darker. */
  border: 1px solid var(--hairline-strong);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit; font-weight: 600; font-size: 14px;
  /*
   * A DEFINITE line-height, and that is the whole reason the small buttons
   * looked wrong. Inherited, it was 1.5 — so 13px of text gave a 19.5px line
   * box and a 31.5px button, and a half-pixel box puts the label a pixel
   * high. Measured: 7px of space above the label and 8.5px below.
   *
   * 20px also makes the base button exactly 44px: 1 + 11 + 20 + 11 + 1.
   *
   * 44, NOT the 38 this was. 38 was taken from Hub's buttons before Hub
   * measured its own: 202 of its 218 controls were under 44px and it put a
   * floor under all of them. A button on a laptop is also a button on the
   * iPad somebody is holding in the classroom, and 38 is below the size a
   * thumb hits reliably. `min-height` as well as the padding, so a button
   * whose content is shorter than a line of text — an arrow, a single
   * glyph — cannot fall back under it.
   */
  min-height: 44px;
  /* And 44 ACROSS, for the two that are a single glyph: the month arrows on
     the rota were 32px wide, so the one control on that page somebody uses
     every time they open it was the smallest target on it. Every other
     button has a label longer than 44px and never notices this. */
  min-width: 44px;
  line-height: 20px;
  cursor: pointer;
  text-decoration: none;
}

.btn:hover { border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { filter: brightness(1.06); }
.btn-quiet { background: none; color: var(--ink-soft); }
.btn-danger { background: none; border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
/*
 * Smaller TYPE and a tighter inline box — not a smaller target.
 *
 * It was 30px tall (1 + 5 + 18 + 5 + 1) and it was everywhere: every row's
 * action, both arrows on the rota, every button in the Pantallas and Botones
 * tables. Thirty pixels is two thirds of a thumb. The label is still 13px so
 * a dense row still reads as a dense row; the box it sits in is now the same
 * 44 as everything else, which is what a finger is aiming at.
 */
.btn-sm { padding: 12px 12px; font-size: 13px; line-height: 18px; border-radius: var(--squircle-sm); }

.field { display: grid; gap: 5px; margin-bottom: 13px; }

/*
 *  A hint that belongs to a field in a horizontal row, but must not live
 *  inside it.
 *
 *  A `.field` is label over input; add a hint and the field grows at the
 *  BOTTOM, so a row with `align-items: end` lines its button up with the
 *  hint rather than the input. Putting the hint on its own full-width line
 *  keeps every field in the row the same height, which is the condition
 *  that makes end-alignment correct in the first place.
 */
/*
 *  A MEASURE ON `.hint` WOULD BREAK THIS, which is why the cap below is on
 *  `.bo-page > .hint` and not on `.hint` itself.
 *
 *  Flexbox clamps an item's flex base size by its max-width BEFORE it works
 *  out where the line breaks. A `max-width: 64ch` on `.hint` therefore turns
 *  `flex: 1 0 100%` from "take a whole line" into "take 480px" — and 480px
 *  fits beside the button, so the hint comes back up onto the input's line
 *  and `align-items: flex-end` lines the button up with the HINT again,
 *  which is the exact fault this class exists to prevent. Measured on
 *  Maestros while it was wrong: the hint sat at y=237 beside an 81px button
 *  instead of under it.
 */
.hint-row { flex: 1 0 100%; margin-top: -2px; }
.field label { font-size: 13px; font-weight: 600; color: var(--ink-soft); }
/*
 * Inputs, selects and textareas together.
 *
 * The select was left to the browser and the input was not, so a form row read
 * as two different form rows — a native dropdown sitting a few pixels off the
 * baseline of everything beside it, in a different typeface.
 *
 * The textarea was the same omission and nobody had noticed, because until
 * Apoyo arrived no page put one next to an input. It came out in the
 * browser's monospace on its own fill, which made the one field asking for a
 * sentence look like a code box. Ian, 2026-11-01. `height` is overridden
 * below — a textarea is the one control here that is not 44px.
 */
.field input,
.field select,
.field textarea {
  width: 100%;
  /*
   * 44, matching `.btn` above, which is the only number that matters here:
   * the fields and the button beside them have to be one height or the row
   * reads as crooked. It was 38 with the button; both moved together.
   *
   * A date input is in here too, which is the one somebody opens on a phone.
   */
  height: 44px;
  padding: 0 12px;
  border-radius: var(--squircle-sm);
  border: 1px solid var(--hairline-strong);
  /* A WELL, not a tile: Hub's fields are zinc-950 on a zinc-900 card. This
     was `--surface-2`, lighter than the card it sits on, which made every
     form read as a row of buttons. */
  background: var(--field);
  color: var(--ink);
  font: inherit;
  appearance: none;
  -webkit-appearance: none;
}

/* The chevron, drawn rather than native, so it is the same on every platform
   and the same colour as the label above it. */
.field select {
  padding-right: 34px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  /* Half of 44 less half the 5px arrow. Tied to the height above: change
     one and this drifts off centre without anything failing — it did, when
     the field went 38 -> 44 and this stayed at the 38px answer. */
  background-position: calc(100% - 17px) 19px, calc(100% - 12px) 19px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* Rows, so it is sized by `rows=` rather than by the 44px above; the padding
   is the input's, with the vertical lead a line of text needs. Resizing is
   left on but only upward, so dragging it can never break the row beside it. */
.field textarea {
  height: auto;
  min-height: 44px;
  padding: 9px 12px;
  line-height: 1.45;
  resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: 1px;
}
/* A hint is a hint wherever it stands. This was only ever scoped to
   `.field`, so a `<p class="hint">` under a ROW of fields — which is where a
   hint about the row has to go — came out at body size and read as a
   paragraph of the page rather than as a note under the inputs. */
.hint { display: block; font-size: 12px; line-height: 1.5; color: var(--ink-faint); }
/* Emphasis inside a hint, the same way `.card-sub b` works — one step less
   faint rather than the same grey as everything around it. Imágenes wrote it
   as an inline `style="color:var(--ink-soft)"` and was the only page that
   needed it, which is how it stayed inline. */
.hint b { color: var(--ink-soft); }
/*
 * A measure, like `.card-sub` has — but ONLY for a hint standing on its own
 * as a paragraph of the page. Pantallas ends with one, and without this it
 * is a full-width line of 12px type on a 1280px laptop: about 180
 * characters, which nobody reads to the end of twice.
 *
 * Scoped to a direct child of the page rather than written on `.hint`,
 * because `.hint-row` is a flex item whose whole job is to be 100% wide and
 * a max-width on `.hint` silently takes that away. See `.hint-row` above.
 */
.bo-page > .hint { max-width: 64ch; }
.field .hint { font-size: 12px; color: var(--ink-faint); }

/*
 * A field standing on its own gets a measure too.
 *
 * Only a DIRECT child of a card body: inside `.form-row` or one of the flex
 * rows on the child's page the field is already sized by its column, and a
 * max-width there would pull it out of the row it was lined up with.
 */
.card-body > .field input,
.card-body > .field select { max-width: 560px; }

/*
 * One row of fields that all end on the same line.
 *
 * `align-items: end` is what does it: the fields are different heights because
 * their labels wrap differently, and without this each one centres itself and
 * the row reads as scattered. A hint belongs UNDER the row for the same
 * reason — inside one field it pushes only that field's control up.
 */
.form-row {
  display: grid;
  grid-template-columns: minmax(170px, 1.7fr) 120px 150px minmax(190px, 2fr) auto;
  gap: 12px;
  align-items: end;
}
.form-row .field { margin-bottom: 0; }
/* The fields in this row are 44; the button sitting at the end of it has to
   be the same number or `align-items: end` lines up two different boxes. */
.form-row .btn { height: 44px; }

@media (max-width: 920px) {
  .form-row { grid-template-columns: 1fr 1fr; }
  .form-row .btn { grid-column: 1 / -1; justify-content: center; }
}

table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
  text-align: left; padding: 10px 18px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-faint);
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
}
td { padding: 10px 18px; border-bottom: 1px solid var(--hairline); vertical-align: middle; }
tr:last-child td { border-bottom: 0; }

/* A table of names is not a page of links. Underlining every row turned the
   balances page into a wall of green rules. */
td a { color: var(--ink); text-decoration: none; font-weight: 600; }
td a:hover { text-decoration: underline; }

/* Money lines up or it is not money. */
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}
.when { text-align: right; white-space: nowrap; color: var(--ink-faint); font-size: 12.5px; }

/*
 * The buttons inside a table cell.
 *
 * They were laid out with `white-space: nowrap` on the <td> and a
 * `margin-left: 6px` written on each button after the first — so the column
 * was as wide as its labels laid end to end and could not wrap however
 * narrow the screen got. Once every button is 44px tall that is three
 * buttons across a cell with no way out of it.
 *
 * A flex row with a real gap instead. It wraps, which is the point, and the
 * spacing is said once rather than on each button.
 */
.cell-acts { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }

.pill {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: 11.5px; font-weight: 700;
}
.pill-on  { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok); }
.pill-off { background: var(--surface-2); color: var(--ink-faint); }

/* A token is long and monospaced and must be selectable in one go. */
.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  color: var(--ink-soft);
  word-break: break-all;
}

.note {
  padding: 11px 14px; border-radius: var(--squircle-sm); font-size: 13.5px;
  border: 1px solid var(--hairline); background: var(--surface);
}
/*
 * Both of these were literal Tailwind colours — green-400 and red-400 —
 * written before there were tokens to say the same thing. Two consequences:
 * the good note's EDGE was green while its FILL was the accent wash, which
 * turned violet with the rest of the back office and left a green outline
 * around a violet panel; and neither could follow a palette change.
 */
.note-good { border-color: color-mix(in srgb, var(--ok) 40%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }
.note-bad  { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: color-mix(in srgb, var(--danger) 9%, transparent); }


/* --------------------------------------------------------------------------
   The shared pieces — 2026-10-15

   Ian: the back office "feels homemade". It was not the primitives; buttons
   and fields were already 38px and already on the tokens. It was that every
   page BUILT ITS OWN LAYOUT inline. 210 `style="..."` attributes across 26
   templates, 46 of them on one child's page, and the same four things
   re-invented each time:

       a card with a title and a sentence under it   (5x on child.php alone)
       a face beside a name and some meta
       a list row ending in a button
       a row of buttons under a form

   The dashboard had already solved its own version — `.db-card-head`,
   `.db-fig`. These are those, generalised, so the other twenty-five pages
   stop hand-rolling them. Classes, not inline styles: the point is that
   changing the look is one edit here rather than two hundred there.
   -------------------------------------------------------------------------- */

/* Title, sentence and actions — the pattern that was inline on every page. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 18px;
}
/* Only when something follows. A head on its own is the whole card, and a
   rule under it then draws a line along the bottom edge. */
.card-head:not(:last-child) { border-bottom: 1px solid var(--hairline); }

/*
 * The text block takes the slack — whether or not a mark comes first.
 * Written as two rules rather than one `:first-child`, because adding an
 * icon to a heading would otherwise hand the FLEX to the icon and squeeze
 * the title into a column two words wide.
 */
/*
 * 8rem, not the 260px this started as.
 *
 * 260 is wider than the whole side column on a child's page (about 175px of
 * content), so the text block could not share a line with its own icon and
 * wrapped underneath it — "El banco" rendered with the mark stranded on the
 * line above. A basis small enough to sit beside a 30px tile in the
 * narrowest column we have, and `grow` does the rest everywhere wider.
 */
.card-head > :first-child:not(.ico) { flex: 1 1 8rem; min-width: 0; }
.card-head > .ico + * { flex: 1 1 8rem; min-width: 0; }

/*
 * The mark on a heading. Ian, 2026-10-16: "add icons to make it easier to
 * navigate." A tinted tile rather than a bare glyph — on a page of five
 * headings the eye finds a block of colour long before it reads a word.
 */
.card-head > .ico {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  margin-top: 1px;
  /* The token, so a heading's tile is the same shape as the buttons and the
     fields below it. It was 10px against a card's 16 and a button's 10 that
     has since become 12 — three radii on one page, which is the whole thing
     this pass is removing. */
  border-radius: var(--squircle-sm);
  background: var(--accent-wash);
  color: var(--accent);
}
.card-head > .ico svg { width: 17px; height: 17px; display: block; }
/* The destructive section is the one that must NOT wear the accent. */
.zone .card-head > .ico,
.ico-danger { background: rgb(220 38 38 / 0.11); color: var(--danger); }
/*
 * 14px/600 — the reference's own card title.
 *
 * Measured off shadcn's default dashboard, 2026-11-09: every card on it
 * titles itself at 14px, weight 600, line-height 19.25 (`text-sm
 * leading-snug font-semibold`). This was 15/700, which is the heavier,
 * slightly larger thing a card title becomes when it is guessed from the
 * page heading rather than measured — and on a page of five headings the
 * difference is the whole reason the reference reads calm and this read
 * busy. The sentence under it is 13/400 grey, so 600 still separates them.
 */
.card-head h2,
.card-title {
  display: block;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.375;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.card-head p,
.card-sub {
  display: block;
  margin: 4px 0 0;
  max-width: 64ch;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.5;
}
/* Emphasis inside the sentence under a title. It was written inline — one
   `style="color:var(--ink)"` on Ajustes' "Hoy: Navidad" — and every other
   <b> in a card-sub was left the same grey as the words around it, so the
   emphasis did nothing. */
.card-sub b { color: var(--ink); }
/* The quieter second sentence some of these carry. */
.card-sub-2 { display: block; margin: 3px 0 0; color: var(--ink-faint); font-size: 12.5px; line-height: 1.5; }
.card-acts { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* A card whose body is a table or a list: the head supplies the padding. */
.card-flush { padding: 0; }

/*
 * A second section inside the SAME card.
 *
 * Hub puts a whole form in one card; the child's page had each group of
 * fields in a card of its own, so saving one form meant looking at three
 * separate slabs with the save button floating under all of them. This is a
 * heading that opens a new section without starting a new card.
 */
.card-head-mid { border-top: 1px solid var(--hairline); }

/*
 * The card's own footer — where the form's buttons belong.
 *
 * Tinted, so the end of the form is visible without reading it, and inside
 * the card so the primary action of the page is attached to the thing it
 * saves rather than hanging in the gap below it.
 */
.card-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 14px 18px;
  border-top: 1px solid var(--hairline);
  background: var(--surface-2);
}
.card-foot .spacer { flex: 1 1 auto; }
.card-foot .hint { font-size: 12.5px; color: var(--ink-faint); }

/*
 * Somebody, with their face.
 *
 * Hub learned this on the household index: a list of children with no faces
 * reads as a database, and the same list with faces reads as the class. The
 * avatar falls back to initials on a tinted disc rather than a grey box.
 */
.ident { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.ident-main { flex: 1 1 200px; min-width: 0; }
.ident-name { display: block; font-size: 19px; font-weight: 700; letter-spacing: -0.02em; }
.ident-meta { display: block; margin-top: 1px; color: var(--ink-soft); font-size: 13.5px; }
.ident-note { display: block; margin-top: 3px; color: var(--ink-faint); font-size: 12px; }

/*
 * A list of people, each ending in one action.
 *
 * Rows rather than a table: a table makes the name a cell among cells, and
 * what matters here is the person and the one thing you can do about them.
 */
.rows { display: grid; }
.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 18px;
  border-top: 1px solid var(--hairline);
}
.rows > .row:first-child { border-top: 0; }
.row-main { flex: 1 1 auto; min-width: 0; }
.row-name { display: block; font-size: 14px; font-weight: 600; color: var(--ink); }
.row-meta { display: block; color: var(--ink-faint); font-size: 12.5px; }
.row-acts { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
/* Only where a row goes somewhere. A hover on a row with nothing behind it
   says it is a link when it is not. */
a.row:hover, .row-link:hover { background: var(--surface-2); }
a.row { color: inherit; text-decoration: none; }

/*
 * THE ROW IS THE LINK, and the chevron is what says so.
 *
 * Both of these lists used to end in a BUTTON that went where the row
 * already went — "Ver su ficha" on a duplicate child, and nothing at all at
 * the end of an inscripción. A 30px button inside a 60px link is two
 * targets for one destination: the row is bigger, so it is what gets
 * pressed, and the button is then just a second thing to read.
 *
 * Drawn in CSS rather than as an icon because `View::icon()` has no chevron
 * and `app/` is not this change's to edit. Two borders on a rotated square
 * is the same mark the sidebar's `.bo-caret` draws.
 */
a.row::after,
.wv-list a::after {
  content: '';
  flex: none;
  align-self: center;
  width: 7px;
  height: 7px;
  margin-left: 2px;
  border-top: 2px solid var(--ink-faint);
  border-right: 2px solid var(--ink-faint);
  transform: rotate(45deg);
}
a.row:hover::after,
.wv-list a:hover::after { border-color: var(--accent); }

/*
 * Rows inside a padded body.
 *
 * `.row` carries the card's own 18px so a list can sit flush against the
 * card's edges. Nested in a `.card-body`, which already has that padding,
 * it indented twice and the faces stood a thumb's width from the edge.
 */
.card-body > .rows { margin-top: 12px; }
.card-body > .rows > .row { padding-left: 0; padding-right: 0; }

/*
 * The face, at more than one size.
 *
 * `.kid-face` already existed at a fixed 62px with a 20px radius — the
 * squircle is the kid twist and is deliberate, so this does NOT introduce a
 * second, round avatar beside it. It makes the existing one take a size, and
 * the radius follow, which is what a row needs.
 */
/* The size modifiers live with the rule itself, further down — a copy
   here would sit BEFORE it and lose on source order. */

/*
 * Nothing here yet.
 *
 * It was centred grey text floating in an empty card, which reads as a page
 * that failed to load. A mark above the line makes it read as a state.
 */
.empty {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 40px 20px;
  text-align: center;
  color: var(--ink-soft);
  font-size: 13.5px;
}
.empty::before {
  content: '';
  width: 38px;
  height: 38px;
  margin-bottom: 4px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px dashed var(--hairline);
}
.empty b { display: block; color: var(--ink); font-size: 14.5px; font-weight: 700; }
.empty p { margin: 2px 0 0; max-width: 46ch; line-height: 1.5; }

/*
 * The buttons under a form.
 *
 * They were a bare flex div hanging in the middle of the page between two
 * cards, so the primary action of the screen looked like an afterthought.
 * A hairline above it and the page's own padding under it make it a footer.
 */
.form-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
}
.form-foot .spacer { flex: 1 1 auto; }
.form-foot .hint { font-size: 12.5px; color: var(--ink-faint); }

/*
 * The destructive thing at the bottom of a page.
 *
 * Set apart rather than dressed in red: a red panel shouts on a page that is
 * mostly routine, and the button inside it is already `.btn-danger`.
 */
.zone { margin-top: 24px; border-style: dashed; background: none; box-shadow: none; }
.zone .card-body { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.zone-text { flex: 1 1 240px; color: var(--ink-soft); font-size: 13px; line-height: 1.5; }
.zone-text b { color: var(--ink); }

/* --------------------------------------------------------------------------
   Faces in a grid — the children and the teachers

   Ian, 2026-10-16: show the photos as squares with the name under them, and
   put a red mark beside a child who has an allergy or something else the
   teacher must know.

   A table answered "how much does each one have?" and nobody was asking it.
   The question at the door is "which one is this, and is there anything I
   need to know" — a face and a flag, not a row of columns.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   THE DIRECTORY — children and teachers.

   Ian, 2026-11-09: "please also fix the layout for the kids and teacher
   directory give me something new and modern and intuitive."

   WHAT IT WAS: a grid of portrait tiles, 132px wide, photo on top and two
   lines of text under it. Four things were wrong with it and only the first
   is cosmetic.

     - EVERY CARD A DIFFERENT HEIGHT. A name that wrapped to two lines made
       its card taller, so on the teachers page the Retirar buttons sat at
       four different heights across one row. `align-items: start` was there
       to stop a long name dragging a whole row with it, which traded one
       unevenness for another.
     - A RAGGED LAST ROW. Eleven teachers in a ten-column grid left one card
       alone on a line of its own with an acre of empty beside it.
     - TALL. A 1:1 photo at 132px plus two lines of text is a 200px row, so
       fifteen children were 400px of page — on a screen with 2200px of
       width going spare.
     - FLAGS AS 22px ICONS IN A CORNER. An allergy is the single most
       important thing on that card and it was a glyph the size of a
       fingernail, overlapping the child's face.

   WHAT IT IS NOW: a responsive grid of ROW-SHAPED cards. Each one is a 48px
   round face, the name, a line of facts, and whatever belongs on the right.
   `auto-fill` with a 300px floor means three or four across on a desktop and
   one on a phone, with no breakpoint doing the deciding.

   The face stays, because recognising a child is the job this page exists
   for — this is not Hub's flat row list, which has no faces to speak of. It
   is Hub's row ANATOMY (face, name, a quiet second line, something on the
   right) laid out across the width instead of down it.
   -------------------------------------------------------------------------- */

.people {
  display: grid;
  /*
   * 340, MEASURED, not chosen.
   *
   * The first number here was 300 and the teachers' meta line wrapped to
   * three rows beside the face at it: a card is 304px, of which 48 is the
   * face, 36 is padding and gaps, and 84 is the Retirar button — leaving
   * 136px for "2 domingos · último 22 nov", which needs about 150.
   *
   * At 340 the text column is 172 and the line fits with room.
   *
   * `min(340px, 100%)` AND NOT A BARE 340. This is the auto-fill trap and it
   * is silent here: when the container is narrower than the floor, the track
   * stays at the floor and the grid overflows. At 375px the page is 343
   * wide and the card body leaves 307 for the grid, so a bare 340 put 33px
   * of every row outside its card — and `.card` has `overflow: hidden`, so
   * it was CLIPPED rather than scrollable. Nothing to scroll means nothing
   * for a page-width probe to find; narrowcheck caught it because it asks a
   * different question, "is anything hidden", and reported 32px on both
   * pages. `min()` clamps the floor to the container and the grid falls to
   * one column by itself, with no breakpoint deciding anything.
   */
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: 10px;
}

.pcard {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  /* A STATED HEIGHT, so a wrapped name cannot make one card taller than its
     neighbours — which is the whole bug. The name truncates instead. */
  min-height: 68px;
  padding: 10px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  color: inherit;
  text-decoration: none;
  min-width: 0;
}
a.pcard { transition: border-color .12s var(--ease), background-color .12s var(--ease); }
a.pcard:hover { border-color: var(--hairline-strong); background: var(--surface); }
@media (prefers-reduced-motion: reduce) { a.pcard { transition: none; } }
a.pcard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/*
 * The face, round and 48px.
 *
 * Round rather than square because it is now beside text rather than above
 * it, and that is the shape every other face in these three apps uses in
 * that position. No photo gets initials on the accent wash — never an empty
 * grey box, which reads as an image that failed rather than a child nobody
 * has photographed yet.
 */
.pcard-photo {
  position: relative;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  flex: none;
  border-radius: 50%;
  background: var(--accent-wash) center/cover;
  color: var(--accent);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.02em;
  box-shadow: inset 0 0 0 1px var(--hairline);
  overflow: hidden;
}
/* The ring is for the EMPTY circle only — over a photograph it would put a
   line across the edge of the child's face. */
.pcard-photo[style] { box-shadow: none; }

.pcard-body { min-width: 0; flex: 1 1 auto; display: grid; gap: 2px; }

.pcard-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink);
}
/* TRUNCATE, do not wrap. A second line is what made the cards uneven. The
   full name is in the row's own title attribute and on the child's page. */
.pcard-who { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

.pcard-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 12.5px;
  color: var(--ink-faint);
  min-width: 0;
}
.pcard-meta .saldo { color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* The right of the row: a button on the teachers page, a chevron on the
   children's. */
.pcard-foot { flex: none; display: flex; align-items: center; gap: 8px; }
.pcard-foot .btn { white-space: nowrap; }
.pcard-go { color: var(--ink-faint); font-size: 18px; line-height: 1; }

/* Retired, or off the television. Not greyscale on the photograph any more:
   at 48px a grey face is just a worse photograph. The NAME carries it. */
.pcard-off { opacity: .68; }
.pcard-off .pcard-name { color: var(--ink-soft); }

/*
 * THE FLAGS ARE WORDS NOW, on the second line.
 *
 * They were 22px icon chips stacked in the corner of the photograph. An
 * allergy is the most important fact on this card and it was rendered at the
 * size of a fingernail, on top of the child's face, distinguishable from
 * "no photographs" only by its colour. They sit in the meta line as small
 * labelled pills, in severity order, and the title still carries the detail.
 *
 * Still gated on kids.safety, exactly as the icons were: an allergy is
 * medical information and a missing emergency contact says something about a
 * family. Somebody who may not open a child's safety panel may not read it
 * off a directory either.
 */
.flag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  line-height: 16px;
}
.flag svg { width: 12px; height: 12px; display: block; }
.flag-bad   { background: rgb(220 38 38 / 0.16); color: var(--danger); }
.flag-quiet { background: var(--surface-2); color: var(--ink-soft); }

/* A SUBHEADING INSIDE THE LIST, so the retired teachers are a group rather
   than one greyed card floating at the end of the grid. It spans every
   column the grid happens to have. */
.people-head {
  grid-column: 1 / -1;
  margin: 6px 0 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.people-head:first-child { margin-top: 0; }

/*
 * `.gap` IS GONE. It was declared twice — 24px up with the page's rhythm and
 * 32px here — and because this one is further down the file it won, so the
 * two blocks wearing it (the trailing note on Pantallas, the history warning
 * on Estado) sat 32px below their neighbours while every other band on the
 * page sat 24px below its own. One band out of rhythm on two pages, from one
 * class declared in two places.
 *
 * The reference has ONE spacing between bands and no bigger break, so the
 * class is removed rather than resolved: `.bo-page > * + *` already gives
 * both of those blocks the 24px they should have had, and a utility whose
 * value equals the default is a trap waiting for the next reader.
 */

/*
 * The handful of one-line utilities the templates kept writing inline.
 *
 * Counted before writing them: `margin-bottom:16px` on a notice appeared 18
 * times, `font-size:15px` on a section's <b> 17 times, the same five-property
 * flex row on a form 8 times. These are those, named once.
 */
/*
 * Two columns on a detail page — Hub's profile layout.
 *
 * A child's page was seven full-width slabs in a column, which is most of
 * what "all over the place" meant: nothing said which of the seven was the
 * thing you came to do. The form is the main column; what you only read —
 * the balance, the paperwork — goes beside it.
 *
 * 1100px, not 1024: below that the side column is under 300px and the
 * balance table inside it starts scrolling sideways.
 */
.bo-split { display: grid; gap: 20px; align-items: start; }
.bo-split > * { min-width: 0; }
@media (min-width: 1100px) {
  .bo-split { grid-template-columns: minmax(0, 1.9fr) minmax(300px, 1fr); }
}
/* Each column stacks on the page's own rhythm. */
.bo-col > * + * { margin-top: 20px; }

.note-top { margin-bottom: 16px; }
.inline-form { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.inline-form .field { margin-bottom: 0; }
.field-grow { margin-bottom: 0; flex: 1; min-width: 220px; }
/*
 * The other half of `.field-grow`: a field that must NOT take the slack.
 *
 * A PIN is four digits, a clock time is five characters, and Ajustes wrote
 * `style="margin:0;width:150px"` and `style="margin:0;min-width:150px"` for
 * the same idea two cards apart. One name for it, and the margin is already
 * zero inside an `.inline-form`.
 */
/* 150, measured: at 390px a card's inside is 322px, so two of these plus the
   10px gap (310) share a line and the button wraps under them. At 160 they
   did not, and the two clock fields came out on two different rows. */
.field-tight { margin-bottom: 0; flex: none; width: 150px; }
/* A one-button form that must not break the line it sits on. */
.inline { display: inline; }
.soft  { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }
.right { text-align: right; }

/* A sentence introducing a group of fields, inside a card body. */
.sect { margin: 0 0 12px; color: var(--ink-soft); font-size: 13px; line-height: 1.5; max-width: 64ch; }
.sect:last-child { margin-bottom: 0; }

/*
 * REMOVED, 2026-11-09: a `@media (prefers-color-scheme: dark)` block that
 * repainted `.bo-body .card` and blanked its shadow.
 *
 * It was written when the back office followed the system preference. It
 * does not any more — `.bo-body` pins one dark palette for everybody, for
 * the stated reason that Hub is dark always and two apps the same person
 * moves between should not disagree about it. The media query stayed behind.
 *
 * So the back office was rendering cards TWO WAYS depending on a setting it
 * no longer honours: a translucent white fill with no shadow for anyone
 * whose laptop was in dark mode, and the stylesheet's real card for anyone
 * whose laptop was not. Nobody would see both, which is why it survived; it
 * surfaced only when Ian asked for Hub's shading and the new treatment
 * applied to half the audience.
 *
 * `.bo-body .zone { background: none }` went with it for the same reason.
 */

/* --------------------------------------------------------------------------
   Sign in
   -------------------------------------------------------------------------- */

.si {
  min-height: 100%;
  display: grid;
  place-items: center;
  padding: 24px;
  background: linear-gradient(178deg, var(--sky-top) 0%, var(--sky-bottom) 100%);
}

.si-card { width: min(400px, 100%); }
.si-card .bo-mark { width: 46px; height: 46px; border-radius: 15px; margin-bottom: 14px; }
.si-card h1 { margin: 0 0 3px; font-size: 21px; font-weight: 800; letter-spacing: -0.03em; }
.si-card .sub { margin: 0 0 18px; color: var(--ink-soft); font-size: 13.5px; }
.si-card .btn { width: 100%; justify-content: center; padding: 10px; }

/* --------------------------------------------------------------------------
   Horario — the month
   -------------------------------------------------------------------------- */

/*
 * `.cal-bar` IS GONE, and the month navigator is the calendar's `.card-head`.
 *
 * It was a bare toolbar floating on the page above the grid — no padding, no
 * ground, no border — with the month name at 16px/700 and `Hoy` pushed right
 * by an inline `margin-left:auto`. Every other page in the back office puts
 * its title and its controls in a head row at the top of the card they
 * belong to, which is also what the reference does on every one of its own
 * cards, so Horario was the one page whose heading was not attached to its
 * content. There is nothing here to replace it with: it uses `.card-head`,
 * `.card-title` and `.card-acts` like everything else, and the calendar
 * itself wears `.card` for the lift its neighbours already had.
 */
/* --------------------------------------------------------------------------
   IMÁGENES — the byline and the preview.
   -------------------------------------------------------------------------- */

/* Who uploaded it, under the thumbnail. */
.pic-by {
  display: flex; align-items: center; gap: 8px;
  margin: 0; padding: 9px 12px 0;
  min-width: 0;
}
.pic-by-face {
  width: 22px; height: 22px; flex: none;
  border-radius: 50%;
  background: var(--accent-wash) center/cover;
  color: var(--accent);
  display: grid; place-items: center;
  font-size: 10px; font-weight: 700;
}
.pic-by > span { min-width: 0; display: grid; gap: 0; }
.pic-by b {
  font-size: 12.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.pic-by small { font-size: 11px; color: var(--ink-faint); }

/*
 * The preview dialog.
 *
 * The picture is capped by the VIEWPORT, not by the dialog: `70svh` leaves
 * room for the title, the byline and the button on a short laptop screen,
 * and `svh` rather than `vh` because on iOS `vh` is the chrome-hidden height
 * and a 70vh image is taller than 70% of what you can actually see.
 *
 * `contain`, never `cover`: this is the one place somebody is looking at the
 * whole picture, and cropping it here would hide exactly what they opened it
 * to check.
 */
/*
 * `.pic-full`, and NOT `.pic-view img`.
 *
 * hiddencheck.py reads a `display` beside any <dialog> class as the bug it
 * exists to catch — an author `display` outside `[open]` beats the user
 * agent's `dialog:not([open]) { display: none }` whatever the specificity,
 * and this app has shipped that twice. Here the `display` is on the image
 * INSIDE the dialog, which is harmless, but a guard that has to tell the
 * difference is a guard that can be argued with. The image gets its own
 * class and the rule never mentions the dialog's.
 */
/*
 * THE PICTURE, SIZED BY ITS OWN SHAPE.
 *
 * It was `width: 100%` with `max-height: 70svh`, which for the square and
 * portrait pictures teachers actually upload meant a 620px-wide box and a
 * 620px-tall image — a dialog taller than most laptop screens for a preview
 * nobody needs at full size. Ian, 2026-11-09: "the preview modal is also not
 * good looking".
 *
 * `width: auto` with a height cap lets the image find its own width: a
 * landscape photograph fills the dialog, a portrait one is tall and narrow
 * and CENTRED rather than stretched across a grey field. `max-width: 100%`
 * is what stops a very wide one escaping.
 *
 * The cap is the smaller of 56svh and 480px. The viewport unit alone leaves
 * a 27-inch screen showing a 700px image inside a 620px dialog; the pixel
 * value alone is taller than a laptop in landscape. `svh` rather than `vh`
 * for the usual iOS reason — `vh` is the chrome-hidden height.
 */
.pic-full {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: min(56svh, 480px);
  margin-inline: auto;
  object-fit: contain;
  border-radius: var(--squircle-sm);
  background: var(--sky-top);
}

/* --------------------------------------------------------------------------
   BANCO — the figures strip and the adjustment dialog.
   -------------------------------------------------------------------------- */

/* The figures and the one button, on a line. `flex-wrap` so a phone stacks
   the button under the strip instead of squeezing both. */
.bk-top { display: flex; flex-wrap: wrap; align-items: start; gap: 10px; }
.bk-figs {
  flex: 1 1 420px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: 10px;
}
/*
 * THE BUTTON IS A BUTTON, 44px, aligned to the top of the strip.
 *
 * It said `align-self: stretch`, on the theory that two things side by side
 * should be the same height. They should not: a 66px violet slab beside
 * three figure tiles reads as a fourth tile somebody coloured in, and Ian
 * called it — "some buttons ... have special formatting that are not
 * allowing them to constrain to the specific style". A button in this back
 * office is 44px high everywhere else and it is 44 here.
 *
 * `align-self: start` rather than `center`: the strip's tiles are a band,
 * and a control hanging in the middle of a band has no edge to line up with.
 */
.bk-top > .btn { flex: 0 0 auto; align-self: start; }

/* Two fields that belong together on one line, one per line on a phone. */
.bk-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr)); gap: 12px; }

/* The balance at the end of a row. Tabular, so a column of them lines up
   whatever the digits are. */
.bk-num { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }

/* --------------------------------------------------------------------------
   THE LEDGER — the bank's history, as rows rather than a five-column table.
   See the note in templates/kids/bank.php for why the table went.
   -------------------------------------------------------------------------- */

/*
 * THE HISTORY, SHUT.
 *
 * `.card-head` is also the <summary>, so the browser's own marker has to go
 * — it is a black triangle that ignores `color` and on Safari sits outside
 * the padding box. Our own chevron turns over instead, like every other
 * disclosure in this back office.
 */
.bk-hist > summary { cursor: pointer; list-style: none; }
.bk-hist > summary::-webkit-details-marker { display: none; }
.bk-hist > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.bk-hist-go {
  margin-left: auto; flex: none; display: flex; color: var(--ink-faint);
  transition: transform .3s cubic-bezier(0.23, 1, 0.32, 1);
}
.bk-hist-go svg { width: 16px; height: 16px; display: block; }
.bk-hist[open] > summary .bk-hist-go { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .bk-hist-go { transition: none; } }

.led { margin: 0; padding: 6px; list-style: none; display: grid; gap: 2px; }
.led li {
  display: flex; align-items: center; gap: 12px;
  min-height: 52px; padding: 7px 10px;
  border-radius: var(--squircle-sm);
}
.led li:hover { background: var(--surface-2); }

/*
 * The amount, as a chip with a FIXED width so a column of them lines up
 * without being a table column. `tabular-nums` is what makes a 1 as wide as
 * a 7; without it "+5" and "+13" start at different pixels and the eye reads
 * the edge as ragged.
 */
.led-amt {
  flex: none; width: 62px;
  display: inline-flex; align-items: center; justify-content: center;
  height: 26px; border-radius: 999px;
  background: var(--surface-2); color: var(--ink);
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* Taking points off is the rarer thing, and the thing somebody scans for. */
.led-amt.is-out { background: rgb(220 38 38 / 0.16); color: var(--danger); }

.led-body { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
.led-body b { font-size: 14px; font-weight: 500; color: var(--ink);
              overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.led-body small { font-size: 12px; color: var(--ink-faint);
                  overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

/* The time, right of the row. It wraps to the second line on a phone rather
   than squeezing the name — see the query below. */
.led-when { flex: none; font-size: 12px; color: var(--ink-faint);
            font-variant-numeric: tabular-nums; white-space: nowrap; }

@media (max-width: 560px) {
  /* At 375 the name, the reason and the time do not share a line. The time
     is the least urgent of the three, so it goes under the name. */
  .led li { flex-wrap: wrap; }
  .led-when { width: 100%; padding-left: 74px; }
}

/* --------------------------------------------------------------------------
   HORARIO — the rota, on Hub's cleaning-rota pattern.

   Ian, 2026-11-09: "scheduling calendar should be like the cleaning rotation
   that we did for the hub copy the style and function of that one."

   Hub's /cleaning is: a row of figures, a List/Calendar toggle, then both
   panels side by side once there is room for them. Everything below is that
   shape, built on this app's own tokens rather than Tailwind utilities.
   -------------------------------------------------------------------------- */

/* The figures. `auto-fit` rather than three fixed columns so a phone gets
   them stacked without a breakpoint being written for it. */
.ro-figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  gap: 10px;
}
.ro-fig { padding: 10px 14px; }
.ro-fig-l {
  margin: 0; font-size: 11px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint);
}
.ro-fig-n {
  margin: 2px 0 0; font-size: 22px; font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--ink); line-height: 1.15;
}
.ro-fig-n.is-ok  { color: var(--ok); }
.ro-fig-n.is-bad { color: var(--danger); }

/* The bar: the toggle on the left, the month and its arrows on the right. */
.ro-bar {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 12px;
}
.ro-month { display: flex; align-items: center; gap: 8px; }
.ro-month-l {
  font-size: 15px; font-weight: 600; color: var(--ink);
  min-width: 9ch; text-align: center;
}

.ro-switch {
  display: inline-flex; padding: 2px;
  border: 1px solid var(--hairline); border-radius: var(--squircle-sm);
}
.ro-switch button {
  border: 0; border-radius: 6px; background: none; cursor: pointer;
  /* 40, not 44: this sits INSIDE a 2px-padded group, so the group itself is
     44 and the finger target is the group. A 44px child would make it 48. */
  min-height: 40px; padding: 0 14px;
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink-soft);
}
.ro-switch button.is-on { background: var(--accent); color: var(--accent-ink); }

/*
 * BOTH PANELS AT ONCE, once there is room — and the CALENDAR goes left.
 *
 * The list is FIRST in the document, so a phone and a screen reader get it
 * first; `order` is what puts the month on the left of a wide screen. Hub
 * does exactly this and says why: the list is the thing you came for.
 *
 * 1180px, not Hub's 1280: its calendar carries a 640px minimum and ours is
 * narrower, so the two fit sooner. Measured — below this the month grid
 * starts squeezing the day names.
 */
.ro-views { display: grid; gap: 16px; }

@media (min-width: 1180px) {
  /*
   * 672 FOR THE MONTH, measured rather than guessed. A month grid is seven
   * columns, and a cell has to hold a day number, a face and a first name.
   * At the 420px this first carried, a cell was 60px and every name on the
   * calendar truncated to one letter — the panel was there and told you
   * nothing. 672/7 is 96px a cell, which fits "Raquel" beside a 16px face.
   *
   * It is Hub's number too: its cleaning rota gives the calendar 42rem and
   * says why — the month grid would otherwise scroll sideways inside its own
   * column, which is worse than not showing it.
   */
  .ro-views { grid-template-columns: minmax(0, 672px) minmax(0, 1fr); align-items: start; }
  .ro-views > [data-ro='calendario'] { order: 1; }
  .ro-views > [data-ro='lista'] { order: 2; }
  /* Above this width the toggle switches between two things already on
     screen, which is a control that only invites a question. */
  .ro-switch { display: none; }
}

/*
 * ONE PANEL AT A TIME, BELOW THE SPLIT — and driven by an ATTRIBUTE ON THE
 * CONTAINER, not by `hidden` on the panels.
 *
 * The first version put `hidden` on the calendar and tried to override it
 * above 1180. It cannot be overridden: kids-tokens.css carries
 * `[hidden] { display: none !important }` as a deliberate global guard — see
 * the note at `.bo-scrim`, where an author `display` beating the user-agent
 * rule once left an invisible layer over every phone. `!important` wins
 * against any specificity, so the calendar was simply never drawn beside the
 * list. Measured: the media query matched, the grid was two columns wide,
 * and the second column was 0px.
 *
 * So the panels keep no attribute of their own and `.ro-views` says which
 * one is showing. Above 1180 this block does not apply at all and both are
 * drawn, which is the whole arrangement in one rule instead of two fighting.
 *
 * `data-ro`, NOT `data-panel`. Two reasons, and the second is the one that
 * cost an hour: `data-panel` already means "tab panel" in this codebase —
 * the iPad console and the child's profile both use it that way — and
 * bin/checks/skeleton.py injects `[data-panel] ~ [data-panel]{display:none
 * !important}` to mimic a tab strip when it renders a page. So the calendar
 * previewed as a 0px column beside a two-column grid, in the harness only,
 * and the real page was never wrong. Prove the harness before believing it.
 */
@media (max-width: 1179.98px) {
  .ro-views[data-show='lista'] > [data-ro='calendario'],
  .ro-views[data-show='calendario'] > [data-ro='lista'] { display: none; }
}

/* --------------------------------------------------------------------------
   THE LIST IS HUB'S CLEANING ROW, copied rather than reinterpreted.

   Ian, 2026-11-09, twice: "copy the style and function of that one", then
   "you need to copy the same layout/design as the hub cleaning rotation
   please..."

   The first attempt took Hub's SHAPE — a figures strip, a toggle, two panels
   — and then wrote its own row inside it. That was the miss. Hub's row is a
   specific thing and this is it, from `.task-row` in its app.css:

     a 22px BADGE that is a ring when there is nothing to say, a filled tick
     when the day is covered and a filled warning when it is not;
     a TITLE that truncates, with a quieter clause after a middot;
     the FACES of whoever is coming;
     a PILL saying the state in a word;
     a CHEVRON that turns over;
     and a DETAIL that grows a grid track from 0fr to 1fr — no height is
     measured anywhere, which is what keeps the whole thing a class toggle.

   The values are Hub's, resolved into this app's tokens: 14px radius, a
   zinc-800 inset ring instead of a border, 12px padding, 10px gaps, a 22px
   badge, 14px/500 title, an 11px pill. The stagger is Hub's too — each row
   enters a beat after the one above, so the list assembles rather than
   appearing.
   -------------------------------------------------------------------------- */

.task-row {
  border-radius: 14px;
  background: var(--surface);
  /* An inset ring rather than a border: a border would add to the box and
     put every row a pixel out from the calendar beside it. */
  box-shadow: inset 0 0 0 1px var(--hairline);
  overflow: hidden;
  animation: task-enter 450ms cubic-bezier(0.23, 1, 0.32, 1) var(--enter, 0ms) both;
}
.ro-panel[data-ro='lista'] { display: grid; gap: 8px; }

@keyframes task-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.task-head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  /* 12px each side and a 44px floor, so the row clears a thumb even on the
     shortest title. Hub's is 12px with no floor; this app puts one under
     every control it has. */
  min-height: 44px;
  padding: 12px;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.task-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Empty when there is nothing to say: a ring is a thing waiting to be
   filled. */
.task-badge {
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: 0 0 auto;
  border-radius: 999px;
  box-shadow: inset 0 0 0 2px var(--hairline-strong);
  color: transparent;
}
.task-badge svg { width: 13px; height: 13px; display: block; }
.task-badge.is-done { background: var(--ok); box-shadow: none; color: var(--sky-top); }
.task-badge.is-late { background: var(--danger); box-shadow: none; color: var(--sky-top); }

.task-title {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px; font-weight: 500; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.task-title.is-past { opacity: .6; }
.task-sub { color: var(--ink-faint); }

/* The face stack. One teacher per Sunday here, so it is one face — the
   class stays because the rule that overlaps them is Hub's and a day with
   two helpers would need nothing new. */
.task-faces { display: flex; flex: 0 0 auto; }
.task-faces > * + * { margin-left: -8px; }

.task-pill {
  flex: 0 0 auto;
  border-radius: 999px;
  padding: 2px 8px;
  font-size: 11px; font-weight: 500;
  color: var(--ink-soft); background: var(--surface-2);
  white-space: nowrap;
}
.task-pill.is-done { color: var(--ok); background: rgb(110 231 183 / 0.12); }
.task-pill.is-late { color: var(--danger); background: rgb(220 38 38 / 0.16); }

.task-chevron {
  flex: 0 0 auto; display: flex; color: var(--ink-faint);
  transition: transform .3s cubic-bezier(0.23, 1, 0.32, 1);
}
.task-chevron svg { width: 16px; height: 16px; display: block; }
.task-row[data-open] .task-chevron { transform: rotate(180deg); }

/*
 * 0fr to 1fr. The row grows to whatever the detail turns out to be, with no
 * height measured anywhere — which is what keeps this to a class toggle.
 */
.task-detail {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .3s cubic-bezier(0.23, 1, 0.32, 1), opacity .3s ease;
}
.task-row[data-open] .task-detail { grid-template-rows: 1fr; opacity: 1; }

.task-detail-inner {
  overflow: hidden;
  display: grid;
  /*
   * `minmax(0, 1fr)`, NOT `1fr`. A grid track's minimum is its content, so a
   * plain 1fr refuses to go narrower than the widest thing in it — and the
   * `overflow: hidden` above, which exists for the 0fr-to-1fr animation,
   * then CLIPS the difference with no way to scroll to it. Hub measured 262
   * unreachable pixels on a phone before it found this.
   */
  grid-template-columns: 22px minmax(0, 1fr);
  gap: 10px;
  padding: 0 12px 12px;
}
/* The line under the badge, tying the detail to the row it belongs to. */
.task-rule { justify-self: center; width: 1px; height: 100%; background: var(--hairline); }

.task-who { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.task-who b { font-size: 14px; font-weight: 600; }
.task-note { margin: 0 0 10px; font-size: 13px; color: var(--ink-soft); }
.task-none { margin: 0 0 10px; font-size: 13px; color: var(--warn); }

/* The face, wherever it appears in a row. The ring is what separates two
   overlapping faces in a stack. */
.face {
  width: 26px; height: 26px; flex: none;
  border-radius: 50%;
  background: var(--accent-wash) center/cover;
  color: var(--accent);
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700;
  box-shadow: 0 0 0 2px var(--surface);
}

@media (prefers-reduced-motion: reduce) {
  .task-row, .task-chevron, .task-detail { animation: none; transition: none; }
}

.cal {
  border: 1px solid var(--hairline);
  border-radius: var(--squircle);
  overflow: hidden;
  background: var(--surface);
}

.cal-week { display: grid; grid-template-columns: repeat(7, 1fr); }

.cal-dow {
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
}
.cal-dow span {
  padding: 8px 0; text-align: center;
  font-size: 11px; font-weight: 700; letter-spacing: 0.07em;
  color: var(--ink-faint);
}

/*
 * A day. Every one is a button — including the greyed days from the
 * neighbouring months, because a service on the 1st is still a service when
 * you happen to be looking at the month before.
 */
.cal-day {
  /* 84 fitted a number and a teacher chip. Two lines of note need a little
     more, and every cell takes it so the rows stay level. */
  min-height: 96px;
  display: flex; flex-direction: column; align-items: stretch; gap: 4px;
  /*
   *  A GRID ITEM DEFAULTS TO `min-width: auto`, which is its content's
   *  min-content width — and a note set `white-space: nowrap` has no
   *  min-content width worth the name. Without these two the seven `1fr`
   *  tracks stop being equal and size themselves to the longest note in
   *  each column: measured 491px / 24px / 223px / 25px / 31px / 29px / 30px
   *  on a month with three notes in it. Both are load-bearing — `min-width`
   *  lets the track win the argument, `overflow` keeps the text inside the
   *  cell it belongs to.
   */
  min-width: 0;
  overflow: hidden;
  padding: 7px 8px;
  border: 0;
  border-right: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.cal-week:last-child .cal-day { border-bottom: 0; }
.cal-day:nth-child(7n) { border-right: 0; }
.cal-day:hover { background: var(--surface-2); }
.cal-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.cal-num { font-size: 12.5px; font-weight: 700; color: var(--ink-soft); }

/* Sundays carry the class, so they read first. */
.cal-day.is-sunday .cal-num { color: var(--accent); }
.cal-day.is-out { opacity: 0.35; }
.cal-day.is-past { opacity: 0.5; }
.cal-day.is-today { background: var(--accent-wash); }
.cal-day.is-today .cal-num { color: var(--accent); }

/*
 *  A note on the day.
 *
 *  ONE line, clipped. The cell is a glance — "which Sundays still need
 *  somebody" is the question this calendar answers, and a note that wraps to
 *  three lines makes every other cell taller to no purpose. The whole text is
 *  in the cell's own aria-label, and in the dialog when it is opened.
 */
.cal-note {
  /*
   *  TWO lines, then clamped.
   *
   *  One line was the first cut and it was useless: a cell is ~112px, which
   *  is about fourteen characters, so "Clase combinada con los grandes"
   *  arrived as "Clase combinad…" — not readable and not decorative, just
   *  broken-looking text. Two lines carries the notes people actually write
   *  ("Llega tarde — empieza 10:15") whole. The full text is on the cell's
   *  title and aria-label either way, and in the dialog when it is opened.
   */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  min-width: 0;
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}
.cal-day.is-today .cal-note { color: var(--ink-soft); }

/* The note box in the day dialog. */
.modal-note { margin: 14px 0 0; }
.modal-note input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.cal-who {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 6px;
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  min-width: 0;
}
.cal-day.is-filled.is-sunday .cal-who { background: var(--accent-wash); }

.cal-face {
  width: 20px; height: 20px; flex: none;
  border-radius: 50%;
  background: var(--hairline) center/cover;
}
.cal-name {
  font-size: 12px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 700px) {
  .cal-day { min-height: 64px; padding: 5px; }
  .cal-name { display: none; }
}

/* --------------------------------------------------------------------------
   The day picker
   -------------------------------------------------------------------------- */

.modal {
  width: min(420px, calc(100vw - 32px));
  padding: 20px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle);
  background: var(--sky-top);
  color: var(--ink);
}
.modal::backdrop { background: rgb(0 0 0 / 0.55); }

.modal-title { margin: 0; font-size: 19px; font-weight: 800; letter-spacing: -0.02em; }
.modal-sub { margin: 3px 0 14px; color: var(--ink-soft); font-size: 13.5px; }

.modal-list { display: grid; gap: 4px; max-height: 46vh; overflow-y: auto; }

/*
 * THE BUTTONS AT THE FOOT OF A DIALOG.
 *
 * There was no rule for this at all — the markup carried `.modal-acts` and
 * the class did nothing, so Cerrar sat on its own at the left of a 620px
 * dialog looking like something that had come loose. Ian, 2026-11-09: "the
 * preview modal is also not good looking".
 *
 * Right-aligned, because that is where a dialog's confirm sits in this app's
 * other dialogs and in Hub. `flex-wrap` so two buttons and a long label do
 * not run past the edge at 375px.
 */
.modal-acts {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: 8px; margin-top: 16px;
}

/* A radio row in the rota's dialog. The radio itself is hidden and the whole
   row is the control, so the ROW is what has to clear 44px — it was 46 on a
   name and 44 on "Nadie" only because the 28px face happened to carry it. */
.pick {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px;
  padding: 8px 10px;
  border-radius: var(--squircle-sm);
  cursor: pointer;
}
.pick:hover { background: var(--surface); }
/* The radio itself is hidden; the row is the control. Keyboard focus still
   lands on it, so the ring has to be drawn on the row instead. */
.pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.pick:has(input:checked) { background: var(--accent-wash); font-weight: 600; }
.pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }

.pick-face {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink-soft);
  font-size: 11px; font-weight: 800;
}
.pick:has(input:checked) .pick-face { background: var(--accent); color: var(--accent-ink); }

.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }

/* --------------------------------------------------------------------------
   Niños
   -------------------------------------------------------------------------- */

.kid-face {
  /*
   * Takes a size. The squircle is the kid twist and stays — this is NOT a
   * second, round avatar beside the existing one, it is the existing one
   * with a knob, so a list row and a page header share a single rule.
   */
  width: var(--face, 62px); height: var(--face, 62px); flex: none;
  display: grid; place-items: center;
  border-radius: 20px;
  background: var(--accent-wash) center/cover;
  color: var(--accent);
  font-size: calc(var(--face, 62px) * 0.34); font-weight: 800; letter-spacing: -0.02em;
  overflow: hidden;
}
/* Radius tracks the size, or a small face reads as a rounded square. */
.kid-face-md { --face: 46px; border-radius: 15px; }
.kid-face-sm { --face: 34px; border-radius: 11px; }

/*
 * A checkbox with its explanation attached.
 *
 * The label matters more than usual on this page: "En la rotación" with a line
 * saying what it actually excludes is the difference between a teacher
 * understanding the setting and calling a regular class member a visitor.
 */
.check {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 11px 0;
  /* The same floor as `.nk-confirm`, and for the same reason: the label is
     the target, the box stays 17. A one-line option here measured 39. */
  min-height: 44px;
  cursor: pointer;
}
.check + .check { border-top: 1px solid var(--hairline); }
.check input { margin-top: 3px; width: 17px; height: 17px; flex: none; accent-color: var(--accent); }
.check b { display: block; font-size: 14px; font-weight: 600; }
.check span span { display: block; color: var(--ink-soft); font-size: 12.5px; margin-top: 2px; max-width: 62ch; }


/* ==========================================================================
   Imágenes
   ========================================================================== */

.pic-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 14px;
  padding: 16px 18px;
}

.pic {
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface);
  overflow: hidden;
}

/*
 * 16:9, the shape of the screen it is going onto. A thumbnail in a different
 * shape from the television quietly hides that a portrait photograph will sit
 * in the middle of a wide screen with air down both sides.
 *
 * IT IS A <button> NOW — it opens the preview dialog — so the user agent's
 * own button styling has to be undone or a border, a background and a default
 * padding all draw over the picture. `width: 100%` because a button does not
 * fill its grid cell the way a span did.
 */
.pic-shot {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  padding: 0;
  background-color: var(--surface-2);
  background-size: cover;
  background-position: center;
  cursor: zoom-in;
}
/* What says it does something. A ring inside the box rather than a border,
   so the picture does not shift by a pixel on hover. */
.pic-shot:hover { box-shadow: inset 0 0 0 2px var(--accent); }
.pic-shot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.pic-body { display: grid; gap: 8px; padding: 11px 12px; }
.pic-body input[name='caption'] {
  width: 100%; height: 34px; padding: 0 10px;
  border-radius: 9px; border: 1px solid var(--hairline);
  background: var(--surface-2); color: var(--ink);
  font: inherit; font-size: 13.5px;
}
/*
 * THE DATE FIELD, WEARING THE SAME CLOTHES AS EVERYTHING ELSE.
 *
 * Ian, 2026-11-09: "can we also make the date box match the other buttons
 * styles please."
 *
 * `.pic-date` was a class in the markup with NO RULE BEHIND IT, so the input
 * inside it was a bare user-agent date control: a different height, a
 * different radius, a white-ish ground and the browser's own calendar glyph,
 * sitting in a card where everything else is this app's own. The same shape
 * of fault as `.modal-acts` earlier today — a class that looks styled in the
 * markup and is not.
 *
 * The values are `.field input`'s, not re-chosen: 44px, `--squircle-sm`,
 * `--hairline-strong` on `--field`. A date input is still the one control
 * whose INSIDES the browser draws, so the indicator is recoloured rather
 * than replaced — `filter: invert` is the only handle Chromium gives on it,
 * and without it the glyph is near-black on a near-black ground.
 */
.pic-date { display: grid; gap: 4px; font-size: 12.5px; color: var(--ink-soft); }
.pic-date input {
  width: 100%;
  height: 44px;
  padding: 0 10px;
  border-radius: var(--squircle-sm);
  border: 1px solid var(--hairline-strong);
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
}
.pic-date input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* Chromium draws the picker glyph itself and ignores `color`. Inverting is
   the only way to lift it off a dark ground; Firefox and Safari ignore this
   pseudo-element entirely and draw a glyph that already follows
   `color-scheme: dark`, which `.bo-body` sets. */
.pic-date input::-webkit-calendar-picker-indicator {
  filter: invert(1);
  opacity: .55;
  cursor: pointer;
}
.pic-date input::-webkit-calendar-picker-indicator:hover { opacity: 1; }

.pic-check { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--ink-soft);
             min-height: 44px; }
.pic-check input { width: 18px; height: 18px; accent-color: var(--accent); }
/* One row at the foot of the card, both buttons on the same baseline and
   both lined up with the card's own 12px gutter. `.pic-remove` is gone with
   the stacked block it wrapped. */
.pic-acts { display: flex; gap: 8px; padding: 0 12px 12px; }
.pic-acts .btn { flex: 1 1 0; }

/* ==========================================================================
   The back office follows the laptop
   ==========================================================================

   Only this surface. The television is light with its drifting seasons
   whatever the operating system thinks, because nobody sets a colour scheme on
   a television; the iPad is light because it is held in a lit classroom. The
   back office is the one thing here opened on somebody's laptop at night, and
   that laptop already knows the answer.

   Tokens only. Every rule in this file and in kids-tokens.css reads from them,
   so this block is the whole of dark mode — and `.bo-body` keeps it off the
   two surfaces that must not follow.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE BACK OFFICE WEARS HUB'S CLOTHES.

   Ian, 2026-11-09: "this new redesign should turn the admin pages to not
   look so childish and match the hubs aesthetic style".

   Three things were doing the childish, and all three are tokens:

     THE GREEN. `--accent` is seasonal — green, then orange for autumn, then
     another green — which is right on the classroom television and on the
     page a visiting family meets, and wrong on a screen where somebody is
     editing a telephone number. Hub's accent is violet-400 and never moves.
     THE RADII. `--squircle: 28px` is the kid twist, written down as such in
     kids-tokens.css. At 28px a table of children reads as a toy. Hub's
     sections are 16px.
     THE LIGHT MODE. The back office followed the system preference, so it
     was white half the time. Hub is dark, always, and two apps the same
     person moves between should not disagree about that.

   TOKENS ONLY, and it REPLACES the light/dark pair that used to live here
   rather than sitting at the end of the file overriding it. One name, one
   place: clashcheck reports `.bo-body` declared twice however far apart the
   two are, and it is right to — the next person to change a colour here
   would have changed the half that no longer applies.

   It reaches `.bo-body` and nothing else. The television, the class sheet,
   the safety cards and the public inscripción form are all outside the back
   office and keep the colours they were designed in.
   -------------------------------------------------------------------------- */
.bo-body {
  color-scheme: dark;

  /* zinc-950, flat. Hub's own ground. */
  --sky-top: #09090b;
  --sky-bottom: #09090b;

  /* SOLID, not a translucent white overlay. Two stacked translucent panels
     came out two different greys against the same background, which is the
     seam you see on a settings page before you can name it. */
  --surface: #18181b;     /* zinc-900 */
  --surface-2: #27272a;   /* zinc-800 */
  --hairline: #27272a;    /* zinc-800 */

  --ink: #f4f4f5;         /* zinc-100 */
  --ink-soft: #a1a1aa;    /* zinc-400 */
  /*
   * NOT Tailwind's zinc-500.
   *
   * This said #71717a with a comment reading "zinc-500", which is what
   * Tailwind ships — and Hub MOVED off it on 2026-10-07 because #71717a
   * fails AA on every ground Hub puts quiet text on. Its own token is
   * `--z-500: 141 141 151`, measured rather than taken from the scale.
   *
   * Found 2026-11-09 by measuring the two sidebars side by side for Ian's
   * "make sure it identical": every geometry number already matched and the
   * subtitle under the wordmark came back rgb(113,113,122) here against
   * rgb(141,141,151) there. It is not only the brand line — `--ink-faint`
   * is every quiet label, caption and placeholder in this back office, so
   * the whole app was one step dimmer than Hub and below AA with it.
   */
  --ink-faint: #8d8d97;   /* Hub's --z-500, not zinc-500 */

  /* violet-400 with zinc-950 on top of it — the exact pair every primary
     button in Hub uses. */
  --accent: #a78bfa;
  --accent-ink: #09090b;
  --accent-wash: rgb(167 139 250 / 0.12);

  --ok: #6ee7b7;          /* emerald-300 */
  --warn: #fcd34d;        /* amber-300  */
  --danger: #fda4af;      /* rose-300   */

  /*
   * HUB'S RADII, read off its templates rather than chosen.
   *
   * Ian, 2026-11-09: "copy the style from our hub too for all our admin
   * pages please it needs to look identical in style shading etc."
   *
   * Hub writes `rounded-lg` — 8px — on cards, buttons and fields alike (58
   * of its 70 card declarations), and keeps `rounded-2xl` for the dashboard's
   * own cards. This file had 16 and 12, which is close enough to read as a
   * near miss rather than a different system.
   *
   * `--squircle` is still the big one and `.sk-card` takes it; everything
   * else now takes `--squircle-sm`, which is Hub's 8. The 28px for the
   * television stays in kids-tokens.css, untouched.
   */
  --squircle: 16px;
  --squircle-sm: 8px;

  /*
   * TWO HAIRLINES, because Hub has two.
   *
   * `border-zinc-800` separates a card from the page; `border-zinc-700`
   * outlines a control you can type in or press. This file used one colour
   * for both, so every input and button in the back office was outlined a
   * step darker than Hub's and read as flatter.
   */
  --hairline-strong: #3f3f46;   /* zinc-700 */

  /*
   * A FIELD IS DARKER THAN THE CARD IT SITS ON, which is the opposite of
   * what this file did.
   *
   * Hub's inputs are `bg-zinc-950` on a `bg-zinc-900` card — a well, not a
   * raised tile. These were `--surface-2` (zinc-800), lighter than the card,
   * so a form read as a stack of buttons.
   */
  --field: #09090b;             /* zinc-950 */
}

/* The preflight pill is told its colour in the dark block above; that block
   now only ever applies to this same surface, so it is said once here. */
.bo-body .pill-on { color: var(--ok); }

/* --------------------------------------------------------------------------
   Inscripciones

   `wv-` here, `wz-` on the public form. Two prefixes on purpose: the two
   pages share nothing but a subject, and a class that drifted between them
   would be edited for one and silently change the other.
   -------------------------------------------------------------------------- */

/* The badge on the nav. */
.bo-badge {
  display: inline-block;
  min-width: 18px;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: center;
  vertical-align: 1px;
}

.wv-link { display: flex; gap: 8px; align-items: center; }
.wv-link input {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  color: var(--ink-soft);
  font-size: 13px;
}

/* The filter row */
.wv-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  /* NO margin of its own. It carried `18px 0 12px`, and 18 is not 24: the
     filter row sat six pixels closer to the card above it than any other
     band on any other page, which is the kind of gap that is only visible
     when you measure it and only wrong because everything else agrees.
     `.bo-page > * + *` owns the rhythm. */
}
.wv-tabs { display: flex; gap: 4px; flex-wrap: wrap; flex: 1; }
/* A filter chip is a control. These were 34px of text with nothing but
   padding holding them up — a pill you had to aim at. `inline-flex` so the
   floor can centre the label instead of stretching the text block. */
.wv-tabs a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 15px;
  border-radius: 999px;
  color: var(--ink-soft);
  font-size: 13.5px;
  font-weight: 650;
  text-decoration: none;
}
.wv-tabs a:hover { background: var(--surface-2); color: var(--ink); }
.wv-tabs a.is-on { background: var(--accent); color: var(--accent-ink); }

.wv-count {
  display: inline-block;
  margin-left: 5px;
  padding: 0 6px;
  border-radius: 999px;
  background: rgb(255 255 255 / 0.25);
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.wv-tabs a:not(.is-on) .wv-count { background: var(--surface-2); color: var(--ink-faint); }

.wv-find { display: flex; gap: 6px; }
/* The search box beside the chips. It was 36px and a 999px pill, so it was
   neither the height nor the shape of any other text field in the app; the
   chips stay pills because a chip is a chip, but a thing you type into
   should look the same everywhere. */
.wv-find input {
  height: 44px;
  padding: 0 14px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
}

/* The queue */
.wv-list { list-style: none; margin: 0; padding: 0; }
.wv-list li + li { border-top: 1px solid var(--hairline); }
.wv-list a {
  display: flex;
  gap: 14px;
  align-items: center;
  flex-wrap: wrap;
  padding: 15px 18px;
  color: inherit;
  text-decoration: none;
}
.wv-list a:hover { background: var(--surface-2); }

.wv-who { flex: 1; min-width: 190px; display: grid; }
.wv-who b { font-size: 14.5px; }
.wv-who span { color: var(--ink-faint); font-size: 12.5px; }

.wv-meta { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; }
.wv-meta time { color: var(--ink-faint); font-size: 12.5px; white-space: nowrap; }
.wv-kids { font-size: 13px; color: var(--ink-soft); white-space: nowrap; }

.wv-todo {
  display: inline-block;
  margin-left: 5px;
  padding: 1px 7px;
  border-radius: 999px;
  /* amber-600 at 14% was the literal here, and it is a DIFFERENT amber from
     `--warn` (amber-300) printed on top of it. Mixed from the token, the
     chip and its text are one colour again. */
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn);
  font-size: 11.5px;
  font-style: normal;
  font-weight: 700;
}
/* Named for how they look, not for one of the things they say.
   `.wv-nophoto` ended up carrying "Sin fotos", "huella distinta" AND
   "Sin inscripción" — three facts under one fact's name. */
.pill-warn { background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); }
.pill-bad  { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }

/* One inscription */
.wv-head { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.wv-contact { margin-top: 5px; font-size: 13.5px; display: flex; gap: 7px; flex-wrap: wrap; }
.wv-contact a {
  color: var(--accent);
  text-decoration: none;
  /*
   * A precaution, not a fix — the 94px that was being cut off this card was
   * `.wv-go` below, and this changed nothing when it was tried first.
   *
   * Kept because it is still right: an email address is one unbreakable
   * token inside a card that is `overflow: hidden`, and the longest in the
   * directory are longer than any test data. `min-width: 0` as well as the
   * wrap, or the flex item never shrinks far enough for the wrap to matter.
   */
  min-width: 0;
  overflow-wrap: anywhere;
}
.wv-contact a:hover { text-decoration: underline; }
.wv-contact span { color: var(--ink-faint); }
.wv-sub { color: var(--ink-faint); font-size: 12.5px; }
.wv-head .wv-sub { display: block; margin-top: 4px; }

/* The roster on the detail page. NOT `.wv-kids` — that is the "3 niños"
   span in the queue, and one name for two things is how the PIN keypad
   ended up wearing the drawing canvas's styles. */
.wv-roster { list-style: none; margin: 0; padding: 0; }
.wv-roster > li { padding: 15px 18px; border-top: 1px solid var(--hairline); }

.wv-kid-top { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; }
.wv-kid-top b { font-size: 14.5px; display: block; }
.wv-kid-top .wv-sub { display: block; margin-top: 2px; }

.wv-linked {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: 14px;
  background: var(--accent-wash);
}
.wv-linked .kid-face { width: 42px; height: 42px; border-radius: 14px; font-size: 16px; }
.wv-linked b { display: block; font-size: 14px; }

.wv-pick-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-top: 10px; }
/* The number `.btn`'s own min-height already forces. Said out loud so the
   next person does not read 40 and believe it. */
.wv-pick-row .btn { height: 44px; }

/* ---------------------------------------------------------------------------
   Choosing which child this is.

   A <select> and an Enlazar button until 2026-11-01. One line of text per
   candidate meant telling two children of the same name apart required
   opening both fichas somewhere else — "i have to constantly go back and
   forth to compare info". Each candidate is now its own button carrying the
   face, the birthday and whether they are already in the class, so the thing
   you compare and the thing you press are one object.
   --------------------------------------------------------------------------- */
.wv-picks { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.wv-picks-lead { margin: 0; font-size: 13px; color: var(--ink-faint); }

/* A row that holds a face, two lines of type and an ordinary button — the
   same shape /admin/ninos uses. Not a giant custom button: btncheck is right
   that a second button height is a mess, and the information is what had to
   come onto this screen, not a new control. */
.wv-pick {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.wv-face {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: 999px; object-fit: cover;
}
.wv-face-none {
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--ink-faint);
  font-weight: 600; font-size: 16px;
}

.wv-pick-who { flex: 1 1 auto; min-width: 0; }
.wv-pick-name { display: block; font-weight: 600; }
.wv-pick-sub { display: block; margin-top: 2px; font-size: 12px; color: var(--ink-faint); }

.wv-pick-new { margin: 2px 0 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wv-pick-hint { font-size: 12px; color: var(--ink-faint); }

.pill-ok { background: var(--accent-wash); color: var(--accent); }

.wv-none { margin: 10px 0 0; color: var(--ink-faint); font-size: 13px; }
.wv-none a { color: var(--accent); }

/* Facts, as a real definition list: the label IS the question asked. */
.wv-facts {
  display: grid;
  grid-template-columns: minmax(130px, auto) 1fr;
  gap: 10px 18px;
  margin: 14px 0 0;
  font-size: 13.5px;
}
.wv-facts-tight { gap: 7px 18px; margin-top: 18px; }
.wv-facts dt { color: var(--ink-faint); font-size: 12.5px; font-weight: 650; padding-top: 1px; }
.wv-facts dd { margin: 0; min-width: 0; }
.wv-alert { color: var(--danger); }
.wv-hash, .wv-agent { word-break: break-all; font-size: 11.5px; color: var(--ink-faint); }

.wv-sign-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  margin-top: 12px;
}
@media (min-width: 760px) {
  .wv-sign-grid { grid-template-columns: 1fr 1.2fr; align-items: start; }
}

.wv-signed {
  display: block;
  margin-top: 4px;
  font-size: 20px;
  letter-spacing: -0.01em;
}

/*
 *  The signature, on paper.
 *
 *  A white plate under it whatever the theme: the PNG is drawn with dark ink
 *  on nothing, so on a dark back office it would be ink on ink — invisible,
 *  and invisible in the one place somebody is checking that a signature
 *  exists at all.
 */
.wv-ink { margin: 0; }
.wv-ink img {
  display: block;
  width: 100%;
  max-width: 460px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: #fff;
}
.wv-ink figcaption { margin-top: 6px; color: var(--ink-faint); font-size: 12px; }

.wv-note-row { display: flex; gap: 8px; }
/* 44, with the button it shares a line with. It was 40 against a 38px
   button and the pair looked level by accident; once the button went to 44
   the two centres were 2px apart, which rowcheck measured and reported. */
.wv-note-row input {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
}

.wv-acts {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  border-top: 1px solid var(--hairline);
}

/* --------------------------------------------------------------------------
   Botones (Stream Deck)
   -------------------------------------------------------------------------- */

.pk-warn {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: var(--squircle-sm);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 26%, transparent);
  color: var(--ink-soft);
  font-size: 12.5px;
}
.pk-warn b { color: var(--ink); }

.pk-list { list-style: none; margin: 0; padding: 0; }
.pk-list li {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  padding: 13px 18px;
}
.pk-list li + li { border-top: 1px solid var(--hairline); }
.pk-list li.is-off { opacity: 0.5; }

/* The key number, drawn like a key. */
.pk-num {
  flex: none;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: var(--squircle-sm);
  background: var(--ink);
  color: var(--sky-top);
  font-size: 15px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.pk-list li.is-off .pk-num { background: var(--ink-faint); }

.pk-what { flex: 1; min-width: 180px; display: grid; gap: 1px; }
.pk-what b { font-size: 14.5px; }
.pk-what span { color: var(--ink-soft); font-size: 12.5px; }
.pk-where { color: var(--ink-faint) !important; font-size: 11.5px !important; }
.pk-broken { color: var(--danger) !important; font-weight: 650; }

.pk-url {
  flex: 1;
  min-width: 190px;
  max-width: 380px;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  color: var(--ink-faint);
  font-size: 11.5px;
}

.pk-acts { display: flex; gap: 6px; flex-wrap: wrap; }

/* --------------------------------------------------------------------------
   THE BUTTON FORM — a CONTAINER query, not a viewport one.

   The breakpoints here used to be `@media (min-width: …)`, and the result was
   a form that got NARROWER as the window got WIDER. Measured field widths:

     viewport 1000 → 165px each
     viewport 1040 → 119px      ← the fifth column arrives
     viewport 1100 → 137px
     viewport 1200 → 169px
     viewport 1440 → 246px

   A media query asks the WINDOW how wide it is. This grid does not live in
   the window: it lives inside a card, inside `.bo-page`, inside `.bo-main`,
   which sits in a grid column 224px narrower than the window whenever the
   sidebar is expanded — and 52px narrower when it is railed, which is a
   different answer again for the same window. So "there is room for five
   columns" was being decided by a measurement of something else, and at 1040
   the fifth column arrived into a box that had 816px in it.

   `@container` asks the BOX. `.card` declares `container-type: inline-size`
   and these two rules read its width, so the form adds a column exactly when
   its own container can hold one — with the sidebar open, with it railed, and
   on a phone, all from the same two numbers.

   `inline-size`, not `size`: a `size` container must have its BLOCK size
   constrained too, which would make every card in this back office a fixed
   height. Inline-only containment has no such requirement.

   Found by the 2026-11-09 sweep, which also measured the same mismatch in
   `.wv-sign-grid` (760), `.st-head` (720) and `.kid-grid` (1000). Those
   three are still workable at every width their pages reach; this one was
   not, so this one moved.
   -------------------------------------------------------------------------- */

.pk-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  /* Beyond this the five controls are just far apart. The card still runs the
     full width; the form inside it does not have to. */
  max-width: 1180px;
}

/*
 * The numbers are the CARD's width now, so they are smaller than the old
 * viewport ones by roughly the sidebar plus the page's own padding: a 1040px
 * window with the sidebar open leaves about 750px inside the card, which is
 * where the five-column layout actually starts to fit.
 */
@container (min-width: 560px) {
  /* 132px, not 110: "Número de tecla" wrapped to two lines at the old width,
     which made that field taller than its neighbours and put its label on a
     different line from all of them. */
  .pk-grid { grid-template-columns: 132px 1fr 1fr 1fr; align-items: end; }
  .pk-grid .field { margin-bottom: 0; }
  /* And it must not wrap again if a label is ever reworded. */
  .pk-grid > .field > label { white-space: nowrap; }
}

@container (min-width: 820px) {
  /* Five fields on one line once there is room; below that the auto-clear
     drops to its own row rather than squeezing the other four.
     The FIXED column is the last one, not the fourth: "Se quita solo" holds
     "No, se queda" and never needs more, while "Nombre" is free text and was
     the one being squeezed to 150px. */
  .pk-grid { grid-template-columns: 132px 1fr 1fr 1fr 165px; }
}

/* The auto-clear note, on its own row under the five fields.
   Inside its field it made that column taller than the rest, and
   `align-items: end` lifted the select above the row it belonged to. */
.pk-hint { grid-column: 1 / -1; margin-top: -4px; }

.pk-needs {
  display: grid;
  gap: 12px;
  margin-top: 14px;
  padding: 14px;
  border: 1px dashed var(--hairline);
  border-radius: var(--squircle-sm);
  /* The same cap as .pk-grid, so the parameter row lines up with the fields
     it belongs to instead of running the whole width of the card. */
  max-width: 1180px;
}
@media (min-width: 700px) {
  .pk-needs { grid-template-columns: 1fr 160px auto; align-items: end; }
  .pk-needs .field { margin-bottom: 0; }
}
.pk-needs[hidden] { display: none; }

/* --------------------------------------------------------------------------
   The sidebar's icons

   Hub centres a 16px glyph inside a 20px box (`h-5 w-5` holding `h-4 w-4`),
   so the icon column is 20 wide and the glyphs inside it are 16 — which is
   what keeps a row's text at the same x whatever the glyph's own aspect is.
   This was a bare 16px box, so every label sat 4px left of Hub's.

   A FIXED size on both, because an svg with no width in a flex row collapses
   to nothing in Safari and leaves the labels stepping in and out by a few
   pixels as you move down the list.
   -------------------------------------------------------------------------- */

.bo-ico {
  flex: none;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  color: var(--ink-faint);
}
.bo-ico svg { width: 16px; height: 16px; display: block; }

/* The label takes the rest, and the badge keeps its place at the end. */
.bo-nav-label { flex: 1 1 auto; min-width: 0; overflow: hidden;
                white-space: nowrap; text-overflow: ellipsis; }

/* The icon leads the colour change rather than following it: on the current
   section it goes full accent while the label is only ink, which is what
   makes the eye land on the icon column. */
.bo-nav a:hover .bo-ico { color: var(--ink-soft); }
.bo-nav a[aria-current='page'] .bo-ico { color: var(--accent); }

/* --------------------------------------------------------------------------
   The sidebar, in groups

   Ten flat links had no shape. The groups are <details>, so the disclosure,
   the keyboard and the state come from the element rather than from script —
   kids-admin.js only makes the choice survive the next page load.

   The metrics are Hub's `.nav-group-head`: a 28px head, 11px/600 at .06em,
   and 6px between groups. They were 24-ish (built from padding), 700 at
   .07em, and 14px apart — close enough to look like a near miss rather than
   a different system, which is the worst of both.
   -------------------------------------------------------------------------- */

/* `.bo-nav` itself is declared with the shell above — the groups only add
   the spacing between them. */
.bo-group + .bo-group,
.bo-group:first-of-type { margin-top: 6px; }

.bo-group-title {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 8px 0 10px;
  color: var(--ink-faint);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
  border-radius: 8px;
  transition: color 140ms ease;
  /* The default marker is a black triangle that ignores `color`, and on
     Safari it sits outside the padding box. Our own caret is below. */
  list-style: none;
}
.bo-group-title::-webkit-details-marker { display: none; }

/* COLOUR ONLY on hover, like Hub — no background. A heading that lit up a
   block of its own competed with the gliding highlight directly below it. */
.bo-group-title:hover { color: var(--ink-soft); }
.bo-group-title:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  color: var(--ink-soft);
}

.bo-group-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.bo-caret {
  width: 13px;
  height: 13px;
  flex: none;
  opacity: 0.7;
  transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* Hub's half-turn on a down-pointing chevron. `[open]` is on the <details>,
   so the caret is reached through it rather than from the summary's own
   state. */
.bo-group[open] > .bo-group-title .bo-caret { transform: rotate(180deg); }

/* The nav's rows sit 1px apart; rows inside a group are a level down and
   would otherwise sit flush against each other and the summary. */
.bo-group > a { margin-top: 1px; }

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

/* --------------------------------------------------------------------------
   Panel — the dashboard
   -------------------------------------------------------------------------- */

.db-alert {
  display: block;
  margin-bottom: 16px;
  text-decoration: none;
  color: inherit;
}

/* --------------------------------------------------------------------------
   PARA RESOLVER — the band that interrupts.

   One place for everything somebody has to act on, above the figures. The
   dot carries the severity, so a row can be read before it is read; rose is
   a child in the room without a signed form or a Sunday with nobody in front
   of them, amber is work waiting rather than something wrong.

   The ROW is the link, like every list in Hub.
   -------------------------------------------------------------------------- */
.db-todo {
  display: grid;
  gap: 1px;
  margin-bottom: 16px;
  padding: 6px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface);
}
.db-todo-head {
  margin: 6px 9px 4px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.db-todo-row {
  display: flex;
  align-items: center;
  gap: 10px;
  /* 44px, like every other control in the back office. */
  min-height: 44px;
  padding: 7px 9px;
  border-radius: 10px;
  color: inherit;
  text-decoration: none;
}
.db-todo-row:hover { background: var(--surface-2); }
.db-todo-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.db-todo-dot {
  width: 8px; height: 8px; flex: 0 0 auto;
  border-radius: 50%;
  background: currentColor;
}
.db-todo-bad .db-todo-dot { color: var(--danger); }
.db-todo-warn .db-todo-dot { color: var(--warn); }
.db-todo-words { display: grid; gap: 1px; min-width: 0; }
.db-todo-words b { font-size: 13.5px; font-weight: 700; }
.db-todo-words small {
  font-size: 12px;
  color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.db-todo-row .db-chev { margin-left: auto; color: var(--ink-faint); }

.db-figures {
  display: grid;
  /* auto-fit rather than a fixed four: at 860px the sidebar goes to the top
     and this is the full width, where four columns are 180px of nothing.
     The floor is 150px rather than 190px so a 375px phone gets two across
     instead of four stacked — at 190px the four cards were 440px of scrolling
     before the page's first sentence. */
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.db-fig {
  display: grid;
  gap: 2px;
  padding: 14px 15px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  /* The grid row would otherwise size to the content and let a long figure
     push its neighbours out of line. */
  min-width: 0;
  /* The LABEL sits above the NUMBER now, the way every figures band in Hub
     reads: the eye lands on the word that says what it is about to count.
     `start` so a card with a short note keeps its figure at the top rather
     than floating it in the middle of a taller neighbour. */
  align-content: start;
}
.db-fig:hover { border-color: var(--accent); }
.db-fig:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/*
 * The figure, and the word above it.
 *
 * `display: block` on both, which does nothing inside `.db-fig` — a grid
 * blockifies its children anyway — and is what lets the pair be used on
 * their own. The bank's "Total en circulación" is one figure in a card
 * rather than a band of four, and it was hand-rolled inline with a
 * hard-coded green; it reads from these now instead of growing a second
 * copy of them under a `bank-` name.
 */
.db-fig-n {
  display: block;
  font-size: 30px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.db-fig-n small { font-size: 17px; font-weight: 600; color: var(--ink-faint); }
/* Rose, and only when this figure is the thing to act on — the same rule the
   Hub health page's "Need attention" uses. A band where every number is
   coloured is a band with no colour in it. */
.db-fig-bad { color: var(--danger); }
.db-fig-l {
  display: block;
  font-size: 11px; font-weight: 800; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-faint);
}
.db-fig-s { font-size: 12px; color: var(--ink-faint); }
.db-warn { color: var(--warn); font-weight: 700; }

.db-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px;
  align-items: start;
}

.db-card { padding: 0; overflow: hidden; }

.db-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 13px 15px;
  border-bottom: 1px solid var(--hairline);
}
/* 600, not 700 — the same weight `.card-title` carries, measured off the same
   reference. The dashboard had the size right and the weight one step heavy,
   so its card titles read bolder than the identical titles on every other
   page of the back office. */
.db-card-head h2 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.375; }

.db-sundays { margin: 0; padding: 6px; list-style: none; display: grid; gap: 1px; }
.db-sundays li {
  display: grid;
  /* A fixed date column so the names line up; minmax(0,1fr) on the rest so a
     long name ellipsises instead of widening the card. */
  grid-template-columns: 62px minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 10px;
  padding: 7px 9px;
  border-radius: 9px;
  font-size: 13.5px;
}
.db-sundays li.is-next { background: var(--surface-2); }
.db-date { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.db-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.db-empty { color: var(--warn); font-weight: 700; }
.db-note {
  grid-column: 2;
  font-size: 12px;
  font-style: italic;
  color: var(--ink-faint);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.db-none { margin: 0; padding: 22px 15px; color: var(--ink-faint); font-size: 13px; }

/* The bars. A grid of equal columns, each one a three-row stack: count on
   top, bar in the middle, week underneath. The MIDDLE row is the only one
   that stretches, so every bar is measured from the same baseline. */
.db-bars {
  margin: 0;
  padding: 14px 15px 12px;
  list-style: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  /* Bottom-aligned, so every week's label lines up and the bars grow from a
     common baseline. */
  align-items: end;
  gap: 7px;
}
/* Bars share a baseline because the COLUMNS are bottom-aligned, not because
   anything is stretched to a fixed track: each bar carries its own height in
   pixels and the row is as tall as the tallest one needs. */
.db-bars li {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
  min-width: 0;
}

.db-bar-n { font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.db-bar {
  width: 100%;
  max-width: 26px;
  /* `flex: none` is load-bearing: a flex item's default `flex-shrink: 1`
     would squash the tallest bars to make the column fit and the heights
     would stop meaning anything. */
  flex: none;
  border-radius: 5px 5px 2px 2px;
  background: var(--accent);
}
.db-bar-l { font-size: 10.5px; color: var(--ink-faint); white-space: nowrap; }

.db-health {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 12px;
  padding: 11px 14px;
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: 13px;
  text-decoration: none;
}
.db-health:hover { border-color: var(--ink-faint); }
.db-health b { color: var(--ink); }
.db-health span:nth-child(2) { flex: 1; min-width: 0; }
.db-chev { color: var(--ink-faint); font-size: 17px; line-height: 1; }

.db-dot { width: 9px; height: 9px; flex: none; border-radius: 50%; background: var(--ok); }
.db-health-warn .db-dot { background: var(--warn); }
.db-health-fail .db-dot { background: var(--danger); }
.db-health-fail { border-color: var(--danger); }

/* --------------------------------------------------------------------------
   Insights — the carousel on the dashboard

   Hub's `_insight-cards.php` in this app's clothes. Hub is Tailwind and this
   is hand-written, so nothing is shared but the anatomy: a sentence, then a
   labelled card with one big figure, then the shape behind it.

   TONE drives colour through `data-tone` on the article, so the delta, the
   sparkline and the gradient stop all change together and cannot get out of
   step with one another.
   -------------------------------------------------------------------------- */

.ins {
  display: flex;
  flex-direction: column;
  padding: 14px 15px 15px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--surface);
  /* The grid cell it sits in is as tall as the Sundays list beside it. */
  height: 100%;
  min-width: 0;
}

.ins-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ins-title { display: flex; align-items: baseline; gap: 7px; }
.ins-title b { font-size: 14px; font-weight: 700; }
.ins-pos { font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

.ins-ctrl { display: flex; align-items: center; gap: 2px; }
.ins-ctrl button {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}
.ins-ctrl button:hover { background: var(--surface-2); color: var(--ink); }
.ins-ctrl button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ins-ctrl svg { width: 15px; height: 15px; display: block; }

/* The card grows so the panel keeps one flush bottom edge whichever page is
   showing. A min-height only where there is room for it: pages differ in
   height and without this the page below jumps on every arrow press. */
.ins-card {
  display: flex;
  flex-direction: column;
  flex: 1;
  /* TONE, in one place. The delta, the sparkline, the gradient stop and the
     marker all read this, so they cannot get out of step with each other. */
  --ins-ink: var(--accent);
}
.ins-card[data-tone='up'] { --ins-ink: var(--ok); }
.ins-card[data-tone='down'] { --ins-ink: var(--danger); }

@media (min-width: 1100px) {
  .ins-card { min-height: 320px; }
}

.ins-prose {
  margin: 11px 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-soft);
  flex: none;
}
.ins-prose b { color: var(--ink); font-weight: 700; }

.ins-box {
  display: flex;
  flex-direction: column;
  flex: 1;
  margin-top: 11px;
  overflow: hidden;
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--surface-2);
  /* Flex items refuse to shrink below their content without this, and a wide
     chart would push the panel past its column. */
  min-height: 0;
  min-width: 0;
}

.ins-box-head {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 11px;
  border-bottom: 1px solid var(--hairline);
}
.ins-label { font-size: 11.5px; color: var(--ink-faint); }
.ins-badge {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: 10px;
  font-weight: 600;
}

.ins-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 9px 11px 11px;
  min-height: 0;
  min-width: 0;
}

.ins-figure { display: flex; flex: none; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.ins-value {
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.ins-delta { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.ins-note { font-size: 11.5px; color: var(--ink-faint); }

/* One place decides what a tone looks like. */
.ins-card[data-tone='up'] .ins-delta { color: var(--ok); }
.ins-card[data-tone='down'] .ins-delta { color: var(--danger); }

.ins-chart { display: block; width: 100%; flex: 1; min-height: 86px; margin-top: 11px; }
.ins-line { stroke: var(--ins-ink); }
.ins-line-b { stroke: var(--ink-faint); }
.ins-stop-a { stop-color: var(--ins-ink); stop-opacity: .26; }
.ins-stop-b { stop-color: var(--ins-ink); stop-opacity: 0; }
.ins-mark-rule { stroke: var(--ins-ink); stroke-opacity: .35; }
.ins-mark { fill: var(--ins-ink); stroke: var(--surface-2); }

.ins-key { display: flex; flex: none; flex-wrap: wrap; gap: 4px 14px; margin-top: 9px; font-size: 11.5px; }
.ins-key span { display: inline-flex; align-items: center; gap: 6px; }
.ins-key i { width: 15px; height: 2px; border-radius: 999px; }
.ins-key-a { color: var(--ink-soft); }
.ins-key-a i { background: var(--ins-ink); }
.ins-key-b { color: var(--ink-faint); }
.ins-key-b i { background: var(--ink-faint); }

.ins-bars {
  display: flex;
  flex: 1;
  align-items: flex-end;
  gap: 4px;
  margin-top: 11px;
  min-height: 86px;
}
/* NOTE: there is deliberately no `.ins-bars span` rule. The bars used to be
   bare spans; they are `.ins-bar-col > i` now, and a leftover span rule here
   painted the whole column, so every bar looked full height whatever number
   it carried. */

.ins-split { display: flex; flex: 1; flex-direction: column; justify-content: center; gap: 11px; margin-top: 11px; }
.ins-part-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12px; }
.ins-part-name { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--ink-soft); }
.ins-part-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ins-part-name i { width: 8px; height: 8px; flex: none; border-radius: 50%; }
.ins-part-pct { flex: none; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.ins-track { height: 7px; margin-top: 5px; border-radius: 999px; background: var(--hairline); overflow: hidden; }
.ins-track span { display: block; height: 100%; border-radius: 999px; }

/* Five swatches, in the order a split chart uses them. The last is deliberately
   grey: by the fifth row the name matters more than the colour. */
.ins-part[data-k='0'] i, .ins-part[data-k='0'] .ins-track span { background: var(--accent); }
.ins-part[data-k='1'] i, .ins-part[data-k='1'] .ins-track span { background: #38bdf8; }
.ins-part[data-k='2'] i, .ins-part[data-k='2'] .ins-track span { background: #a78bfa; }
.ins-part[data-k='3'] i, .ins-part[data-k='3'] .ins-track span { background: var(--warn); }
.ins-part[data-k='4'] i, .ins-part[data-k='4'] .ins-track span { background: var(--ink-faint); }

.ins-link {
  align-self: flex-start;
  flex: none;
  margin-top: 11px;
  padding: 5px 12px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  color: var(--ink-soft);
  font-size: 12px;
  text-decoration: none;
}
.ins-link:hover { background: var(--surface-2); color: var(--ink); }

/* A bar column: the bar, and optionally a date under it. The COLUMN is the
   flex child so the label sits below a bar that grows from the baseline. */
.ins-bar-col { display: flex; flex: 1; flex-direction: column; justify-content: flex-end;
               align-items: center; gap: 4px; min-width: 0; height: 100%; }
.ins-bar-col i {
  display: block;
  width: 100%;
  border-radius: 4px 4px 2px 2px;
  background: var(--ins-ink);
  background: linear-gradient(to top,
              color-mix(in srgb, var(--ins-ink) 28%, transparent), var(--ins-ink));
}
/* Good and not-good, independent of the card's tone. */
.ins-bar-col i[data-on='1'] { background: var(--ok); }
.ins-bar-col i[data-on='0'] { background: var(--danger); }
.ins-bar-col em {
  font-style: normal;
  font-size: 9.5px;
  color: var(--ink-faint);
  white-space: nowrap;
  /* Eight dates across a narrow card: let them clip rather than widen it. */
  overflow: hidden;
  max-width: 100%;
}

/* Turnstile on the sign-in card. Cloudflare sizes its own iframe. */
.si-turnstile { margin: 4px 0 14px; min-height: 65px; }

/* --------------------------------------------------------------------------
   The phone menu

   Above 860px nothing here applies: the sidebar is a column of the grid and
   the bar, the scrim and the close button are all display:none — and so out
   of the tab order, which `visibility` or opacity alone would not do.

   Below it the sidebar becomes a drawer. It stays in the SAME place in the
   DOM so there is one navigation, not two that can disagree.
   -------------------------------------------------------------------------- */

.bo-top,
.bo-scrim,
.bo-close { display: none; }

@media (max-width: 860px) {
  .bo-top {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    /* env() so an installed iOS app draws under the status bar without its
       own content sitting under the clock. 0px fallback everywhere else. */
    padding-top: calc(9px + env(safe-area-inset-top, 0px));
    border-bottom: 1px solid var(--hairline);
    background: var(--surface-2);
  }

  .bo-top-name { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .bo-top-name b { font-size: 14.5px; }
  /*
   * The mark is NOT restyled here, only resized — and it stays SQUARE.
   *
   * This used to set `width: 30px; height: 22px; display: block;
   * color: var(--accent)`, which broke it three ways at once: `.bo-mark` is a
   * green rounded TILE with the wordmark inside it, so a 30x22 box made the
   * tile itself an oblong; `display: block` threw away the grid centring that
   * holds the wordmark in the middle; and painting the mark the accent colour
   * put a green wordmark on a green tile. The result was a squished green
   * blob with nothing in it — reported 2026-09-13.
   */
  .bo-top .bo-mark { width: 30px; height: 30px; border-radius: 10px; }

  /* The drawer itself. */
  .bo-side {
    position: fixed;
    z-index: 40;
    inset: 0 auto 0 0;
    /* The insets decide the height here, not the desktop rule's 100svh —
       stated so the two cannot disagree on an iOS address bar, where the
       fixed-position box and the small viewport are different heights. */
    height: auto;
    width: min(290px, 84vw);
    padding-top: calc(14px + env(safe-area-inset-top, 0px));
    border-right: 1px solid var(--hairline);
    border-bottom: 0;
    overflow-y: auto;
    /*
     * OPAQUE, which the sidebar is not.
     *
     * Every surface token in this design is translucent — they are meant to
     * be layered over the page's own gradient — so a drawer that inherits
     * `--surface-2` floats over the page and you read both at once. The
     * gradient stop here is the same tint, composited over a solid ground,
     * so it looks exactly like the column it is and hides what is behind it.
     */
    background: linear-gradient(var(--surface), var(--surface)), var(--sky-top);
    box-shadow: 0 0 40px rgb(0 0 0 / .3);
    /* Off-canvas rather than display:none, so it can slide. `visibility`
       alongside it is what keeps the links out of the tab order while it is
       shut — a transform alone leaves them focusable off-screen, and tabbing
       lands you on a link you cannot see. */
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform .22s ease, visibility 0s linear .22s;
  }

  [data-shell].is-open .bo-side {
    transform: none;
    visibility: visible;
    transition: transform .22s ease, visibility 0s;
  }

  /*
   * The scrim, and the bug it caused.
   *
   * This said `display: block` here while the script set the `hidden`
   * ATTRIBUTE to put it away. `[hidden] { display: none }` is a USER-AGENT
   * rule, and any author `display` beats it — so on every phone an invisible
   * full-screen layer sat over the whole app and swallowed every tap.
   * Reported 2026-09-13: "the buttons don't seem to be working".
   *
   * One source of truth now: the `is-open` class, the same one the drawer
   * uses. No `hidden` attribute anywhere near it. `visibility` keeps it out
   * of hit-testing and the tab order while still allowing the fade, exactly
   * as it does for the drawer; `pointer-events` says the same thing a second
   * way, because this must not come back.
   */
  .bo-scrim {
    position: fixed;
    inset: 0;
    z-index: 35;
    display: block;
    border: 0;
    padding: 0;
    background: rgb(8 12 14 / .55);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity .22s ease, visibility 0s linear .22s;
  }
  [data-shell].is-open .bo-scrim {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity .22s ease, visibility 0s;
  }

  /*
   * 44, like everything else. It was a 32px box, which is the one control in
   * this layout that only ever appears on a PHONE — the drawer is not there
   * on a laptop — so the smallest target in the back office was the one
   * nothing but a thumb would ever press.
   *
   * 6px from the corner rather than 12 keeps the glyph exactly where it was:
   * a 32px box inset 12 and a 44px box inset 6 share a centre.
   */
  .bo-close {
    display: grid;
    place-items: center;
    position: absolute;
    top: calc(6px + env(safe-area-inset-top, 0px));
    right: 6px;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: var(--squircle-sm);
    background: none;
    color: var(--ink-faint);
    cursor: pointer;
  }
  .bo-close svg { width: 17px; height: 17px; }
  .bo-close:hover { background: var(--surface); color: var(--ink); }
  .bo-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

  /*
   * Bigger targets on a phone than on a laptop — 42px, and stated as a
   * HEIGHT now. It was `padding: 11px 10px`, which worked only while the
   * desktop row was built from padding too; the desktop row sets
   * `height: 36px` to match Hub, and a height always beats padding.
   */
  .bo-nav a { height: 42px; padding: 0 10px; }
}

/*
 * THE SAME FLOOR, for the bar — because below 861px the BAR is what sits
 * against the status bar, not the page.
 *
 * `.bo-top` above already adds `env(safe-area-inset-top, 0px)`, and on a
 * notched iPhone that is the right number and enough. On an iPad it is not:
 * an iPad reports little or nothing there and iOS still draws its clock over
 * the top of the window, so "MDV Kids" came up underneath it — and an iPad
 * in PORTRAIT is 810–834pt, which is this side of the 860px line, so it is
 * the bar's problem and not `.bo-main`'s.
 *
 * A separate block rather than an edit to the rule above, and this is the
 * reason: that rule also runs in a phone BROWSER, where `env()` is 0 and
 * 9px is correct. A floor written into it would put 30px of dead space at
 * the top of every phone tab. Scoped to `standalone` it cannot, and scoped
 * to `max-width: 860px` it cannot double up with `.bo-main` either — the two
 * rules are on opposite sides of the same breakpoint.
 *
 * It must come after the block above to win: same selector, same
 * specificity, so source order is what decides.
 */
@media (display-mode: standalone) and (max-width: 860px) {
  .bo-top { padding-top: calc(9px + max(env(safe-area-inset-top, 0px), 30px)); }
}

@media (max-width: 860px) and (prefers-reduced-motion: reduce) {
  .bo-side, .bo-scrim { transition: none; }
}

/* --------------------------------------------------------------------------
   The picture upload row

   Its own grid. `.form-row` is five columns because the bank's form needs
   five; this has two fields and a button, so under that rule they fell into
   arbitrary columns and came out at wildly different widths — on a wide
   screen the file field ran half the page while its label sat on a different
   line from the one beside it.
   -------------------------------------------------------------------------- */

/*
 * TWO COLUMNS, not three — the name field is gone.
 *
 * Ian, 2026-11-09: "remove the having to name an image please its annoying
 * and no one uses that." This was `1fr 1fr auto` for file, name and button;
 * with the middle one removed the BUTTON inherited the spare 1fr and came
 * out a 240px violet bar. A submit is `auto` and the file field takes the
 * rest.
 */
.up-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 12px 14px;
  /* The field and the button are the same height, so bottom-aligning puts
     them on one line. */
  align-items: end;
  /* A form is not a billboard. Beyond this it is just a long way for the eye
     to travel between a file and the button that sends it. */
  max-width: 620px;
}
.up-row .field { margin-bottom: 0; }
/* As `.wv-pick-row .btn`: the number `.btn` already enforces, written down
   rather than left at a 40 that is silently overridden. */
.up-row .btn { height: 44px; }
.up-hint { grid-column: 1 / -1; margin-top: -2px; }

@media (max-width: 720px) {
  .up-row { grid-template-columns: minmax(0, 1fr); }
  .up-row .btn { justify-content: center; }
}

/* --------------------------------------------------------------------------
   File inputs

   There was no rule for these at all, so `.field input` gave one a 40px box
   with 12px of padding and `appearance: none` — and the browser drew its own
   grey "Choose File" button inside it, in its own typeface, against a box
   that looked empty. Every other control on these pages is styled; this was
   the one that was not.
   -------------------------------------------------------------------------- */

.field input[type='file'] {
  /* The button provides the left inset; padding here would double it. */
  padding: 0;
  cursor: pointer;
  /* A long filename must not be able to widen the grid column. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--ink-soft);
}

/* Both spellings: the standard one, and WebKit's older name for the same
   thing. Safari understood only the second until fairly recently, and a file
   button that is styled in Chrome and native in Safari is worse than neither. */
.field input[type='file']::file-selector-button,
.field input[type='file']::-webkit-file-upload-button {
  height: 38px;
  margin: 0 11px 0 0;
  padding: 0 14px;
  border: 0;
  border-right: 1px solid var(--hairline);
  border-radius: 9px 0 0 9px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.field input[type='file']:hover::file-selector-button { background: var(--hairline); }
.field input[type='file']:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --------------------------------------------------------------------------
   Estado — the readiness dashboard

   Was a twelve-row table, which gave a working database and a missing Stream
   Deck the same weight and left somebody reading all twelve to find the two
   that mattered. Exception-first now: a verdict, then only what is wrong,
   then the rest compressed.

   Every state colour comes from --ok / --warn / --danger through one
   `data-state` hook, so a chip, a dot, a bar segment and a card edge cannot
   disagree about what amber means.
   -------------------------------------------------------------------------- */

.st-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 12px 18px;
  align-items: center;
  padding: 16px 18px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle);
  background: var(--surface);
  --st-ink: var(--ok);
}
.st-warn { --st-ink: var(--warn); }
.st-fail { --st-ink: var(--danger); border-color: var(--danger); }

.st-verdict { display: flex; align-items: center; gap: 11px; min-width: 0; }
.st-verdict b { display: block; font-size: 16px; letter-spacing: -0.01em; }
.st-sub { display: block; margin-top: 1px; font-size: 12.5px; color: var(--ink-faint); }

.st-dot {
  width: 11px;
  height: 11px;
  flex: none;
  border-radius: 50%;
  background: var(--st-ink, var(--ok));
}

/*
 * The count, with its word ABOVE it.
 *
 * It read "8/12" and then, underneath, "bien" — a figure you had to finish
 * reading before you learned what it counted. Every figure in Hub carries
 * its label first, in 11px uppercase, and the markup is ordered to match.
 */
.st-tally { text-align: right; }
.st-tally b { display: block; font-size: 21px; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.st-tally span {
  display: block;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* One segment per check. `grid-auto-columns: 1fr` rather than flex so every
   segment is the same width whatever the count — a bar where the amber one is
   wider than the green ones is saying something it does not mean. */
.st-bar {
  grid-column: 1 / -1;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 3px;
  height: 8px;
}
.st-bar span { border-radius: 3px; background: var(--ok); }
.st-bar span[data-state='warn'] { background: var(--warn); }
.st-bar span[data-state='fail'] { background: var(--danger); }

@media (min-width: 720px) {
  /* The bar between the verdict and the count, all on one line. */
  .st-head { grid-template-columns: minmax(0, 1fr) minmax(120px, 300px) auto; }
  .st-bar { grid-column: auto; }
}

.st-title {
  display: flex;
  align-items: center;
  gap: 8px;
  /* 24, not 22. This is a direct child of `.bo-page`, so it was overriding
     the page's own 24px rhythm with a number two pixels short of it — three
     times on Estado, which is every section label the page has. Measured in
     the browser, not read off the file: `.bo-page > * + *` and `.st-title`
     have the same specificity, so the one further down the file wins and
     this one is further down. */
  margin: 24px 0 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.st-title span {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-faint);
  font-size: 11px;
  letter-spacing: 0;
}
.st-title-quiet { color: var(--ink-faint); }

/* What is wrong: full cards, because each one is something to go and do. */
.st-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
}

.st-card {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 14px 15px;
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--st-ink);
  border-radius: var(--squircle-sm);
  background: var(--surface);
  --st-ink: var(--warn);
  min-width: 0;
}
.st-card[data-state='fail'] { --st-ink: var(--danger); }

.st-card-top { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.st-card-top b { font-size: 14.5px; }
.st-card p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink-soft); }

.st-chip {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--st-ink);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* The one thing to go and do about this card, so it is a control and gets a
   control's target — it was a 19px line of text. */
.st-fix {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.st-fix:hover { text-decoration: underline; }

/* What is fine: tiles. Twelve green rows is a wall of text saying one thing. */
.st-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 9px;
}

.st-tile {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  min-height: 44px;
  padding: 11px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  min-width: 0;
}
.st-tile .st-dot { margin-top: 5px; width: 8px; height: 8px; }
a.st-tile:hover { border-color: var(--accent); }
a.st-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.st-tile-body { min-width: 0; }
.st-tile-body b { display: block; font-size: 13px; }
.st-tile-body span {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--ink-faint);
  /* A long detail must not widen its column and drag the whole grid with it. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Estado — the televisions

   The one MEASURED thing on this page. A screen showing the classroom polls
   every two seconds, so "online" here is observed rather than deduced, and the
   strip below each screen is a record of days it was actually switched on.

   A day with no mark is a HOLE, not a colour. Drawing an unlit day green
   would say the opposite of the truth.
   -------------------------------------------------------------------------- */

.tv-list {
  display: grid;
  /*
   * `min(300px, 100%)`, NOT a bare 300 — the auto-fit floor trap.
   *
   * When the container is narrower than the floor, the track stays AT the
   * floor and the item overflows. Measured on a 320px phone: the list is
   * 288 wide and the screen card inside it is 300, so it ran 12px past the
   * list and ate the page's right-hand gutter. It did not reach the viewport
   * edge only because `.bo-main`'s 16px padding happened to absorb it;
   * anything narrower than about 316 would scroll sideways.
   *
   * Same one-token fix `.people` needed on the same day, and the same reason
   * narrowcheck did not report it: at 375 it genuinely fits, and nothing was
   * hidden or scrolling at the widths it measures.
   */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 12px;
}

.tv {
  padding: 14px 15px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface);
  min-width: 0;
  --tv-ink: var(--ink-faint);
}
.tv[data-on='1'] { --tv-ink: var(--ok); }
/* Switched off on purpose is not the same as not answering, and must not
   read as a fault. */
.tv[data-off='1'] { --tv-ink: var(--ink-faint); opacity: .72; }

.tv-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.tv-head b { font-size: 14.5px; }

.tv-dot {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50%;
  background: var(--tv-ink);
}
/* A quiet pulse only while it is genuinely live — the one place on this page
   where something is happening right now. */
.tv[data-on='1'] .tv-dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }

.tv-when { margin-left: auto; font-size: 12px; color: var(--ink-faint); }
.tv[data-on='1'] .tv-when { color: var(--ok); font-weight: 600; }

.tv-days {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 2px;
  height: 26px;
  margin-top: 11px;
}
.tv-days span {
  border-radius: 2px;
  /* The default is the HOLE: a day nothing was heard from. */
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.tv-days span[data-on='1'] { background: var(--ok); border-color: transparent; }

.tv-foot {
  margin: 9px 0 0;
  font-size: 12px;
  color: var(--ink-faint);
  /* A long line must not widen the card and drag the grid with it. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tv-foot b { color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.st-foot {
  margin: 12px 0 0;
  max-width: 74ch;
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------
   Wide tables

   `.card` clips (`overflow: hidden`) so its rounded corners stay rounded, and
   `table` is `width: 100%`. A table with more columns than fit was therefore
   cut off with no way to reach the rest: on a 375px phone the children table
   wants 536px, the card gives it 347, and the 191px hidden included the
   child's own NAME. Scrolling it was possible in script and impossible with a
   finger. Ian reported it as "can't scroll left and right".

   The scroller goes INSIDE the card, so the card keeps clipping its corners
   while the table scrolls within it.
   -------------------------------------------------------------------------- */
.t-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  /* Keeps a sideways swipe on the table from turning into a page-level
     back-gesture once it reaches the end. */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/*
 * `max-content`, and the bug that `auto` was.
 *
 * `min-width` rather than `width` so the table still fills the card when its
 * columns fit. But `width: auto` inside an overflow container means
 * shrink-to-fit against the CONTAINER, so the browser handed every column its
 * MIN-content width: on a phone "Sofía Marchetti" broke after the first name
 * and the row went from 43px to 63px tall — wrapping and scrolling sideways at
 * the same time, which is the worst of both. `max-content` gives each column
 * the width its content actually wants; the extra 60px is scroll the box was
 * already doing.
 */
.t-scroll > table { width: max-content; min-width: 100%; }

/* Headings do not wrap inside a scroller — "EN PANTALLA" breaking over two
   lines made the row taller without making the table any narrower, since the
   cells below it set the real width. */
.t-scroll th { white-space: nowrap; }

/*
 * The one column that IS prose.
 *
 * Everything else in these tables is a name, a date, an amount or a pill, and
 * `max-content` above is right for all of them. A bank movement's note is free
 * text up to 255 characters, which under `max-content` would draw one 2000px
 * line and make the whole table that wide. `max-width` does not apply to a
 * table cell, so the cap lives on a block INSIDE it — that the column does
 * honour.
 */
.t-prose > span { display: block; max-width: 30ch; }

/*
 * The end that is still off-screen.
 *
 * A table that scrolls looks exactly like a table that is cut off, so the
 * script marks each scroller `data-edge` with the side that has more to see
 * and these fade it. Script-only on purpose: without it you get today's
 * behaviour, which is a table that scrolls with no hint — never a table that
 * cannot.
 */
.t-scroll[data-edge] { -webkit-mask-image: var(--t-fade); mask-image: var(--t-fade); }
.t-scroll[data-edge='end']   { --t-fade: linear-gradient(to left,  transparent, #000 26px); }
.t-scroll[data-edge='start'] { --t-fade: linear-gradient(to right, transparent, #000 26px); }
.t-scroll[data-edge='both']  {
  --t-fade: linear-gradient(to right, transparent, #000 26px, #000 calc(100% - 26px), transparent);
}

/*
 * Phone padding.
 *
 * 18px either side is 36px a column — on a six-column table that is 216px of
 * nothing but air being scrolled past. The type size does not change; only the
 * gutters, which is what was actually making it feel cramped and long at once.
 */
@media (max-width: 760px) {
  th, td { padding: 9px 12px; }
  .t-prose > span { max-width: 24ch; }
}

/* --------------------------------------------------------------------------
   The bottom bar, and the sheets it opens
   --------------------------------------------------------------------------

   One cell per sidebar GROUP. Panel goes straight there; the other three open
   a sheet directly above the bar listing that group's pages. Phones only —
   above 860px the sidebar is a real column and a second navigation under it
   would be two.

   Everything here is closed with the `hidden` ATTRIBUTE, which is only safe
   because kids-tokens.css carries `[hidden] { display: none !important }`.
   Without that guard any author `display` below would outrank the user-agent
   rule and leave a full-screen scrim over the app eating every tap — which is
   exactly what happened on 2026-09-13.
   -------------------------------------------------------------------------- */

.bo-tabs, .bo-sheet, .bo-sheet-scrim { display: none; }

@media (max-width: 860px) {
  .bo-tabs {
    position: fixed;
    z-index: 36;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    width: min(440px, calc(100% - 20px));
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    padding: 6px;
    border-radius: 24px;
    border: 1px solid var(--hairline);
    /* Opaque, over the gradient the page paints. A translucent surface token
       lets a table scroll visibly under the labels. */
    background: linear-gradient(var(--surface-2), var(--surface-2)), var(--sky-top);
    box-shadow: 0 10px 34px rgb(0 0 0 / .28);
  }

  .bo-tabs a,
  .bo-tabs button {
    position: relative;
    display: grid;
    justify-items: center;
    align-content: center;
    gap: 3px;
    /* 52px: the one control a thumb reaches for on every page. */
    min-height: 52px;
    padding: 4px 2px;
    border: 0;
    border-radius: 18px;
    background: none;
    color: var(--ink-faint);
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  /* The page you are on, and the group it lives in, read the same. */
  .bo-tabs [aria-current='page'],
  .bo-tabs [aria-expanded='true'] {
    background: var(--surface);
    color: var(--accent);
  }

  .bo-tab-ico { display: block; width: 21px; height: 21px; }
  .bo-tab-ico svg { width: 100%; height: 100%; display: block; }

  /* A dot, not a count. The number is on the sheet's own Inscripciones row and
     in the panel's figures; out here the only question is whether there is
     something waiting. */
  .bo-tab-dot {
    position: absolute;
    top: 6px;
    left: calc(50% + 7px);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--danger);
    border: 2px solid var(--surface-2);
  }

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

  .bo-sheet {
    position: fixed;
    z-index: 36;
    left: 50%;
    transform: translateX(-50%);
    /* Clears the bar: its own height plus the gap it sits in. */
    bottom: calc(86px + env(safe-area-inset-bottom, 0px));
    width: min(440px, calc(100% - 20px));
    display: grid;
    gap: 2px;
    padding: 6px;
    border-radius: 20px;
    border: 1px solid var(--hairline);
    background: linear-gradient(var(--surface-2), var(--surface-2)), var(--sky-top);
    box-shadow: 0 14px 40px rgb(0 0 0 / .34);
    /*
     * IT HAS TO BE ABLE TO RUN OUT OF ROOM.
     *
     * The sheet grows upward from the bar and had no ceiling, so Sistema —
     * five pages plus the account block, 333px of it — went straight past
     * the top of the window on a phone held sideways. Measured at 812x375
     * and 667x375: top at -44, so "Imágenes" was entirely off screen and
     * there was nothing to scroll to it with. A phone in landscape is still
     * under 860px, so it is still this layout.
     *
     * 98 is the 86px the sheet already clears the bar by, plus 12 so the top
     * row is not flush against the edge.
     *
     * `svh`, not `vh`: iOS measures `vh` against the window with the address
     * bar HIDDEN, which is taller than what is on screen, so a ceiling built
     * from it would still let the sheet run off the top while the bar is
     * showing. Same reason `.bo-side` is 100svh.
     */
    max-height: calc(100svh - 98px - env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
  }

  /* No `svh` means the line above is simply dropped and nothing is worse
     than it was; `vh` is right everywhere except that iOS address bar. Its
     own rule rather than a second value in the block, which is how
     `.bo-side` states the same pair. */
  @supports not (height: 100svh) {
    .bo-sheet { max-height: calc(100vh - 98px - env(safe-area-inset-bottom, 0px)); }
  }

  .bo-sheet a {
    display: flex;
    align-items: center;
    gap: 11px;
    /* 48px rather than the bar's 52: these are a list, read before they are
       pressed, and a taller row only pushes the last one further from the
       thumb that opened the sheet. */
    min-height: 48px;
    padding: 0 12px;
    border-radius: 14px;
    color: var(--ink);
    font-size: 14.5px;
    font-weight: 600;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
  }
  .bo-sheet a[aria-current='page'] { background: var(--surface); color: var(--accent); }
  .bo-sheet-ico { flex: none; display: block; width: 19px; height: 19px; color: var(--ink-faint); }
  .bo-sheet a[aria-current='page'] .bo-sheet-ico { color: var(--accent); }
  .bo-sheet-ico svg { width: 100%; height: 100%; display: block; }
  .bo-sheet-label { flex: 1; min-width: 0; }
  .bo-sheet .bo-badge { flex: none; }

  /*
   * Below both, so a tap outside closes without the sheet's own taps passing
   * through it.
   *
   * INERT BY DEFAULT, in this media context, and not left to the `hidden`
   * attribute alone. The global `[hidden]` guard does hide it — but
   * `bin/checks/overlaycheck.py` refuses an off switch declared in a
   * different media context, and it is right to: that is precisely how the
   * drawer's scrim became an invisible full-screen layer over every phone on
   * 2026-09-13. A full-screen element says how it is switched off in the same
   * block that makes it full-screen.
   */
  .bo-sheet-scrim {
    position: fixed;
    inset: 0;
    z-index: 35;
    /* `display` is what this media block has to switch ON. Everything above
       is `display: none` outside it, and leaving that in place here meant the
       scrim never rendered and a tap outside an open sheet did nothing —
       caught by hit-testing the real page rather than by reading it. */
    display: block;
    border: 0;
    padding: 0;
    background: rgb(0 0 0 / .32);
    visibility: hidden;
    pointer-events: none;
  }
  .bo-sheet-scrim:not([hidden]) { visibility: visible; pointer-events: auto; }

  /* Room for the bar, plus the home indicator on a notched phone. The page
     ends behind it otherwise, and on the children list that is the last row
     of the table. */
  .bo-main { padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
}

@media (max-width: 860px) and (prefers-reduced-motion: no-preference) {
  .bo-tabs a, .bo-tabs button { transition: background-color .18s ease, color .18s ease; }
  .bo-sheet { animation: bo-sheet-in .18s cubic-bezier(0.16, 1, 0.3, 1) both; }
}

@keyframes bo-sheet-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

@media (max-width: 860px) {
  /* Who you are, at the foot of the last sheet. Separated by a rule: it is not
     another destination, and a row that looks like one invites the tap that
     signs you out by accident. */
  .bo-sheet-who {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 4px;
    padding: 11px 12px 5px;
    border-top: 1px solid var(--hairline);
  }
  .bo-sheet-who .bo-face { flex: none; }
  .bo-sheet-who b { font-size: 13.5px; font-weight: 600; }
  .bo-sheet-out {
    flex: none;
    padding: 7px 11px;
    border-radius: 10px;
    background: var(--surface);
    color: var(--danger);
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
  }
}

/*
 * ---------------------------------------------------------------------------
 *  Crear un niño que no está en el directorio
 * ---------------------------------------------------------------------------
 *
 * A <details> card, shut until somebody needs it. Asked for 2026-10-25: a
 * visiting family hands a child over at the door and nobody is going to open
 * Hub, add a person and come back.
 *
 * It is shut rather than absent because the search above it is the right
 * answer for almost every child in the class; a form sitting open under the
 * search makes the page look like it wants both filled in.
 */
.nk-sum {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 18px;
  cursor: pointer;
  list-style: none;
}
/* Safari draws its own triangle from the UA sheet, which `list-style: none`
   above does not reach. Same pair as .bo-group-title. */
.nk-sum::-webkit-details-marker { display: none; }
/* The TEXT block is what grows, by name. It used to be `> span:last-child`,
   which stopped being the text the moment anything was added after it. */
.nk-sum-text { flex: 1 1 8rem; min-width: 0; }
/* `.card-head > .ico` cannot reach inside a summary, so the tile is drawn
   again here. Same 30px and the same wash — the mark on this card must be
   the same object as the mark on every other heading. */
.nk-sum > .ico {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  margin-top: 1px;
  /* Same tile, same token as `.card-head > .ico` — this summary is a card
     heading that happens to be clickable. */
  border-radius: var(--squircle-sm);
  background: var(--accent-wash);
  color: var(--accent);
}
.nk-sum > .ico svg { width: 17px; height: 17px; display: block; }
details[open] > .nk-sum { border-bottom: 1px solid var(--hairline); }

/* The pill that says pressing this does something, and the hover that says
   the whole strip is the target. Swapped by the open state rather than by
   script, so it is right on first paint when $nkOpen opens the card for us. */
.nk-sum-go { flex: none; margin-top: 2px; margin-left: auto; }
.nk-sum:hover { background: var(--surface-2); }
.nk-sum:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nk-when-open { display: none; }
details[open] > .nk-sum .nk-when-shut { display: none; }
details[open] > .nk-sum .nk-when-open { display: inline-flex; }

@media (max-width: 560px) {
  /* The pill drops under the text rather than squeezing the heading into
     two words per line. */
  .nk-sum { flex-wrap: wrap; }
  .nk-sum-go { margin-left: 42px; }
}

.nk > .field { max-width: 560px; }

/*
 * Three fields across, one under the other on a phone.
 *
 * Not `.form-row`: that one carries a five-column template built for the
 * rota's own fields, and borrowing it would put a date box in a 120px slot.
 * `align-items: end` for the same reason it has it — labels wrap to different
 * heights, and without it the row reads as scattered.
 */
.nk-row {
  display: grid;
  gap: 12px;
  align-items: end;
  grid-template-columns: minmax(150px, 1.4fr) minmax(150px, 1.4fr) minmax(140px, 1fr);
}
.nk-row .field { margin-bottom: 0; }
/* The second row of a pair sits closer than two separate groups would. */
.nk-row-2 { margin-top: 14px; }
/*
 * Allergies, two columns wide.
 *
 * A grid item rather than a loose field with a max-width, so its right edge
 * lands on a COLUMN boundary. Capped at 560px it stopped 358px short of the
 * rows above it, which reads as a field somebody forgot to finish rather than
 * one that is deliberately shorter than the page.
 */
.nk-span2 { grid-column: 1 / 3; }

@media (max-width: 760px) {
  .nk-row { grid-template-columns: 1fr; }
  /* `1 / 3` against a single-column grid conjures a second column and the
     field hangs off the right edge. */
  .nk-span2 { grid-column: 1 / -1; }
}

/*
 * The names that already exist, above the form.
 *
 * `.card-body > .rows` is the rule that strips a list's own 18px when it sits
 * in a padded body — it is a CHILD selector, so this wrapper would have put
 * the faces a thumb's width in from the edge. Written out again rather than
 * flattening the markup, because the block also needs a rule under it.
 */
.nk-dupes {
  margin-bottom: 16px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--hairline);
}
.nk-dupes > .rows { margin-top: 10px; }
.nk-dupes > .rows > .row { padding-left: 0; padding-right: 0; }

/* The override, which only exists once the matches have been shown. */
.nk-confirm {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /*
   * 44, like every other control here — on the LABEL, not the box.
   *
   * The checkbox itself stays 17px: a 44px checkbox is a button pretending
   * to be a tick, and the label is the real target anyway because a <label>
   * wrapping its input toggles it. Measured at 20px before this, which is
   * the smallest thing to press on a form somebody fills in on an iPad.
   */
  min-height: 44px;
  font-size: 13px;
  color: var(--ink-soft);
  cursor: pointer;
}
.nk-confirm input { width: 17px; height: 17px; flex: none; accent-color: var(--accent); }

/* ===========================================================================
 * One child — the rail and its panels.
 *
 * Ian, 2026-11-01: "too many sections." Six cards down one column. The rail
 * is Hub's person profile in this app's own vocabulary: the same list in two
 * shapes, sideways above the content on a laptop and narrower, a column
 * beside it once there is room for one.
 * ========================================================================= */

.kid-strip { margin-bottom: 18px; }

/* The two numbers somebody came to this page for. A figure, not a sentence —
   "7 años · Saldo $30.00" was a line of prose you had to read. */
.kid-figures { display: flex; flex-wrap: wrap; gap: 28px; }
.kid-figure b {
  display: block;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.kid-figure > span {
  display: block;
  margin-top: 1px;
  color: var(--ink-faint);
  font-size: 12px;
}

.kid-grid { display: grid; gap: 18px; align-items: start; }
.kid-grid > * { min-width: 0; }

/* Sideways first. `overflow-x: auto` rather than wrapping, so three tabs
   never become two rows on a narrow screen; `min-width: 0` on the grid item
   above is what lets it scroll instead of stretching the page. */
.kid-rail {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: none;
}
.kid-rail::-webkit-scrollbar { display: none; }

.kid-rail-item {
  flex: none;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 13px;
  /* 44, like `.bo-close` and `.pick` and the buttons. 9px of padding on a
     14px line came out 39 measured, and on a phone this rail IS the way
     between the five sections of a child's record — the one thing on the
     page a thumb has to hit. A min-height rather than a height: the row is
     already `align-items: center`, so the label stays on the centre line
     and a longer label is still free to make the row taller. */
  min-height: 44px;
  border-radius: 10px;
  color: var(--ink-soft);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.kid-rail-item:hover { background: var(--surface-2); color: var(--ink); }
.kid-rail-item.is-on { background: var(--accent-wash); color: var(--accent); }
.kid-rail-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.kid-rail-item .ico { flex: none; display: grid; place-items: center; }
.kid-rail-item .ico svg { width: 17px; height: 17px; display: block; }

/* Each panel stacks on the page's own rhythm. `[hidden]` is spelled out:
   a bare attribute loses to any class that sets `display`, which is how a
   closed panel stays on screen. */
.kid-panel[hidden] { display: none; }
.kid-panel > * + * { margin-top: 18px; }

@media (min-width: 1000px) {
  .kid-grid { grid-template-columns: minmax(180px, 210px) minmax(0, 1fr); }
  .kid-rail { flex-direction: column; overflow: visible; }
  /* Beside the content, the rail must not ride down the page with it. */
  .kid-rail { position: sticky; top: 18px; align-self: start; }
}

/* `.hint-row` above already owns the full-width hint under a row of fields;
   all a grid row needs is the gap, since it has no flex basis to set. Three
   identical "sale en el iPad" notes were most of the height of that card. */
.nk-row + .hint { margin: 8px 0 16px; }

/* Hub's label treatment on the shared facts grid: small, upper, quiet, so a
   row reads as LABEL then value rather than as a sentence. Ian, 2026-11-01. */
.wv-facts-caps dt {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 11.5px;
  font-weight: 700;
}

/* The edit dialog is wider than `.modal`, because it carries the three-across
   row; below that width the row stacks and the dialog is the viewport. */
.modal-wide { width: min(620px, calc(100vw - 32px)); }
/* Wider than a form dialog: what is in it is a picture bound for a
   widescreen television, and 620px cropped the sense of it. */
.pic-view { width: min(820px, calc(100vw - 32px)); }
/* `.modal-title` and `.modal-foot` already exist above, from the rota's
   dialog — the edit dialog uses them unchanged. Only the gap under the
   heading is new, and it belongs to this dialog, not to every modal. */
.modal-wide .modal-title { margin-bottom: 16px; }

/* The inscription, as one panel ------------------------------------------
   Ian, 2026-11-01: "just make it a simple panel". The blocks below the
   roster are sections of that panel, not cards of their own, so they are
   separated by the same hairline the roster rows use. */
.wv-block { border-top: 1px solid var(--hairline); }

/* A section label inside a panel. Same treatment as a `.wv-facts-caps` key,
   because it is the same kind of word — what this part of the panel is. */
.wv-caps {
  margin: 0 0 10px;
  color: var(--ink-faint);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Apply and Archive, on the banner. `flex: none` so the contact block beside
   them takes the slack; they drop under it on a phone rather than squeezing
   an email address into one word per line. */
/*
 * The buttons beside the waiver's details.
 *
 * `flex: none` was the bug, not the email beneath it. It made this block
 * refuse to shrink, so at 375px it stayed 402px wide inside a 326px card —
 * and `.card` is `overflow: hidden` for its rounded corners, which simply
 * CUT THE BUTTONS OFF. No scrollbar, nothing to drag: on a phone the only
 * way to press "Aplicar" was to turn it sideways.
 *
 * `flex: none` reads as "keep these buttons on one line beside the text",
 * and on a laptop that is what it does. It was never only that: it also
 * forbade the shrinking that this block's OWN `flex-wrap` needs before it
 * can wrap. Both rules were present and the first one stopped the second
 * ever applying.
 *
 * `min-width: 0` as well, because a flex item will not go below its
 * min-content width without it, and these buttons have long Spanish labels.
 */
.wv-go {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}
.wv-what { border-top: 1px solid var(--hairline); }

/* --------------------------------------------------------------------------
   Ajustes — the season windows

   Two MM-DD boxes per row inside a table. They were written inline, with
   their own padding, their own 8px radius and their own border — so the one
   form on this page that is a GRID of controls was the one place where a
   control was 29px tall and a different shape from every other field in the
   back office. A class, because the rule is "a thing you type into looks
   like a thing you type into", and that cannot be true if it is restated by
   hand in two <td>s.

   A fixed width rather than `width: 100%`: the content is five characters
   and a cell that fills its column puts five characters in a 300px box.
   -------------------------------------------------------------------------- */

.ss-date {
  width: 7.5ch;
  height: 44px;
  padding: 0 10px;
  border: 1px solid var(--hairline);
  border-radius: var(--squircle-sm);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.ss-date:focus {
  outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: 1px;
}

/* ==========================================================================
   THE PANEL, as a card grid.

   Ian, 2026-11-09, pointing at shadcn's default dashboard a second time:
   "your just redecorating it the overview but your not giving me a fresh new
   layout." He was right — the first pass changed spacing and colour and left
   the structure alone.

   The structure here is the reference's: three equal columns, 24px gaps, and
   cards that are deliberately NOT all the same shape. A list, two figures
   each with their own small chart, one card two columns wide, and a table
   across the full width at the bottom. Nothing on this page is a carousel.
   ========================================================================== */
.dash {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px;
  align-items: start;
}
.dash-wide { grid-column: span 2; }
.dash-full { grid-column: 1 / -1; }

/* Two columns at tablet width and one on a phone: three 190px cards side by
   side is a dashboard nobody can read. A card that spans 2 or 3 has to be
   told to stop spanning, or it reaches past the grid. */
@media (max-width: 1100px) {
  .dash { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-wide, .dash-full { grid-column: 1 / -1; }
}
@media (max-width: 680px) {
  .dash { grid-template-columns: minmax(0, 1fr); }
  .dash-wide { grid-column: auto; }
}

.dash-card { display: flex; flex-direction: column; }
.dash-body { padding: 16px 18px 18px; }

.dash-fig {
  margin: 0;
  font-size: 32px; font-weight: 700; line-height: 1.05; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
/* Rose only when this figure is the thing to act on. A band where every
   number is coloured is a band with no colour in it. */
.dash-fig.is-bad { color: var(--danger); }
.dash-sub { margin: 4px 0 0; font-size: 13px; color: var(--ink-faint); }
.dash-foot { margin: 8px 0 0; font-size: 11.5px; color: var(--ink-faint); }
.dash-link {
  display: inline-block; margin-top: 14px;
  font-size: 13px; font-weight: 600; color: var(--accent); text-decoration: none;
}
.dash-none { margin: 0; padding: 18px 0; color: var(--ink-faint); font-size: 13px; }

/* --- the little bar charts ------------------------------------------------
   A flex row of equal columns, each a track with the bar pinned to the
   bottom. `align-items: end` is what makes the bars share a baseline; a grid
   of percentage heights without it hangs them from the top. */
.dash-bars {
  display: flex; align-items: flex-end; gap: 5px;
  margin: 16px 0 0; padding: 0; list-style: none;
  height: 76px;
}
.dash-bars li {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; justify-content: flex-end;
  height: 100%;
}
.dash-bars li > span {
  display: block; width: 100%;
  border-radius: 5px 5px 2px 2px;
  background: var(--accent);
}
.dash-bars-good li > span { background: var(--ok); }
.dash-bars li small {
  margin-top: 5px;
  font-size: 9.5px; color: var(--ink-faint);
  text-align: center; overflow: hidden; white-space: nowrap;
}

/* --- who carried the class ------------------------------------------------ */
.dash-rank { margin: 0; padding: 0; list-style: none; display: grid; gap: 12px; }
.dash-rank li {
  display: grid; grid-template-columns: minmax(0, 9rem) minmax(0, 1fr) 2rem;
  align-items: center; gap: 12px;
  font-size: 13.5px;
}
.dash-rank-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-rank-track { height: 10px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.dash-rank-track > span { display: block; height: 100%; border-radius: 999px; background: var(--accent); }
.dash-rank-n { text-align: right; font-variant-numeric: tabular-nums; }

/* --- the table across the bottom ------------------------------------------ */
.dash-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.dash-table th {
  padding: 10px 18px;
  text-align: left; font-size: 11px; font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint);
  border-bottom: 1px solid var(--hairline);
}
.dash-table td { padding: 11px 18px; border-bottom: 1px solid var(--hairline); }
.dash-table tr:last-child td { border-bottom: 0; }
.dash-table tr.is-next td { background: var(--surface-2); }
.dash-date { color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dash-empty { color: var(--warn); font-weight: 700; }
.dash-note { color: var(--ink-faint); }

/* --- para resolver -------------------------------------------------------- */
.dash-todo { padding: 6px; display: grid; gap: 1px; }
.dash-todo-row {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 7px 9px;
  border-radius: 10px; color: inherit; text-decoration: none;
}
.dash-todo-row:hover { background: var(--surface-2); }
.dash-dot { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: currentColor; }
.dash-todo-bad .dash-dot { color: var(--danger); }
.dash-todo-warn .dash-dot { color: var(--warn); }
.dash-todo-words { display: grid; gap: 1px; min-width: 0; }
.dash-todo-words b { font-size: 13.5px; font-weight: 700; }
.dash-todo-words small {
  font-size: 12px; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash-chev { margin-left: auto; color: var(--ink-faint); }

/* ==========================================================================
   THE PANEL, built to the reference's own measurements.

   https://shadcnuikit.com/dashboard/default, measured rather than eyeballed:
   three equal columns, 24px gaps, 14px radius on a card, a 14px/600 title
   with a 12px muted sentence under it, figures around 30px with a small
   muted delta beneath, and a table along the bottom with a status pill.
   ========================================================================== */
.sk { display: grid; gap: 24px; }
/* STRETCH, not start: the reference's three row-one cards are all 250px —
   equal height is most of what makes a row read as a row. The charts inside
   are pushed to the bottom with `margin-top: auto` so they share a baseline
   across the row however long a title wraps. */
.sk-row { display: grid; gap: 24px; align-items: stretch; }
.sk-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.sk-row-1-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
/* The two tables. Equal, because neither is the subordinate of the other. */
.sk-row-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Hub's dashboard uses `rounded-2xl` where its ordinary pages use
   `rounded-lg`; this is that pair, the same way round. */
.sk-card { display: flex; flex-direction: column; min-width: 0; border-radius: var(--squircle); }
.sk-head { padding: 16px 18px 12px; }
.sk-head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.sk-title { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.375; }
.sk-sub { margin: 3px 0 0; font-size: 12px; color: var(--ink-faint); }
.sk-body { padding: 0 18px 18px; display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.sk-people, .sk-todo { flex: 1 1 auto; align-content: start; }
.sk-none { margin: 0; padding: 0 18px 20px; font-size: 13px; color: var(--ink-faint); }

.sk-fig {
  margin: 0; font-size: 30px; font-weight: 700; line-height: 1.1;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.sk-delta { margin: 4px 0 0; font-size: 12px; color: var(--ink-faint); }

/* --- the people list ----------------------------------------------------- */
.sk-people { margin: 0; padding: 0 8px 10px; list-style: none; display: grid; gap: 2px; }
.sk-people li, .sk-todo a {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 10px; border-radius: 10px;
}
.sk-people li:hover, .sk-todo a:hover { background: var(--surface-2); }
/*
 * A FACE, or an initial — never both.
 *
 * Ian, 2026-11-09: "its not showing the teacher or student pictures on the
 * kids overview", then "its also overlaying their initial onto their picture
 * it needs to be only their picture."
 *
 * The photo arrives as an inline `background-image`, because the URL is
 * per-row data and a stylesheet cannot carry it; `cover` here crops it to the
 * circle.
 *
 * THE LETTER IS THE OTHER BRANCH, NOT AN UNDERLAY. The first version kept the
 * initial as the element's text and let the photo paint "over" it — which a
 * background cannot do. Painting order: the background sits behind the box,
 * the box's own text in front of it. Every face came with a capital letter
 * across it. The template now renders one or the other, and `.sk-ava-photo`
 * is simply the empty one.
 */
.sk-ava {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: 0 0 auto; border-radius: 50%;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  background-color: var(--accent);
  color: var(--accent-ink);
  font-size: 13px; font-weight: 700;
  /* The photo must not be stretched by a long name beside it. */
  overflow: hidden;
}
.sk-ava-sm { width: 26px; height: 26px; font-size: 11px; }
/* Carries a photograph and no text. Named rather than inferred from the
   inline style, because a class is what the stylesheet can reason about —
   and because a url() that 404s still leaves this circle the accent colour,
   which is a better failure than Chrome's broken-image glyph. */
.sk-ava-photo { background-color: var(--surface-2); }
.sk-person { display: grid; gap: 1px; min-width: 0; }
.sk-person b { font-size: 13.5px; font-weight: 600; }
.sk-person small {
  font-size: 12px; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sk-tag {
  margin-left: auto; padding: 3px 10px; border-radius: 7px;
  border: 1px solid var(--hairline);
  font-size: 12px; font-weight: 600; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* --- the worklist -------------------------------------------------------- */
.sk-todo { margin: 0; padding: 0 8px 10px; list-style: none; display: grid; gap: 2px; }
.sk-todo a { color: inherit; text-decoration: none; min-height: 44px; }
.sk-dot { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: currentColor; }
.sk-todo-bad .sk-dot { color: var(--danger); }
.sk-todo-warn .sk-dot { color: var(--warn); }
.sk-chev { margin-left: auto; color: var(--ink-faint); }

/* --- the charts ---------------------------------------------------------- */
/*
 * THE CHARTS SIT ON A SHARED BASELINE, and that is `margin-top: auto`.
 *
 * It was declared once here as `margin: 16px 0 0` and AGAIN seventy-five
 * lines earlier as `.sk-bars, .sk-line { margin-top: auto }`. The later rule
 * won, so the chart sat directly under its caption with the leftover height
 * dumped below it — which is the dead space Ian saw: "i have a bunch of room
 * on both sides and your making me scroll a bunch".
 *
 * Worth saying how it got past a check: I measured the three cards' HEIGHTS,
 * found them equal, and read that as the charts being aligned. Equal heights
 * were the stretch rule working; where the chart sat inside that height was
 * never measured. Two different questions.
 *
 * `auto` with a minimum, not a plain `auto`: on a card whose text already
 * fills it there is no free space to absorb, and the chart would butt
 * straight against the line above.
 */
.sk-bars {
  display: flex; align-items: flex-end; gap: 4px;
  margin: auto 0 0; padding: 16px 0 0; list-style: none; height: 70px;
  box-sizing: content-box;
}
.sk-bars li { flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; }
.sk-bars li > span { display: block; width: 100%; border-radius: 4px 4px 2px 2px; background: var(--accent); }

/* `preserveAspectRatio: none` lets one 100x32 path fill any width; the
   vector-effect is what stops the stroke stretching with it. */
.sk-line { display: block; width: 100%; height: 72px; margin-top: auto; padding-top: 14px;
           box-sizing: content-box; overflow: visible; }
.sk-line-tall { height: 170px; }
.sk-line-path {
  fill: none; stroke: var(--accent); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.sk-line-area { fill: var(--accent); opacity: .12; stroke: none; }
.sk-axis {
  display: flex; justify-content: space-between;
  margin: 8px 0 0; padding: 0; list-style: none;
  font-size: 10.5px; color: var(--ink-faint);
}

/* --- the table ----------------------------------------------------------- */
.sk-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.sk-table th {
  padding: 10px 18px; text-align: left;
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  color: var(--ink-faint); border-bottom: 1px solid var(--hairline);
}
.sk-table td { padding: 10px 18px; border-bottom: 1px solid var(--hairline); }
.sk-table tr:last-child td { border-bottom: 0; }
.sk-cell { display: inline-flex; align-items: center; gap: 9px; }
.sk-date { color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.sk-muted { color: var(--ink-faint); }
.sk-pill {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600;
}
.sk-pill-ok  { background: rgb(110 231 183 / 0.14); color: var(--ok); }
.sk-pill-bad { background: rgb(253 164 175 / 0.14); color: var(--danger); }
.sk-pill-warn { background: rgb(252 211 77 / 0.14); color: var(--warn); }
/* Not a fault and not good news — "no photographs" is a parent's choice
   being honoured. Grey, so it does not read as a problem to fix. */
.sk-pill-mute { background: var(--surface-2); color: var(--ink-soft); }

/* Points line up under each other, so two balances can be compared by eye
   without reading them. `tabular-nums` is what makes a 1 as wide as a 7. */
.sk-num { text-align: right; font-variant-numeric: tabular-nums; }

/* A child can be both. Wrapping rather than scrolling: the notes column is
   the last one and has the room. */
.sk-flags { display: inline-flex; flex-wrap: wrap; gap: 5px; }

/*
 * THE TABLES STACK FIRST, at 1400 rather than 1100.
 *
 * They hold five and four columns; a table does not reflow, it scrolls, and
 * a roster scrolling sideways inside a card is worse than a roster taking
 * the whole width. The figure cards above can take a narrower box because
 * their content is one number and one chart.
 */
@media (max-width: 1400px) {
  .sk-row-2 { grid-template-columns: minmax(0, 1fr); }
}

/* --------------------------------------------------------------------------
   THE PHONE VIEW.

   Ian, 2026-11-09: "make sure the kids overview has a speaclizied mobile view
   like the hub overview".

   The switch is at 860px, which is this app's OWN breakpoint — the one where
   the sidebar becomes a drawer and the bottom bar appears. Hub switches its
   dashboard at 1024 because its sidebar goes at 1024. Matching the number
   rather than the rule would put this page's two layouts on a different
   boundary from its own navigation, and a tablet would get the phone
   dashboard beside a desktop sidebar.

   Both halves are in the DOM; one is display:none. See the note in the
   template for why that is the right trade here.
   -------------------------------------------------------------------------- */
.sk-phone { display: none; }

@media (max-width: 860px) {
  .sk-desktop-only { display: none !important; }
  .sk-phone { display: grid; gap: 24px; }
}

/* FOUR NUMBERS, TWO ACROSS. Hub's phone tiles are a `grid-cols-2` of
   `rounded-2xl` cards with a figure over a label; these are the same shape
   with this app's own four figures in them. */
.sk-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }

.sk-tile {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  /* 96, so a tile holding "1.240" and a two-line label is the same height as
     one holding "0" and a short one. Grid stretch equalises a ROW; this
     equalises the two rows against each other. */
  min-height: 96px;
  padding: 14px;
  border-radius: var(--squircle);
  color: inherit;
  text-decoration: none;
}
.sk-tile-fig {
  font-size: 26px; font-weight: 700; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--ink); line-height: 1.1;
}
.sk-tile-lab { font-size: 12.5px; color: var(--ink-soft); line-height: 1.3; }
/* A zero here is good news and a number is not; the tile says which without
   anybody reading the label. */
.sk-tile-bad .sk-tile-fig { color: var(--danger); }

/* The coming Sunday, as one row with a face and a chevron. */
.sk-next { display: grid; gap: 10px; padding: 14px; border-radius: var(--squircle);
           color: inherit; text-decoration: none; }
.sk-next-when { font-size: 11.5px; font-weight: 600; letter-spacing: .05em;
                text-transform: uppercase; color: var(--ink-faint); }
.sk-next-who { display: flex; align-items: center; gap: 11px; }

/* The two lists, tighter than the desktop card's own people list. */
.sk-people-tight li { padding: 7px 10px; }
.sk-people-tight .sk-pill { margin-left: auto; flex: none; }
/* An unassigned Sunday still needs the circle, or the names below it step
   left by 26px and the list reads as two lists. */
.sk-ava-none { background: none; border: 1px dashed var(--hairline-strong);
               color: var(--ink-faint); }

@media (max-width: 1100px) {
  .sk-row-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sk-row-1-2 { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 680px) {
  .sk-row-3 { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
   The check-in card in Ajustes
   ---------------------------------------------------------------------------

   Three rows that are not all the same shape: a `.check` toggle with its own
   explanation, a row of two fields and a button, and a long read-only link.
   The spacing between them belongs to the card, not to any one of them.
*/
.ck-row { margin-top: 16px; }
/* The link form is a sibling of a form, and `.card-body > form + form` has no
   rule of its own; without this the two run together. */
.ck-link { padding-top: 16px; border-top: 1px solid var(--hairline); }

/* `.field-tight` is 150px, right for a clock time and a PIN. A <select> whose
   longest option is "Cámara (Fully Kiosk)" needs more than that, and left to
   `.field-grow` it would eat the whole row. */
.field-wide { margin-bottom: 0; flex: none; width: 230px; }

/* A hint that is a warning. Used for the missing id_secret, which is the one
   thing on this page that stops the kiosk working. */
.hint-bad {
  color: var(--danger);
  margin-bottom: 4px;
}
