:root {
  --color-depot-navy: #17324d;
  --color-depot-navy-deep: #10263a;
  --color-counter-white: #ffffff;
  --color-ledger-grey: #f2f5f9;
  --color-ink: #17212b;
  --color-muted-slate: #596a76;
  --color-rule: #d4dee3;
  --color-purchased: #087a69;
  --color-purchased-wash: #e2f4ef;
  --color-pending: #945a00;
  --color-pending-wash: #fff0ce;
  --color-transit: #2463a7;
  --color-transit-wash: #e7f0fb;
  --color-shortfall: #b42318;
  --color-shortfall-deep: #7a1a12;
  --color-shortfall-wash: #fde8e5;
  --color-focus: #146cc0;
  --color-draft: #5e6872;
  --color-draft-wash: #edf0f2;
  --color-returned: #6941c6;
  --color-returned-wash: #eee9fb;

  /* Phase 11 — the one accent (Ishan: "standard blue"). Buttons, the selected
     menu item, links, the chart and the selected tab. Navy is untouched and
     stays the colour of every heading and figure (see the sidebar note below
     for why that token is never repainted). 5.2:1 on white, so it passes AA as
     text as well as a fill. */
  --color-accent: #2563eb;
  --color-accent-strong: #1d4ed8;
  --color-accent-wash: #e8f0fe;
  --color-attention: #e3a008;
  /* Phase 12: a field's own edge — darker than a surface's hairline, so a
     white input on a white card is still plainly a box to type in. */
  --color-field-border: #b8c4cf;
  --shadow-card: 0 0.0625rem 0.125rem rgb(16 38 58 / 0.06), 0 0.375rem 1.125rem rgb(16 38 58 / 0.07);

  /* Text and figures — CLAUDE.md section 3.

     Source Sans 3 replaced Atkinson Hyperlegible: a taller x-height (0.845 of
     the cap height against 0.793) and ~7% narrower per title, which is real
     room on a 360px card. Atkinson was chosen for its exaggerated letter
     differentiation, and the reason that is affordable to give up is the line
     below — every figure and identifier now renders in the mono, and those are
     the strings where 0/O and 1/l/I actually matter.

     --font-mono is a real webfont now, not a system stack. It used to be
     SFMono/Consolas/Liberation, which resolves to a different face on every
     device class — so an admission number lined up on the machine it was
     designed on and nowhere else. Pinning it is what makes a column of figures
     a column. */
  --font-sans: "Source Sans 3", Arial, Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-md: 1.125rem;
  --text-lg: 1.375rem;
  --text-xl: 1.75rem;
  --text-2xl: 2.25rem;
  --line-tight: 1.2;
  --line-base: 1.5;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  --radius-sm: 0.375rem;
  --radius-md: 0.625rem;
  --radius-lg: 0.875rem;
  --radius-pill: 999px;
  --shadow-raised: 0 0.5rem 1.5rem rgb(23 50 77 / 0.12);
  --shadow-focus: 0 0 0 0.2rem rgb(20 108 192 / 0.28);
  /* Phase 32E: 44px, the floor itself, rather than 48 — four pixels back on
     every control a finger uses. Never below it (§3). */
  --tap-target: 2.75rem;

  /* The sidebar's own tokens — CLAUDE.md section 3.

     Deliberately separate from --color-depot-navy rather than a redefinition
     of it. That token has 47 uses and only 5 are backgrounds: the other 42 are
     `color:` on page titles, section titles, card headings, form labels and
     .btn-primary. Lightening it to lift the sidebar would repaint every
     heading in the application, which is the trap this set exists to avoid.

     --color-sidebar-surface is lifted and desaturated from the navy so the
     shell sits nearer the page and frames the content instead of competing
     with it; --color-sidebar-edge is the hairline that makes it a frame edge
     rather than a slab.

     --color-sidebar-selected is a filled accent, not another shade of the
     surface. Selection and hover used to be the same declaration, so a hovered
     item and the current page were indistinguishable. The mobile drawer
     already gets this right with a wash on a light surface; this is the same
     idea inverted, plus a left marker so it survives being glanced at.

     --sidebar-item-height is 2.5rem and not --tap-target on purpose. The
     sidebar renders only above the wide breakpoint — it is display:none at
     360px — so it is a pointer target, never a thumb one, and 3rem rows around
     a 1rem label are what made it read sparse. Nothing else may use this
     token, and --tap-target stays the floor everywhere touch happens. */
  --color-sidebar-surface: #12263a;
  --color-sidebar-edge: #1f3a54;
  --color-sidebar-selected: #2563eb;
  --color-sidebar-marker: #93c5fd;
  --color-sidebar-text: rgb(255 255 255 / 0.82);
  --color-sidebar-muted: rgb(255 255 255 / 0.6);
  --sidebar-item-height: 2.5rem;
  --sidebar-width: 15.5rem;
  --mobile-nav-height: 4.5rem;
  --sticky-action-height: 5.25rem;
  --content-max: 90rem;

  --bs-body-font-family: var(--font-sans);
  --bs-body-font-size: var(--text-base);
  --bs-body-line-height: var(--line-base);
  --bs-body-color: var(--color-ink);
  --bs-body-bg: var(--color-ledger-grey);
  --bs-primary: var(--color-accent);
  --bs-primary-rgb: 37, 99, 235;
  --bs-secondary-color: var(--color-muted-slate);
  --bs-border-color: var(--color-rule);
  --bs-border-radius: var(--radius-md);
  --bs-link-color: var(--color-accent);
  --bs-link-hover-color: var(--color-accent-strong);
  --bs-focus-ring-color: rgb(37 99 235 / 0.28);
}

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

html {
  min-width: 20rem;
}

body {
  min-width: 20rem;
  margin: 0;
  background: var(--color-ledger-grey);
  color: var(--color-ink);
  font-family: var(--font-sans);
}

body.has-mobile-nav {
  padding-bottom: var(--mobile-nav-height);
}

body.purchase-page {
  padding-bottom: calc(var(--mobile-nav-height) + var(--sticky-action-height));
}

/* Figures and identifiers -------------------------------------------------
   CLAUDE.md section 3: every number a person compares against another number
   renders in the mono, tabular.

   One block rather than a font-family line on fifteen components, because the
   rule is "figures are mono" and a rule stated once cannot be half-applied.
   The next screen that shows a quantity adds its selector here; it does not
   invent its own treatment.

   What belongs: quantities, prices and totals, admission and invoice numbers,
   store and transfer codes. What does not: dates, which are read rather than
   compared and which the sans sets more compactly, and counts inside prose. */

.book-list__quantity,
.balance-due__figure,
.sale-summary__row dd,
.metric-block__figure,
.data-table .number-cell,
.shortfall-table td:last-child,
.invoice-books td:last-child,
.invoice-books__code,
.invoice-books__money,
.quantity-state,
.set-capacity,
.result-strip__figure,
.quantity-readonly,
.report-total__figure,
.choice-card__price,
.invoice-line__value,
.compact-id,
.mobile-record__id,
.matched-student__identity,
.mapping-row__source,
.sticky-purchase-action__figure,
.group-sale-total__figure,
.row-table__figure,
.balance-after__figure,
.set-pick__code,
.set-pick__price,
.set-pick__qty,
.purchase-total__figure,
.bulk-sets__price,
.school-prices__input,
.school-sets__held,
.stock-count__code,
.stock-count__input,
.stock-grid__box {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* The mono runs ~25% wider than the sans at the same size, which is the real
   cost of this rule and lands hardest in a table cell on a 360px card. A step
   down holds the column widths where they were without touching the figure's
   legibility, since a mono digit is wider to begin with. */
.data-table .number-cell,
.compact-id,
.mobile-record__id {
  font-size: var(--text-sm);
}

a {
  text-underline-offset: 0.15em;
}

button,
input,
select,
textarea {
  font: inherit;
}

button,
a,
input,
select,
textarea,
[tabindex]:not([tabindex="-1"]) {
  scroll-margin: var(--space-6);
}

:focus-visible {
  outline: 0.1875rem solid var(--color-focus);
  outline-offset: 0.125rem;
}

.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.form-check-input:focus-visible {
  border-color: var(--color-focus);
  box-shadow: var(--shadow-focus);
  outline: 0;
}

.skip-link {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 2000;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-counter-white);
  color: var(--color-depot-navy);
  font-weight: 700;
  transform: translateY(-150%);
}

.skip-link:focus {
  transform: translateY(0);
}

.app-shell {
  min-height: 100vh;
}

.app-main {
  min-width: 0;
}

/*
 * Phase 12: the top bar holds the session and store strip.
 *
 * It used to be a separate sticky line under the bar, at z-index 1020 — above
 * the account menu's dropdown (Bootstrap's 1000), which therefore opened
 * underneath it. Now the header is the one sticky layer and the dropdown lives
 * inside it, so nothing in the page can cover it.
 *
 * On a phone the header is two rows, and it sticks with its top row scrolled
 * off: top is minus that row's height, so the logo and buttons go and the
 * session and store stay — exactly what the separate strip used to do. The row
 * therefore states its height rather than taking it from its contents. On a
 * desktop it is one line (the 62rem block below).
 */
.topbar {
  --topbar-row-height: 3.75rem;
  position: sticky;
  top: calc(-1 * var(--topbar-row-height));
  z-index: 1020;
  border-bottom: 0.0625rem solid var(--color-rule);
  background: var(--color-counter-white);
  box-shadow: 0 0.1875rem 0.75rem rgb(23 50 77 / 0.06);
}

.topbar__row {
  display: flex;
  height: var(--topbar-row-height);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 0 var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
}

/* min-height is the 44px rule, and it is stated rather than inherited on
   purpose. This link is a real touch target — it is the way back to the
   dashboard on a phone — and its height used to come from whatever line box
   the typeface happened to produce around a 2rem brand square. Atkinson
   Hyperlegible has unusually tall vertical metrics and padded it past 44px by
   accident; Source Sans 3's normal metrics let it collapse to 32px on every
   authenticated page in both shells, which the 360px audit caught on the run
   that swapped the font. A tap target must not be a side effect of a font. */
.topbar__brand {
  display: inline-flex;
  min-height: var(--tap-target);
  align-items: center;
  gap: var(--space-2);
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
  text-decoration: none;
}

.brand-mark {
  display: inline-grid;
  width: 2rem;
  height: 2rem;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--color-depot-navy);
  color: var(--color-counter-white);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
}

.topbar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.icon-button {
  display: inline-grid;
  width: var(--tap-target);
  height: var(--tap-target);
  place-items: center;
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  color: var(--color-depot-navy);
}

/* Inside .topbar since Phase 12, which does the sticking and the shadow. */
.context-strip {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
  background: var(--color-counter-white);
}

.context-strip__item {
  min-width: 0;
  padding: var(--space-2) var(--space-3);
}

.context-strip__item + .context-strip__item {
  border-left: 0.0625rem solid var(--color-rule);
}

.context-strip__label {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: var(--line-tight);
  text-transform: uppercase;
}

.context-strip__value {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-1);
  color: var(--color-depot-navy);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--line-tight);
}

.context-strip__value-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.context-current {
  display: inline-flex;
  flex: 0 0 auto;
  padding: 0.0625rem var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-purchased-wash);
  color: var(--color-purchased);
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.context-lock {
  flex: 0 0 auto;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

/*
 * The store switcher, on an owner's filter screens. Takes the place of
 * .context-strip__value in the same cell, so the strip's height does not jump
 * between a screen that has one and a screen that does not.
 *
 * min-height is the 44px rule, not decoration: this is a real control in the
 * sticky header and a counter reaches it with a thumb.
 */
.context-switcher {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.context-switcher .form-select {
  min-height: var(--tap-target);
  font-size: var(--text-sm);
  font-weight: 700;
}

.page-content {
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--space-4);
}

.page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.page-header__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: var(--line-tight);
}

.page-header__subtitle {
  max-width: 45rem;
  margin: var(--space-1) 0 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.page-header__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.surface {
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-lg);
  background: var(--color-counter-white);
}

.surface + .surface,
.section-stack > * + * {
  margin-top: var(--space-4);
}

.surface__header,
.surface__body,
.surface__footer {
  padding: var(--space-4);
}

.surface__header + .surface__body,
.surface__body + .surface__footer {
  border-top: 0.0625rem solid var(--color-rule);
}

.section-title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.section-copy {
  margin: var(--space-1) 0 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.btn {
  --bs-btn-padding-y: 0.6875rem;
  --bs-btn-padding-x: 1rem;
  --bs-btn-font-weight: 700;
  --bs-btn-border-radius: var(--radius-md);
  min-height: var(--tap-target);
}

.btn-primary {
  --bs-btn-bg: var(--color-accent);
  --bs-btn-border-color: var(--color-accent);
  --bs-btn-hover-bg: var(--color-accent-strong);
  --bs-btn-hover-border-color: var(--color-accent-strong);
  --bs-btn-active-bg: var(--color-accent-strong);
  --bs-btn-active-border-color: var(--color-accent-strong);
  --bs-btn-disabled-bg: var(--color-muted-slate);
  --bs-btn-disabled-border-color: var(--color-muted-slate);
}

.btn-danger {
  --bs-btn-bg: var(--color-shortfall);
  --bs-btn-border-color: var(--color-shortfall);
  --bs-btn-hover-bg: var(--color-shortfall-deep);
  --bs-btn-hover-border-color: var(--color-shortfall-deep);
}

.btn-outline-danger {
  --bs-btn-color: var(--color-shortfall);
  --bs-btn-border-color: var(--color-shortfall);
  --bs-btn-hover-bg: var(--color-shortfall);
  --bs-btn-hover-border-color: var(--color-shortfall);
  --bs-btn-active-bg: var(--color-shortfall-deep);
  --bs-btn-active-border-color: var(--color-shortfall-deep);
}

.btn-outline-primary {
  --bs-btn-color: var(--color-depot-navy);
  --bs-btn-border-color: var(--color-depot-navy);
  --bs-btn-hover-bg: var(--color-depot-navy);
  --bs-btn-hover-border-color: var(--color-depot-navy);
}

.btn-link-danger {
  color: var(--color-shortfall);
}

.form-label,
.form-check-label {
  color: var(--color-ink);
  font-weight: 700;
}

/*
 * Phase 12: fields are white. Bootstrap paints them with --bs-body-bg, which
 * this file sets to the page grey (above), so every input and select in the
 * application was grey — on a grey page, grey on grey. background-color only,
 * never the shorthand: a select's arrow is a background-image.
 */
.form-control,
.form-select {
  min-height: var(--tap-target);
  border-color: var(--color-field-border);
  background-color: var(--color-counter-white);
  color: var(--color-ink);
  font-size: var(--text-base);
}

.form-control::placeholder {
  color: var(--color-muted-slate);
  opacity: 0.8;
}

.form-control:disabled,
.form-select:disabled {
  border-color: var(--color-rule);
  background: var(--color-ledger-grey);
  color: var(--color-muted-slate);
  opacity: 1;
}

.form-text {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.invalid-feedback {
  color: var(--color-shortfall);
  font-size: var(--text-sm);
  font-weight: 700;
}

.form-control.is-invalid,
.form-select.is-invalid {
  border-color: var(--color-shortfall);
}

.field-grid {
  display: grid;
  gap: var(--space-4);
}

.student-search-form {
  display: grid;
  gap: var(--space-3);
}

.search-input-wrap {
  position: relative;
}

.search-input-wrap .form-control {
  padding-left: 2.75rem;
}

.search-input-wrap::before {
  position: absolute;
  top: 50%;
  left: var(--space-4);
  z-index: 2;
  color: var(--color-muted-slate);
  content: "⌕";
  font-size: var(--text-lg);
  line-height: 1;
  transform: translateY(-50%);
}

.search-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.search-summary__count {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 700;
}

.selection-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.selection-list__button {
  display: grid;
  width: 100%;
  min-height: var(--tap-target);
  gap: var(--space-2);
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  color: var(--color-ink);
  text-align: left;
}

.selection-list__button:hover,
.selection-list__button[aria-current="true"] {
  border-color: var(--color-transit);
  background: var(--color-transit-wash);
}

.selection-list__name {
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.selection-list__meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2) var(--space-4);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.selection-list__meta-item {
  min-width: 0;
}

.selection-list__meta-item--wide {
  grid-column: 1 / -1;
}

/*
 * Phase 13 — a result you can sell from. The card is the item, holding the
 * link (still a way to the child's own Sell screen), a row of actions, and the
 * panel the purchase form opens in. The link loses its own box inside it.
 */
.selection-list__item {
  overflow: hidden;
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.selection-list__item > .selection-list__button {
  border: 0;
  border-radius: 0;
}

.selection-list__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: 0 var(--space-4) var(--space-3);
}

.selection-list__actions .btn {
  min-height: var(--tap-target);
}

.selection-list__panel {
  padding: var(--space-4);
  border-top: 0.0625rem solid var(--color-rule);
  background: var(--color-ledger-grey);
}

.selection-list__panel[hidden] {
  display: none;
}

.quick-panel__warning {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-pending-wash);
  color: var(--color-pending);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Inside a result the form's sticky bar is part of the card, not the screen. */
.quick-panel .sticky-purchase-action {
  position: static;
  min-height: 0;
  padding: var(--space-4) 0 0;
  box-shadow: none;
  background: transparent;
}

.selection-list__meta-label {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.status-band {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.status-band--pending {
  border-bottom: 0.125rem solid var(--color-pending);
  background: var(--color-pending-wash);
  color: var(--color-pending);
}

.status-band--purchased {
  border-bottom: 0.125rem solid var(--color-purchased);
  background: var(--color-purchased-wash);
  color: var(--color-purchased);
}

.matched-student {
  overflow: hidden;
}

/* Phase 33: a child's balance, up front on their page. Amber like the
   pending band — money still to collect, not an error. */
.balance-due {
  border-left: 0.25rem solid var(--color-pending);
  background: var(--color-pending-wash);
}

.balance-due__label {
  display: block;
  color: var(--color-pending);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.balance-due__figure {
  margin-right: var(--space-2);
  color: var(--color-depot-navy);
  font-size: var(--text-xl);
}

.balance-due__list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.balance-due__item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

.balance-due__item .btn {
  margin-left: auto;
}

/* Phase 33: "Select all" above a long tick list — a row of its own, set
   apart from the boxes it ticks. */
.check-all {
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-field-border);
  font-weight: 600;
}

.matched-student__body {
  padding: var(--space-4);
}

.matched-student__name {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: var(--line-tight);
}

.matched-student__identity {
  margin: var(--space-1) 0 0;
  color: var(--color-muted-slate);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.detail-list {
  display: grid;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
}

.detail-list__row {
  display: grid;
  grid-template-columns: 6rem minmax(0, 1fr);
  gap: var(--space-3);
}

.detail-list dt {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  font-weight: 400;
}

.detail-list dd {
  min-width: 0;
  margin: 0;
  color: var(--color-ink);
  font-weight: 700;
  overflow-wrap: anywhere;
}

.purchase-details {
  display: grid;
  gap: var(--space-4);
}

.bookset-options,
.payment-options {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.choice-card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  min-height: var(--tap-target);
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  cursor: pointer;
}

.choice-card:hover,
.choice-card:has(.form-check-input:checked) {
  border-color: var(--color-transit);
  background: var(--color-transit-wash);
}

.choice-card .form-check-input {
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
}

.choice-card__title {
  display: block;
  color: var(--color-depot-navy);
  font-weight: 700;
}

.choice-card__meta {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  font-weight: 400;
}

.choice-card__price {
  font-weight: 700;
  white-space: nowrap;
}

.payment-options {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/*
 * Phase 12: Bootstrap's reboot floats every <legend>, and a floated legend is
 * no longer the fieldset's caption — it becomes an ordinary grid item. In the
 * payment-mode grid that put "Payment mode" in a cell of its own beside Cash,
 * wrapped to "Paymen / mode" on a desktop. Every such fieldset in the sale
 * screens gets the legend back across the full width, and so do the lines
 * that belong under the whole group.
 */
.bookset-options > legend,
.payment-options > legend,
.payment-received > legend,
.payment-options > .invalid-feedback {
  grid-column: 1 / -1;
  float: none;
  margin-bottom: 0;
}

/* Bootstrap sizes a legend like a heading; one styled as a field label reads
   as a field label, the same size as every other. */
legend.form-label {
  font-size: var(--text-base);
}

.payment-option {
  display: flex;
  min-height: var(--tap-target);
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  cursor: pointer;
}

.payment-option:hover,
.payment-option:has(.form-check-input:checked) {
  border-color: var(--color-transit);
  background: var(--color-transit-wash);
}

.payment-option .form-check-input {
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
}

.payment-option__text {
  font-weight: 700;
}

/* How much was paid, and the details of how (Phase 8B) ---------------------
   "Paid in full" is pre-selected, so an ordinary sale looks and taps exactly
   as before. Everything below reacts to the radios in CSS alone: no script to
   fail on a counter phone's connection. Wrapped in @supports so a browser
   without :has() shows every field rather than hiding any — a field that
   cannot be reached is worse than one that is always there. */
.payment-received {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  border: 0;
}

/* One per line on a phone: three across at 360px wrapped "Not paid yet" onto
   three lines. Three across from 36rem. */
.payment-received__options {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
}

/* Phase 8E: a walk-in is offered "Paid in full" alone — one card the full
   width, not squeezed into the first of three columns. */
.payment-received__options:has(> .payment-option:only-child) {
  grid-template-columns: minmax(0, 1fr);
}

.payment-received__options .payment-option {
  justify-content: center;
  text-align: center;
}

.payment-details {
  display: grid;
  grid-column: 1 / -1;
  gap: var(--space-3);
}

.form-label__optional {
  color: var(--color-muted-slate);
  font-weight: 400;
}

@supports selector(:has(*)) {
  /* Amount box: only for "Part paid". */
  .payment-form:not(:has(input[name="payment_received"][value="part"]:checked)) .payment-received__amount {
    display: none;
  }

  /* No mode to describe when nothing changed hands. */
  .payment-form:has(input[name="payment_received"][value="none"]:checked) .payment-options {
    display: none;
  }

  /* Mode details: hidden until a mode that has them is chosen. */
  .js-mode-detail {
    display: none;
  }

  .payment-form:has(input[name="payment_mode"][value="upi"]:checked) .js-mode-detail[data-modes~="upi"],
  .payment-form:has(input[name="payment_mode"][value="card"]:checked) .js-mode-detail[data-modes~="card"],
  .payment-form:has(input[name="payment_mode"][value="cheque"]:checked) .js-mode-detail[data-modes~="cheque"],
  .payment-form:has(input[name="payment_mode"][value="bank"]:checked) .js-mode-detail[data-modes~="bank"],
  .payment-form:has(input[name="payment_mode"][value="other"]:checked) .js-mode-detail[data-modes~="other"] {
    display: block;
  }

  /* The reference label's per-mode wording sits inline inside its <label>. */
  label .js-mode-detail[data-modes] {
    display: none;
  }

  .payment-form:has(input[name="payment_mode"][value="upi"]:checked) label .js-mode-detail[data-modes="upi"],
  .payment-form:has(input[name="payment_mode"][value="card"]:checked) label .js-mode-detail[data-modes="card"],
  .payment-form:has(input[name="payment_mode"][value="cheque"]:checked) label .js-mode-detail[data-modes="cheque"],
  .payment-form:has(input[name="payment_mode"][value="bank"]:checked) label .js-mode-detail[data-modes="bank"],
  .payment-form:has(input[name="payment_mode"][value="other"]:checked) label .js-mode-detail[data-modes="other"] {
    display: inline;
  }
}

.permission-note {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.return-form {
  display: grid;
  gap: var(--space-4);
}

.condition-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.condition-option {
  display: flex;
  min-height: var(--tap-target);
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  cursor: pointer;
}

.condition-option:has(.form-check-input:checked) {
  border-color: var(--color-transit);
  background: var(--color-transit-wash);
}

.condition-option .form-check-input {
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
}

.condition-option__title {
  display: block;
  font-weight: 700;
}

.condition-option__copy {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.book-list {
  margin: 0;
  padding: 0;
  border-top: 0.0625rem solid var(--color-rule);
  list-style: none;
}

.book-list__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 0.0625rem solid var(--color-rule);
}

.book-list__quantity {
  flex: 0 0 auto;
  color: var(--color-muted-slate);
  font-variant-numeric: tabular-nums;
}

.modal-footer--stacked {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.sale-summary {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-4) 0 0;
  border-top: 0.0625rem solid var(--color-rule);
}

.sale-summary__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.sale-summary__row dt,
.sale-summary__row dd {
  margin: 0;
}

.sale-summary__row dt {
  color: var(--color-muted-slate);
}

.sale-summary__row dd {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.sale-summary__row--total {
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 0.125rem solid var(--color-depot-navy);
  color: var(--color-depot-navy);
  font-size: var(--text-md);
}

.purchased-actions {
  display: grid;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.return-action {
  color: var(--color-shortfall);
}

.sticky-purchase-action {
  position: fixed;
  right: 0;
  bottom: var(--mobile-nav-height);
  left: 0;
  z-index: 1015;
  min-height: var(--sticky-action-height);
  padding: var(--space-3) var(--space-4);
  border-top: 0.0625rem solid var(--color-rule);
  background: var(--color-counter-white);
  box-shadow: 0 -0.375rem 1rem rgb(23 50 77 / 0.1);
}

.sticky-purchase-action .btn {
  width: 100%;
}

.required-note {
  margin: 0 0 var(--space-2);
  color: var(--color-shortfall);
  font-size: var(--text-xs);
  font-weight: 700;
  text-align: center;
}

.status-badge {
  display: inline-flex;
  min-height: 1.75rem;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: 0.0625rem solid currentColor;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
}

.status-badge::before {
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: 50%;
  background: currentColor;
  content: "";
}

.status-badge--purchased,
.status-badge--paid,
.status-badge--received {
  background: var(--color-purchased-wash);
  color: var(--color-purchased);
}

.status-badge--pending,
.status-badge--unpaid {
  background: var(--color-pending-wash);
  color: var(--color-pending);
}

.status-badge--dispatched,
.status-badge--transit {
  background: var(--color-transit-wash);
  color: var(--color-transit);
}

.status-badge--draft {
  background: var(--color-draft-wash);
  color: var(--color-draft);
}

.status-badge--returned {
  background: var(--color-returned-wash);
  color: var(--color-returned);
}

.status-badge--cancelled {
  background: var(--color-shortfall-wash);
  color: var(--color-shortfall);
}

.metric-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.metric-block {
  /* A size container, so its figure can be fitted to it — see below. */
  container-type: inline-size;
  min-width: 0;
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.metric-block__label {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.metric-block__figure {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-depot-navy);
  font-size: var(--text-xl);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  line-height: var(--line-tight);
  /* The backstop for a browser without container units: wrap, never push
     the page sideways. */
  overflow-wrap: anywhere;
}

/* Phase 8D: a figure is never wider than its card. x-metric-figure passes its
   length as --figure-chars; mono characters are 0.6em each, so this is the
   largest size at which the figure fits, capped at the usual size — short
   figures are untouched and only a long one shrinks. Found when "₹16,160.00"
   (184px) met a 156px half-width card at 360px and both dashboards scrolled
   sideways. */
@supports (font-size: 1cqi) {
  .metric-block__figure[style*="--figure-chars"] {
    font-size: min(var(--text-xl), calc(100cqi / (var(--figure-chars) * 0.62)));
  }
}

.metric-block__secondary {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.split-rail__labels {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 700;
}

.split-rail__purchased {
  color: var(--color-purchased);
}

.split-rail__pending {
  color: var(--color-pending);
  text-align: right;
}

.split-progress {
  display: block;
  width: 100%;
  height: 1rem;
  overflow: hidden;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--color-pending-wash);
  accent-color: var(--color-purchased);
}

.split-progress::-webkit-progress-bar {
  background: var(--color-pending-wash);
}

.split-progress::-webkit-progress-value {
  background: var(--color-purchased);
}

.split-progress::-moz-progress-bar {
  background: var(--color-purchased);
}

.split-rail__summary {
  margin: var(--space-2) 0 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.filter-panel__mobile-toggle {
  width: 100%;
}

.filter-grid {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-4);
}

/*
 * Phase 12: the filter bar is a card of its own, not fields loose on the page
 * ground. Inside a surface (a report) it is already on one, so it stays flat.
 * On a phone the card only shows once "Show filters" has opened it.
 */
.filter-panel:has(.collapse.show),
.filter-panel:has(.collapsing) {
  margin-bottom: var(--space-4);
  padding: 0 var(--space-4) var(--space-4);
  border: 0.0625rem solid rgb(16 38 58 / 0.06);
  border-radius: var(--radius-lg);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-card);
}

.surface .filter-panel {
  margin-bottom: 0;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
}

.filter-grid .form-label {
  margin-bottom: var(--space-1);
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* A filter that is narrowing the list is tinted, so which ones are on reads
   at a glance — the selected option is anything but the "All …" one, or a
   text or date box arrived with a value. */
.filter-grid .form-select:has(option:checked:not([value=""])),
.filter-grid .form-control[value]:not([value=""]) {
  border-color: var(--color-accent);
  background-color: var(--color-accent-wash);
  color: var(--color-depot-navy);
  font-weight: 600;
}

.filter-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-2);
}

.applied-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.filter-chip {
  display: inline-flex;
  min-height: 2.25rem;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
  border: 0.0625rem solid var(--color-transit);
  border-radius: var(--radius-pill);
  background: var(--color-transit-wash);
  color: var(--color-depot-navy);
  font-size: var(--text-sm);
}

.filter-chip__remove {
  display: inline-grid;
  width: 1.75rem;
  height: 1.75rem;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--color-depot-navy);
}

.table-shell {
  overflow: hidden;
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.data-table {
  width: 100%;
  margin: 0;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
  vertical-align: middle;
}

.data-table tr:last-child td {
  border-bottom: 0;
}

.data-table th {
  background: var(--color-ledger-grey);
  color: var(--color-depot-navy);
  font-size: var(--text-sm);
  font-weight: 700;
  text-align: left;
}

.data-table .number-cell {
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.sort-button {
  display: inline-flex;
  min-height: 2rem;
  align-items: center;
  gap: var(--space-1);
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font-weight: inherit;
}

.row-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

.mobile-record-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.mobile-record {
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.mobile-record__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.mobile-record__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.mobile-record__id {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-muted-slate);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.mobile-record__details {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

.mobile-record__label {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.mobile-record__value {
  display: block;
  margin-top: var(--space-1);
  font-weight: 700;
}

.mobile-record__actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

/*
 * Phase 9D — the set form's tick list: a class's books, each with a
 * quantity, ticked many at a time. The row is the tap target (.form-check is
 * already --tap-target tall); the quantity box stays narrow beside it.
 */
.pick-list {
  display: grid;
  gap: var(--space-2);
}

.pick-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5.5rem;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.pick-list__row:has(.form-check-input:checked) {
  border-color: var(--color-depot-navy);
  background: var(--color-transit-wash);
}

.pick-list__qty {
  min-height: var(--tap-target);
}

/* Phase 29: the class's price for the book, after its quantity. On a phone
   the title takes the row and quantity and price share the line under it. */
.pick-list__row {
  grid-template-columns: minmax(0, 1fr) 5.5rem 8.5rem;
}

.pick-list__price .form-control {
  min-height: var(--tap-target);
  text-align: right;
}

@media (max-width: 47.99rem) {
  .pick-list__row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .pick-list__row > .form-check {
    grid-column: 1 / -1;
  }
}

.import-columns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding-left: 1.25rem;
}

.import-columns li {
  margin-right: var(--space-3);
}

.import-problems {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.import-problems__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 0.0625rem solid var(--color-rule);
}

/* Phase 9C — a school's page lists its own sets, class by class. */
.school-sets {
  display: grid;
  gap: var(--space-4);
}

.school-sets__class {
  display: grid;
  gap: var(--space-3);
  padding-bottom: var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
}

.school-sets__class:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.school-sets__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.school-sets__head .btn,
.school-sets__set .btn {
  min-height: var(--tap-target);
}

.school-sets__set {
  padding: var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.copy-sources__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.copy-sources__list .btn {
  min-height: var(--tap-target);
}

.copy-sources__list .btn[aria-current="true"] {
  background: var(--color-depot-navy);
  color: var(--color-counter-white);
}

.feedback-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 0.0625rem solid;
  border-radius: var(--radius-md);
}

.feedback-toast--success {
  border-color: var(--color-purchased);
  background: var(--color-purchased-wash);
  color: var(--color-purchased);
}

.feedback-toast--error {
  border-color: var(--color-shortfall);
  background: var(--color-shortfall-wash);
  color: var(--color-shortfall);
}

.feedback-toast__title {
  display: block;
  font-weight: 700;
}

.feedback-toast__copy {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-ink);
  font-size: var(--text-sm);
}

.empty-state {
  max-width: 34rem;
  margin: 0 auto;
  padding: var(--space-7) var(--space-4);
  text-align: center;
}

.empty-state__mark {
  display: grid;
  width: 4rem;
  height: 4rem;
  margin: 0 auto var(--space-4);
  place-items: center;
  border-radius: 50%;
  background: var(--color-transit-wash);
  color: var(--color-transit);
  font-size: var(--text-xl);
  font-weight: 700;
}

.empty-state__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
  font-weight: 700;
}

.empty-state__copy {
  margin: var(--space-2) 0 var(--space-4);
  color: var(--color-muted-slate);
}

.loading-state {
  display: flex;
  min-height: 7rem;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-5);
  color: var(--color-depot-navy);
  font-weight: 700;
}

.loading-state__spinner {
  width: 1.75rem;
  height: 1.75rem;
  border: 0.1875rem solid var(--color-rule);
  border-top-color: var(--color-transit);
  border-radius: 50%;
  animation: loading-spin 0.8s linear infinite;
}

@keyframes loading-spin {
  to {
    transform: rotate(360deg);
  }
}

.desktop-sidebar {
  display: none;
}

.desktop-sidebar--condensed {
  width: 5rem;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-sidebar-edge);
  color: var(--color-counter-white);
  text-decoration: none;
}

.sidebar-brand__text {
  font-size: var(--text-md);
  font-weight: 700;
}

.sidebar-context {
  padding: var(--space-3) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-sidebar-edge);
  color: var(--color-counter-white);
  font-size: var(--text-sm);
}

.sidebar-context__label {
  display: block;
  margin-bottom: 0.0625rem;
  color: var(--color-sidebar-muted);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.sidebar-nav {
  display: grid;
  gap: 0.125rem;
  margin: 0;
  padding: var(--space-2) var(--space-2);
  list-style: none;
}

/* The row is tightened so the label fills it rather than floating in it: at
   3rem around a 1rem label the list read sparse and scanned slowly. Pointer
   target only — see the token block for why this is not --tap-target. */
.sidebar-link {
  position: relative;
  display: flex;
  min-height: var(--sidebar-item-height);
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-sidebar-text);
  font-weight: 600;
  line-height: var(--line-tight);
  text-decoration: none;
}

/* Hover is a faint wash and says only "you are pointing at this". */
.sidebar-link:hover {
  background: rgb(255 255 255 / 0.08);
  color: var(--color-counter-white);
}

/* Selection is a different thing, not a stronger version of the same thing.
   A filled accent plus a left marker, so the current page is findable at a
   glance rather than by comparing two dark greys. */
.sidebar-link[aria-current="page"] {
  background: var(--color-sidebar-selected);
  color: var(--color-counter-white);
  font-weight: 700;
}

.sidebar-link[aria-current="page"]::before {
  position: absolute;
  top: 50%;
  left: 0;
  width: 0.1875rem;
  height: 1.25rem;
  border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
  background: var(--color-sidebar-marker);
  content: "";
  transform: translateY(-50%);
}

/* Hovering the page you are already on must not repaint it as merely
   hovered — the accent outranks the wash. */
.sidebar-link[aria-current="page"]:hover {
  background: var(--color-sidebar-selected);
}

.sidebar-link__icon {
  display: inline-grid;
  width: 1.5rem;
  place-items: center;
}

.mobile-tabbar {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1030;
  display: grid;
  height: var(--mobile-nav-height);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 0.0625rem solid var(--color-rule);
  background: var(--color-counter-white);
  box-shadow: 0 -0.375rem 1rem rgb(23 50 77 / 0.08);
}

.mobile-tabbar__link,
.mobile-tabbar__button {
  display: flex;
  min-width: 0;
  min-height: var(--tap-target);
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-1);
  border: 0;
  background: transparent;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  text-decoration: none;
}

.mobile-tabbar__link[aria-current="page"],
.mobile-tabbar__button[aria-expanded="true"] {
  color: var(--color-depot-navy);
}

.mobile-tabbar__icon {
  font-size: var(--text-md);
  line-height: 1;
}

.mobile-more-menu {
  position: fixed;
  right: var(--space-3);
  bottom: calc(var(--mobile-nav-height) + var(--space-2));
  left: var(--space-3);
  z-index: 1040;
  display: none;
  padding: var(--space-2);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-lg);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-raised);
}

.mobile-more-menu.is-open,
body[data-demo-more-open="true"] .mobile-more-menu {
  display: grid;
  gap: var(--space-1);
}

.mobile-more-menu__link {
  display: flex;
  min-height: var(--tap-target);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-depot-navy);
  font-weight: 700;
  text-decoration: none;
}

.mobile-more-menu__link:hover,
.mobile-more-menu__link:focus-visible {
  background: var(--color-ledger-grey);
}

.mobile-more-menu__badge {
  display: inline-grid;
  min-width: 1.75rem;
  height: 1.75rem;
  place-items: center;
  border-radius: var(--radius-pill);
  background: var(--color-pending-wash);
  color: var(--color-pending);
  font-size: var(--text-xs);
}

.offcanvas {
  --bs-offcanvas-width: min(19rem, 88vw);
}

.offcanvas-header {
  border-bottom: 0.0625rem solid var(--color-rule);
}

.offcanvas-nav {
  display: grid;
  gap: var(--space-1);
  margin: 0;
  padding: var(--space-3);
  list-style: none;
}

.offcanvas-nav__link {
  display: flex;
  min-height: var(--tap-target);
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-depot-navy);
  font-weight: 700;
  text-decoration: none;
}

.offcanvas-nav__link[aria-current="page"],
.offcanvas-nav__link:hover {
  background: var(--color-transit-wash);
}

.modal-content {
  overflow: hidden;
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-raised);
}

.modal-header,
.modal-footer {
  border-color: var(--color-rule);
}

.modal-warning-mark {
  display: grid;
  width: 2.5rem;
  height: 2.5rem;
  flex: 0 0 auto;
  place-items: center;
  border-radius: 50%;
  background: var(--color-shortfall-wash);
  color: var(--color-shortfall);
  font-weight: 700;
}

.modal-title-group {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.shortfall-table {
  width: 100%;
  margin: var(--space-4) 0;
  border-collapse: collapse;
}

.shortfall-table th,
.shortfall-table td {
  padding: var(--space-3) 0;
  border-bottom: 0.0625rem solid var(--color-rule);
}

.shortfall-table th:last-child,
.shortfall-table td:last-child {
  color: var(--color-shortfall);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  text-align: right;
}

/* The books on an invoice (Phase 8A). Neutral ink — red is reserved for
   "short" — and one line per book, so a whole set fits one A4 sheet. */
.invoice-books {
  width: 100%;
  margin: var(--space-2) 0 0;
  border-collapse: collapse;
}

.invoice-books th,
.invoice-books td {
  padding: var(--space-2) 0;
  border-bottom: 0.0625rem solid var(--color-rule);
  vertical-align: baseline;
}

.invoice-books th {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  font-weight: 700;
}

.invoice-books th:last-child,
.invoice-books td:last-child {
  width: 4rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.invoice-books__code {
  margin-left: var(--space-2);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.component-page {
  padding-bottom: var(--space-7);
}

.component-intro {
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--color-depot-navy);
  color: var(--color-counter-white);
}

.component-intro h1 {
  margin: 0;
  font-size: var(--text-2xl);
}

.component-intro p {
  max-width: 52rem;
  margin: var(--space-3) 0 0;
  color: rgb(255 255 255 / 0.8);
}

.component-section {
  margin-top: var(--space-6);
  scroll-margin-top: var(--space-5);
}

.component-section > h2 {
  margin: 0 0 var(--space-3);
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
}

.component-demo {
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-lg);
  background: var(--color-counter-white);
}

.component-demo + .component-demo {
  margin-top: var(--space-4);
}

.component-label {
  display: block;
  margin-bottom: var(--space-3);
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.component-status-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.component-demo .desktop-sidebar {
  position: static;
  display: block;
  width: min(100%, var(--sidebar-width));
  height: auto;
  min-height: 30rem;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--color-depot-navy);
}

.component-demo .desktop-sidebar--condensed {
  width: 5rem;
  min-height: 19rem;
}

.desktop-sidebar--condensed .sidebar-brand {
  justify-content: center;
  padding-inline: var(--space-2);
}

.desktop-sidebar--condensed .sidebar-nav {
  padding-inline: var(--space-2);
}

.desktop-sidebar--condensed .sidebar-link {
  justify-content: center;
  padding-inline: var(--space-2);
}

.purchase-layout {
  display: grid;
  gap: var(--space-4);
}

.purchase-workspace {
  display: grid;
  gap: var(--space-4);
}

.school-group + .school-group {
  margin-top: var(--space-4);
}

.school-group__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
  background: var(--color-ledger-grey);
}

.school-group__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.school-group__count {
  flex: 0 0 auto;
  color: var(--color-pending);
  font-size: var(--text-sm);
  font-weight: 700;
}

.school-group__body {
  padding: var(--space-3);
}

.counter-summary {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.dashboard-layout,
.dashboard-secondary-grid,
.report-layout {
  display: grid;
  gap: var(--space-4);
}

.trend-chart {
  min-height: 17rem;
  padding: var(--space-3);
}

.trend-chart svg {
  display: block;
  width: 100%;
  height: 15rem;
}

.chart-grid-line {
  stroke: var(--color-rule);
  stroke-width: 1;
}

.chart-area {
  fill: var(--color-transit-wash);
}

.chart-line {
  fill: none;
  stroke: var(--color-transit);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 4;
}

.chart-dot {
  fill: var(--color-counter-white);
  stroke: var(--color-transit);
  stroke-width: 3;
}

.chart-label {
  fill: var(--color-muted-slate);
  font-family: var(--font-sans);
  font-size: 12px;
}

.dashboard-list {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.dashboard-list__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 0.0625rem solid var(--color-rule);
}

.dashboard-list__item:last-child {
  border-bottom: 0;
}

.dashboard-list__title {
  min-width: 0;
  font-weight: 700;
}

.dashboard-list__meta {
  grid-column: 1 / -1;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.quantity-state {
  display: inline-flex;
  min-width: 3rem;
  justify-content: flex-end;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.quantity-state--healthy {
  color: var(--color-purchased);
}

.quantity-state--low {
  color: var(--color-pending);
}

.quantity-state--negative {
  color: var(--color-shortfall);
  text-decoration: underline double;
  text-underline-offset: 0.2em;
}

.set-availability {
  display: grid;
  gap: var(--space-3);
}

.set-availability-card {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 0.125rem solid var(--color-pending);
  border-radius: var(--radius-lg);
  background: var(--color-pending-wash);
}

.set-availability-card--blocked {
  border-color: var(--color-shortfall);
  background: var(--color-shortfall-wash);
}

.set-availability-card--blocked .set-capacity,
.set-availability-card--blocked .set-limit {
  color: var(--color-shortfall);
}

.set-availability-card__header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

.set-availability-card__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.set-capacity {
  color: var(--color-pending);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  line-height: var(--line-tight);
}

.set-limit {
  margin: 0;
  color: var(--color-ink);
  font-weight: 700;
}

.set-entry-panel {
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-transit);
  border-radius: var(--radius-md);
  background: var(--color-transit-wash);
}

.set-entry-controls {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.set-expansion {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 0.0625rem solid var(--color-rule);
}

.set-expansion__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.set-expansion__count {
  color: var(--color-transit);
  font-weight: 700;
}

.entry-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
}

.entry-divider::before,
.entry-divider::after {
  height: 0.0625rem;
  flex: 1;
  background: var(--color-rule);
  content: "";
}

.stepper {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0 0 var(--space-5);
  padding: 0;
  list-style: none;
}

.stepper__item {
  position: relative;
  min-width: 0;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  text-align: center;
}

.stepper__item::before {
  position: relative;
  z-index: 1;
  display: grid;
  width: 2rem;
  height: 2rem;
  margin: 0 auto var(--space-2);
  place-items: center;
  border: 0.125rem solid var(--color-rule);
  border-radius: 50%;
  background: var(--color-counter-white);
  content: attr(data-step);
}

.stepper__item::after {
  position: absolute;
  top: 0.9375rem;
  left: 50%;
  width: 100%;
  height: 0.125rem;
  background: var(--color-rule);
  content: "";
}

.stepper__item:last-child::after {
  display: none;
}

.stepper__item--active,
.stepper__item--complete {
  color: var(--color-depot-navy);
}

.stepper__item--active::before {
  border-color: var(--color-transit);
  background: var(--color-transit);
  color: var(--color-counter-white);
}

.stepper__item--complete::before,
.stepper__item--complete::after {
  border-color: var(--color-purchased);
  background: var(--color-purchased);
  color: var(--color-counter-white);
}

.file-drop {
  padding: var(--space-7) var(--space-4);
  border: 0.125rem dashed var(--color-rule);
  border-radius: var(--radius-lg);
  background: var(--color-ledger-grey);
  text-align: center;
}

.mapping-list {
  display: grid;
  gap: var(--space-3);
}

.mapping-row {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
}

.mapping-row__source {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 700;
}

.result-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}

.result-strip__item {
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-ledger-grey);
  text-align: center;
}

.result-strip__figure {
  display: block;
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.receive-list {
  display: grid;
  gap: var(--space-3);
}

.receive-row {
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.receive-row__title {
  margin: 0 0 var(--space-3);
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.quantity-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.quantity-readonly {
  display: grid;
  min-height: var(--tap-target);
  place-items: center;
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-ledger-grey);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.discrepancy-field {
  margin-top: var(--space-3);
}

.report-total {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  gap: var(--space-3) var(--space-5);
  padding: var(--space-4);
  border-top: 0.125rem solid var(--color-depot-navy);
  background: var(--color-ledger-grey);
}

.report-total__label {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.report-total__figure {
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.auth-page {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: var(--space-4);
  background: var(--color-depot-navy);
}

.auth-card {
  width: min(100%, 27rem);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-raised);
}

.auth-card__business {
  margin: var(--space-2) 0 var(--space-5);
  color: var(--color-muted-slate);
}

.invoice-page {
  min-height: 100vh;
  padding: var(--space-4);
  background: var(--color-ledger-grey);
}

/* Phase 8A: the invoice is an A4 page (210 x 297mm), not the kit's A5. */
.invoice-sheet {
  width: min(100%, 49.625rem);
  min-height: 70.1875rem;
  margin: 0 auto;
  padding: var(--space-6);
  border: 0.0625rem solid var(--color-rule);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-raised);
  page: invoice;
}

.invoice-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding-bottom: var(--space-5);
  border-bottom: 0.1875rem solid var(--color-depot-navy);
}

.invoice-title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-xl);
  font-weight: 700;
}

.invoice-meta,
.invoice-party,
.invoice-totals {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-5) 0;
}

/* Phase 13 — the invoice's head: a blank letterhead band when chosen, then the
   customer on the left and the invoice's details on the right, values
   left-aligned under their labels. A table, so dompdf draws it the same. */
.invoice-letterhead {
  display: block;
}

.invoice-head {
  width: 100%;
  margin: var(--space-4) 0 var(--space-2);
  border-collapse: collapse;
  border-bottom: 0.1875rem solid var(--color-depot-navy);
}

.invoice-head td.invoice-head__party,
.invoice-head td.invoice-head__meta {
  width: 50%;
  padding: 0 0 var(--space-4);
  vertical-align: top;
}

.invoice-head td.invoice-head__meta {
  padding-left: var(--space-5);
}

.invoice-head__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
  color: var(--color-depot-navy);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.invoice-head__title .status-badge {
  letter-spacing: 0;
  text-transform: none;
}

.invoice-facts {
  border-collapse: collapse;
}

.invoice-facts th,
.invoice-facts td {
  padding: 0.125rem 0;
  text-align: left;
  vertical-align: top;
}

.invoice-facts th {
  padding-right: var(--space-4);
  color: var(--color-muted-slate);
  font-weight: 400;
  white-space: nowrap;
}

.invoice-facts td {
  font-weight: 700;
}

.invoice-facts__name {
  color: var(--color-depot-navy);
  font-size: var(--text-md);
}

/* A phone shows the invoice one column at a time; print and PDF keep two. */
@media screen and (max-width: 35.99rem) {
  .invoice-head td.invoice-head__party,
  .invoice-head td.invoice-head__meta {
    display: block;
    width: 100%;
    padding-left: 0;
  }
}

.invoice-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.invoice-line__label {
  color: var(--color-muted-slate);
}

.invoice-line__value {
  text-align: right;
}

.invoice-total {
  padding-top: var(--space-3);
  border-top: 0.125rem solid var(--color-depot-navy);
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
  font-weight: 700;
}

/* Phase 8B: what a part-paid or unpaid sale still owes. Amber, the colour this
   application uses for "still to do" — not red, which means short. */
.invoice-balance {
  padding-top: var(--space-2);
  border-top: 0.0625rem solid var(--color-rule);
  color: var(--color-pending);
  font-weight: 700;
}

.contact-sheet-grid {
  display: grid;
  gap: var(--space-3);
}

.page-card {
  display: block;
  min-height: 100%;
  padding: var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  color: var(--color-ink);
  text-decoration: none;
}

.page-card:hover {
  border-color: var(--color-transit);
  box-shadow: var(--shadow-raised);
}

.page-card__title {
  display: block;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.page-card__copy {
  display: block;
  margin-top: var(--space-2);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.error-page {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: var(--space-4);
}

.error-card {
  width: min(100%, 36rem);
  padding: var(--space-6);
  text-align: center;
}

.error-code {
  display: block;
  color: var(--color-shortfall);
  font-size: clamp(3.5rem, 18vw, 7rem);
  font-weight: 700;
  line-height: 1;
}

.demo-stage-switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.stage-note {
  margin: 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.compact-id {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

.text-purchased {
  color: var(--color-purchased);
}

.text-pending {
  color: var(--color-pending);
}

.text-shortfall {
  color: var(--color-shortfall);
}

@media (min-width: 36rem) {
  .page-content {
    padding: var(--space-5);
  }

  .page-header {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
  }

  .page-header__actions {
    justify-content: flex-end;
  }

  .student-search-form {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .student-search-form .btn {
    min-width: 9rem;
  }

  .field-grid--two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .purchased-actions {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* Phase 12: three across, never five. Five put six modes in a column a
     third of a desktop wide, cut "Cheque" off and left "Other" alone on a
     second row. Three makes two even rows. */
  .payment-options {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .payment-received__options {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .set-entry-controls {
    grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
    align-items: end;
  }
}

@media (min-width: 48rem) {
  .metric-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .filter-panel__mobile-toggle {
    display: none;
  }

  .filter-panel .collapse {
    display: block;
  }

  /* Always open from here, so always a card (Phase 12). */
  .filter-panel {
    margin-bottom: var(--space-4);
    padding: 0 var(--space-4) var(--space-4);
    border: 0.0625rem solid rgb(16 38 58 / 0.06);
    border-radius: var(--radius-lg);
    background: var(--color-counter-white);
    box-shadow: var(--shadow-card);
  }

  .filter-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .filter-actions {
    grid-column: 1 / -1;
    grid-template-columns: auto auto;
    justify-content: end;
  }

  .selection-list__button {
    grid-template-columns: minmax(12rem, 1.2fr) minmax(0, 2fr);
    align-items: center;
  }

  /* Phase 12: the book set's status leads — what opening the child offers. */
  .selection-list__meta {
    grid-template-columns: minmax(8.5rem, 0.9fr) 0.8fr 0.7fr 1fr 1.5fr;
  }

  .selection-list__meta-item--wide {
    grid-column: auto;
  }

  .dashboard-secondary-grid,
  .report-layout {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .mapping-row {
    grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 1.2fr);
    align-items: center;
  }

  .contact-sheet-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 62rem) {
  body.has-mobile-nav,
  body.purchase-page {
    padding-bottom: 0;
  }

  .app-shell {
    display: grid;
    grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  }

  .desktop-sidebar {
    position: sticky;
    top: 0;
    display: block;
    height: 100vh;
    overflow-y: auto;
    /* Lifted off the near-black navy so it frames the content rather than
       competing with it, with a hairline edge doing the separating that sheer
       weight used to. Its own token — never --color-depot-navy, which is the
       colour of every heading in the application. */
    border-right: 0.0625rem solid var(--color-sidebar-edge);
    background: var(--color-sidebar-surface);
  }

  /*
   * Phase 11: the top bar stays on a desktop. It used to be hidden here with
   * the phone's tab bar — and it carries the account menu as well as the
   * search, so a desktop had no Sign out and no search box at all. The 360px
   * audit's account-menu check could never see it. Only the logo and the
   * drawer button go: the sidebar has both.
   */
  .mobile-tabbar,
  .mobile-more-menu,
  .topbar__brand,
  .topbar [data-bs-toggle="offcanvas"] {
    display: none;
  }

  /*
   * Phase 12: one line — search, then the session and store, then the
   * account menu. The row and the actions dissolve (display: contents) so the
   * three can be ordered as siblings, and the bar sticks whole.
   */
  .topbar {
    top: 0;
    display: flex;
    min-height: var(--topbar-row-height);
    align-items: center;
    gap: var(--space-4);
    padding-inline: var(--space-6);
  }

  .topbar__row,
  .topbar__actions {
    display: contents;
  }

  .topbar-search {
    order: 1;
  }

  .topbar .context-strip {
    order: 2;
    display: flex;
    min-width: 0;
    margin-left: auto;
    background: transparent;
  }

  .topbar .user-menu {
    order: 3;
  }

  .context-strip__item {
    min-width: 0;
    max-width: 20rem;
    padding: var(--space-1) var(--space-4);
  }

  .context-strip__item:last-child {
    padding-right: 0;
  }

  .page-content {
    padding: var(--space-6);
  }

  /* Phase 12: the form gets the wide column and the student card the narrow
     one. It was the other way round, which squeezed every choice the counter
     makes into a third of the screen beside a card with five lines on it. */
  .purchase-layout {
    grid-template-columns: minmax(16rem, 22rem) minmax(0, 1fr);
    align-items: start;
  }

  .purchase-layout__search {
    grid-column: 1 / -1;
  }

  .purchase-layout__student {
    position: sticky;
    top: 5.5rem;
  }

  .purchase-layout__details {
    min-width: 0;
  }

  .sticky-purchase-action {
    position: static;
    min-height: 0;
    padding: var(--space-4) 0 0;
    border-top: 0.0625rem solid var(--color-rule);
    box-shadow: none;
  }

  .sticky-purchase-action .btn {
    width: 100%;
  }

  .filter-grid {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .modal-dialog.purchase-shortfall-dialog {
    max-width: 38rem;
  }

  .dashboard-layout {
    grid-template-columns: minmax(0, 1.65fr) minmax(18rem, 0.65fr);
  }

  .contact-sheet-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 61.98rem) {
  .purchase-shortfall-dialog {
    min-height: 100%;
    margin: 0;
    align-items: flex-end;
  }

  .purchase-shortfall-dialog .modal-content {
    max-height: 88vh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }

  .desktop-table-only {
    display: none;
  }
}

@media (min-width: 62rem) {
  .mobile-list-only {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto;
    transition-duration: 0.01ms;
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
  }
}

@media print {
  .desktop-sidebar,
  .topbar,
  .context-strip,
  .mobile-tabbar,
  .mobile-more-menu,
  .sticky-purchase-action,
  .page-header__actions {
    display: none;
  }

  body,
  .app-main {
    background: var(--color-counter-white);
  }

  .app-shell {
    display: block;
  }

  .page-content {
    max-width: none;
    padding: 0;
  }

  .invoice-page {
    min-height: 0;
    padding: 0;
    background: var(--color-counter-white);
  }

  /*
     Phase 8A, A4. Three things keep one sale on one sheet:

     - The whole document sits on the named A4 page, not just the sheet.
       With only .invoice-sheet on it, Chromium switched from the default page
       to the named one and back, and printed a blank page either side —
       three sheets of paper for one receipt.
     - No min-height. The page is already A4; a sheet forced to exactly 297mm
       plus any rounding spilled a blank second page.
     - Print type is 13px to the rem on this document only, which scales every
       rem-based size in the sheet together. At the screen's 16px a full
       seven-book set ran past the bottom of the page.
  */
  html:has(.invoice-sheet) {
    font-size: 13px;
  }

  html:has(.invoice-sheet) body {
    page: invoice;
  }

  .invoice-sheet {
    width: 210mm;
    min-height: 0;
    padding: 15mm;
    border: 0;
    box-shadow: none;
  }
}

@page invoice {
  size: A4 portrait;
  margin: 0;
}

body:not(:has(#mobileNavigation)) [data-bs-target="#mobileNavigation"],
body:not(:has(#ownerNavigation)) [data-bs-target="#ownerNavigation"] {
  display: none;
}

/* ==========================================================================
   Application additions
   --------------------------------------------------------------------------
   Everything below was added when converting the kit into Blade, for chrome
   the static kit did not cover. It uses only the tokens already defined at
   :root above — no new colours, sizes, radii or spacing values.
   ========================================================================== */

/* Account menu ------------------------------------------------------------
   The kit has no sign-out control on any page. On the store shell this is the
   only route to sign-out, so it lives in the top bar, which is visible at
   every width. */

.user-menu__trigger {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.user-menu__panel {
  min-width: 14rem;
  padding: 0;
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-raised);
}

.user-menu__identity {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
}

.user-menu__name {
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 700;
}

.user-menu__meta {
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.user-menu__signout {
  display: flex;
  width: 100%;
  min-height: var(--tap-target);
  align-items: center;
  padding: var(--space-2) var(--space-4);
  border: 0;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  background: transparent;
  color: var(--color-shortfall);
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 700;
  text-align: left;
}

.user-menu__signout:hover,
.user-menu__signout:focus-visible {
  background: var(--color-shortfall-wash);
  color: var(--color-shortfall-deep);
}

/* Phase 19: Change password, above Sign out — the same row, in ink. */
.user-menu__link {
  display: flex;
  min-height: var(--tap-target);
  align-items: center;
  padding: var(--space-2) var(--space-4);
  border-bottom: 0.0625rem solid var(--color-rule);
  color: var(--color-ink);
  font-weight: 600;
  text-decoration: none;
}

.user-menu__link:hover,
.user-menu__link:focus-visible {
  background: var(--color-accent-wash);
  color: var(--color-accent-strong);
}

.account-password {
  max-width: 28rem;
}

/* Navigation group headings -----------------------------------------------
   The owner shell gained an Admin group in Phase 0. The heading renders only
   when at least one of its links survives the permission filter. */

/* Folding navigation groups (Phase 8A, client request 11) -----------------
   Each group header is a real <button> driving a Bootstrap collapse, so it is
   keyboard-reachable and announces aria-expanded. The group holding the
   current page renders open; app-enhancements.js remembers the rest.

   The sidebar toggle is a pointer target and takes --sidebar-item-height, as
   the sidebar links do. The drawer toggle is a thumb target on a phone or
   tablet and takes --tap-target. */
.sidebar-nav__section,
.offcanvas-nav__section {
  list-style: none;
}

.sidebar-nav__items,
.offcanvas-nav__items {
  display: grid;
  gap: 0.125rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.sidebar-nav__items.collapse:not(.show),
.offcanvas-nav__items.collapse:not(.show) {
  display: none;
}

.sidebar-nav__toggle,
.offcanvas-nav__toggle {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  border: 0;
  background: transparent;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
}

.sidebar-nav__toggle {
  min-height: var(--sidebar-item-height);
  margin-top: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-sidebar-muted);
}

.sidebar-nav__section:first-child .sidebar-nav__toggle {
  margin-top: 0;
}

.sidebar-nav__toggle:hover {
  color: var(--color-sidebar-text);
  background: rgb(255 255 255 / 0.06);
}

.offcanvas-nav__toggle {
  min-height: var(--tap-target);
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-muted-slate);
}

.offcanvas-nav__toggle:hover {
  color: var(--color-depot-navy);
  background: var(--color-transit-wash);
}

.sidebar-nav__toggle:focus-visible,
.offcanvas-nav__toggle:focus-visible {
  outline: 0.125rem solid var(--color-focus);
  outline-offset: 0.125rem;
}

.sidebar-nav__chevron,
.offcanvas-nav__chevron {
  font-size: var(--text-sm);
  transition: transform 0.15s ease;
}

.sidebar-nav__toggle[aria-expanded="false"] .sidebar-nav__chevron,
.offcanvas-nav__toggle[aria-expanded="false"] .offcanvas-nav__chevron {
  transform: rotate(-90deg);
}

/* Mobile tab bar column count ---------------------------------------------
   The kit hardcodes four columns. Tabs are filtered by permission, so a
   read-only viewer may have fewer; the bar divides evenly whatever it has. */

.mobile-tabbar[data-tabs="1"] { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.mobile-tabbar[data-tabs="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.mobile-tabbar[data-tabs="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.mobile-tabbar[data-tabs="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* Touch targets ------------------------------------------------------------
   Found by the 360px pass (CLAUDE.md Phase 2.5, finding 3). Bootstrap's
   checkbox is 16x16 and its pagination link is 38px tall; both are well under
   the 44px a finger needs, and both repeat across every screen — 15 class
   checkboxes on a school, 40 departure checkboxes on an import result, a
   pager on every list.

   The fix is the whole row, not a giant checkbox. A 48px control would look
   absurd; a 24px control inside a 48px row whose label is also clickable
   gives a 48px-tall target the width of the surface, which is what a thumb
   actually hits. --tap-target is the kit's own token, already used by
   .icon-button and the form controls. */

.form-check {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding-left: 0;
  margin-bottom: 0;
}

.form-check .form-check-input {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  margin: 0;
  cursor: pointer;
}

/* The label carries the rest of the row, so the tappable area is the whole
   strip rather than the box alone. */
.form-check .form-check-label {
  flex: 1 1 auto;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: var(--tap-target);
  cursor: pointer;
}

.pagination .page-link {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  min-width: var(--tap-target);
}

/* A reason sentence on a record card. Prose, not a value: it reads as a line
   of explanation under the details grid rather than as a labelled field. */

.mobile-record__note {
  margin: 0 0 var(--space-3);
  color: var(--color-muted-slate);
}

/* Context strip on a narrow screen -----------------------------------------
   The store label is the one piece of identity this strip exists to carry,
   and at 360px "Nurpur Branch · STR-001" was being ellipsed to
   "Nurpur Branch · ST…" — a store user could not read their own store's code
   (CLAUDE.md Phase 2.5, finding 5).

   Below the tablet breakpoint the value wraps instead of truncating. Two
   short lines and a slightly taller strip cost nothing; an unreadable code
   costs a phone call to the main store. */

@media (max-width: 47.99rem) {
  .context-strip__value {
    flex-wrap: wrap;
    row-gap: 0;
  }

  .context-strip__value-text {
    overflow: visible;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* The remove control on an applied-filter chip is 28px of visible circle, and
   a 48px circle would swallow the chip it sits in. So the hit area is extended
   past the visible control instead: the ::after box is the target, the circle
   is the picture. Position is relative on the chip already being inline-flex,
   so this stays inside the chip's own stacking context and cannot cover the
   next chip's text. */

.filter-chip {
  position: relative;
}

.filter-chip__remove {
  position: relative;
}

.filter-chip__remove::after {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap-target);
  height: var(--tap-target);
  transform: translate(-50%, -50%);
  content: "";
}

/* An identifier that is also a link.
   .compact-id is usually a plain span in a table cell — an admission number, an
   invoice number — and a 44px floor on all of them would put a thumb's worth of
   air into every row of every list. But Phase 5 made one a link (the invoice on
   a reversal's screen), which the 360px pass caught at 141x19, and Phase 6's
   reports will link invoice numbers everywhere.
   So the rule is scoped to the anchor: the glyph stays the picture and the box
   becomes the target, the same ::after pattern .btn-close and
   .filter-chip__remove already use. */
a.compact-id {
  position: relative;
  display: inline-block;
}

a.compact-id::after {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  min-height: var(--tap-target);
  height: 100%;
  transform: translateY(-50%);
  content: "";
}

/* A dashboard-list row title that is also a link — the same case, one phase
   later, and caught the same way.
   .dashboard-list__title is a span everywhere the kit uses it: the low-stock
   and in-transit panels are labels beside a figure, not destinations. Phase 6's
   reports index makes it a link, and the 360px pass measured it at 282x24 — the
   width was never the problem, the height was.
   Scoped to the anchor for the reason a.compact-id is: a 44px floor on every
   .dashboard-list__title would put a thumb's worth of air into the dashboard
   panels, which are deliberately compact rows a reader scans rather than taps. */
a.dashboard-list__title {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
}

/* A metric card's secondary line, when it is a link.
   Third time this shape has appeared — a.compact-id in Phase 5,
   a.dashboard-list__title in Phase 6 Part A, and now this — and the cause is
   always the same: the kit uses a class as a label and the application turns
   one instance of it into a destination. The 360px pass caught these at 17px
   and 35px tall.
   min-height rather than the ::after hit extension the smaller controls use.
   The extension works where a glyph sits alone in white space; here three of
   these stack down a column of cards, and a 44px box centred on a 17px line
   would reach into the card below and steal its taps. Growing the line is the
   honest fix, and the grid stretches the cards to match. */
.metric-block__secondary a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
}

/* The invoice's closing line. The kit spelled it as Bootstrap utilities
   (mt-5 pt-4 border-top text-center), which the PDF cannot borrow — dompdf
   fetches nothing over the network, so it carries its own stylesheet and would
   have had to restate four utility classes to match. One named class, defined
   here and restated once in the PDF template, keeps the paper and the file
   looking like the same document. */

.invoice-footer {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 0.0625rem solid var(--color-rule);
  text-align: center;
}

.invoice-footer p {
  margin: 0;
}

/* The invoice's own actions — print, download, WhatsApp — above the sheet and
   never on it. print:none rather than a second template, so what goes on paper
   is exactly what is on screen. */

.invoice-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  max-width: 148mm;
  margin: 0 auto var(--space-4);
}

.invoice-actions .btn {
  flex: 1 1 auto;
}

@media print {
  .invoice-actions {
    display: none;
  }
}

/* Phase 8D — the group invoice. Its children's invoices sit behind a
   disclosure above the sheet: a 44px summary, because it is tapped on a phone,
   and never printed, for the same reason as the actions above. */

.invoice-children {
  max-width: 148mm;
  margin: 0 auto var(--space-4);
}

.invoice-children summary {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  color: var(--color-depot-navy);
  font-weight: 600;
  cursor: pointer;
}

@media print {
  .invoice-children {
    display: none;
  }
}

/* One line per child: name and invoice number, then two money columns. The
   base rule sizes the last column for a book quantity; a rupee figure needs
   more, and the second-to-last column is money too. */
.invoice-books--group th.invoice-books__money,
.invoice-books--group td.invoice-books__money {
  width: 6.5rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* Bootstrap's .btn-close is 32px, which is under the 44px target, and it is
   the dismiss control on every dialog in the application — the shortfall
   modal now, the return and cancellation dialogs in Phase 5.

   The 360px pass never caught it before Phase 3 because no modal was ever open
   during the walk: the audit measures pages, and until the shortfall there was
   no page whose modal was server-rendered.

   Extended rather than enlarged, the same way .filter-chip__remove is: a 44px
   × drawn in a modal header looks like a mistake, so the ::after box is the
   target and the glyph stays the picture. Kept inside the button's own
   stacking context so it cannot cover the title beside it. */

.modal-header .btn-close {
  position: relative;
}

.modal-header .btn-close::after {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap-target);
  height: var(--tap-target);
  transform: translate(-50%, -50%);
  content: "";
}

/* Live list updates (Phase 8C, client request 1) ---------------------------
   While app-enhancements.js fetches the next view of a list, the results dim
   so a reader knows the figures are about to change. Never the panel being
   typed into: dimming the box under somebody's cursor reads as it being
   disabled. */
#mainContent[aria-busy="true"] .surface:not(:has(.filter-panel)),
#mainContent[aria-busy="true"] .applied-filters {
  opacity: 0.55;
  transition: opacity 0.15s ease;
}

/* Search everything, in the top bar (Phase 8C, client request 15) ----------
   A box on a wide screen; on a phone a button the size of the menu button,
   because the bar already holds the brand, the account menu and the menu. */
.topbar-search {
  position: relative;
  display: none;
}

.topbar-search__icon {
  position: absolute;
  top: 50%;
  left: var(--space-3);
  color: var(--color-muted-slate);
  transform: translateY(-50%);
  pointer-events: none;
}

.topbar-search__input {
  width: 18rem;
  min-height: var(--tap-target);
  padding-left: 2.25rem;
}

.topbar-search-link {
  color: var(--color-depot-navy);
}

@media (min-width: 48rem) {
  .topbar-search {
    display: block;
  }

  .topbar-search-link {
    display: none;
  }
}

@media (min-width: 62rem) {
  /* Shares the line with the session and store since Phase 12, so it gives
     way on a narrow desktop rather than pushing them off. */
  .topbar-search__input {
    width: clamp(12rem, 24vw, 24rem);
  }
}

/* Phase 9A — the screens inside a section, as tabs under the strip. One row;
   on a phone it scrolls sideways inside its own box, never the page. Each tab
   is a 44px target, and the current one is filled. */
/*
 * Wraps rather than scrolls. A strip scrolling sideways put "Returns" and
 * "Import a student list" past the edge of a 360px screen — hidden, which is
 * the complaint the tabs exist to answer. The 360px audit flagged it on 62
 * pages; two short rows cost less than one hidden tab.
 */
.section-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
}

.section-tabs__link {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  min-height: var(--tap-target);
  padding: 0 var(--space-4);
  border: 0.0625rem solid var(--color-rule);
  border-radius: 999px;
  background: var(--color-counter-white);
  color: var(--color-depot-navy);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.section-tabs__link:hover {
  background: var(--color-transit-wash);
}

.section-tabs__link[aria-current="page"] {
  border-color: var(--color-depot-navy);
  background: var(--color-depot-navy);
  color: var(--color-counter-white);
}

/* Phase 9B — an invoice's four ways out on the sell screen: two per row on a
   phone, one row on desktop, each a full 44px button. */
.invoice-buttons {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}

@media (min-width: 48rem) {
  .invoice-buttons {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.sale-saved {
  border-color: var(--color-purchased);
  background: var(--color-purchased-wash);
}

/*
 * Phase 10 — x-row-table: rows of books, students, items or sets entered at
 * once. Client round 3: "ask how many rows first; the remove button at the
 * end of its row, not below; more table-like".
 *
 * A phone (below 48rem) gets one numbered card per row, its remove button in
 * the card's top line beside the number — at the row's end, as asked, without
 * a table wider than the screen. From 48rem it is a real table: a header row,
 * one line per row, the remove button in the last cell. Rows are numbered by a
 * CSS counter, so a removed row never leaves the numbering wrong, and the
 * number matches the "Row 3: …" a server message names.
 */
.row-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}

.row-table[hidden] {
  display: none !important;
}

.row-table tbody {
  counter-reset: row;
}

.row-table tbody > tr {
  counter-increment: row;
}

.row-table__num::before {
  content: counter(row);
}

thead .row-table__num::before {
  content: none;
}

.row-table__field {
  display: block;
  margin: 0;
}

.row-table__label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted-slate);
}

.row-table__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: var(--tap-target);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  color: var(--color-shortfall);
}

.row-table__remove:hover,
.row-table__remove:focus-visible {
  border-color: var(--color-shortfall);
}

.row-table__cell--invalid .form-control,
.row-table__cell--invalid .form-select {
  border-color: var(--color-shortfall);
}

.row-table__empty {
  margin: 0 0 var(--space-3);
}

.row-adder {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.row-adder__label {
  flex-basis: 100%;
  margin: 0;
  font-weight: 600;
}

.row-adder__count {
  width: 6rem;
  min-height: var(--tap-target);
}

.row-adder__button {
  min-height: var(--tap-target);
}

/* A phone: one card per row. */
@media (max-width: 47.99rem) {
  .row-table,
  .row-table tbody,
  .row-table tr,
  .row-table td {
    display: block;
  }

  .row-table thead {
    display: none;
  }

  .row-table tbody > tr {
    position: relative;
    display: grid;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
    padding: var(--space-3);
    padding-top: calc(var(--tap-target) + var(--space-3));
    border: 0.0625rem solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-counter-white);
  }

  .row-table tbody > tr.repeatable-row--invalid {
    border-color: var(--color-shortfall);
  }

  .row-table__num {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    line-height: var(--tap-target);
    font-weight: 600;
    color: var(--color-depot-navy);
  }

  .row-table__num::before {
    content: "Row " counter(row);
  }

  .row-table__actions {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
  }
}

/* A wide screen: a table, the remove button in the last cell. */
@media (min-width: 48rem) {
  .row-table th {
    padding: 0 var(--space-2) var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-muted-slate);
    text-align: left;
    border-bottom: 0.0625rem solid var(--color-rule);
  }

  .row-table td {
    padding: var(--space-2);
    vertical-align: top;
    border-bottom: 0.0625rem solid var(--color-rule);
  }

  .row-table__num {
    width: 2.5rem;
    padding-top: calc(var(--space-2) + 0.6rem) !important;
    font-weight: 600;
    color: var(--color-muted-slate);
    text-align: right;
  }

  .row-table__actions {
    width: calc(var(--tap-target) + var(--space-4));
    text-align: right;
  }

  .row-table tr.repeatable-row--invalid > .row-table__num {
    box-shadow: inset 0.1875rem 0 0 var(--color-shortfall);
    color: var(--color-shortfall);
  }

  /* The header names the column; the label stays for screen readers. */
  .row-table__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .row-adder__label {
    flex-basis: auto;
    align-self: center;
  }

  /* A wide row of fields scrolls inside its own box, never the page: the
     box is held to its container's width, which a grid or flex parent
     otherwise lets grow to fit the table (min-width: auto). */
  .row-table-wrap {
    min-width: 0;
    max-width: 100%;
  }

  .row-table-scroll {
    /* Positioned, so the visually hidden labels inside it (position:
       absolute) are clipped with the table rather than placed against the
       page — they were what widened a tablet's page to 1137px. */
    position: relative;
    max-width: 100%;
    overflow-x: auto;
  }
}

/*
 * Phase 10 — cell widths in a row table. Every field keeps a usable minimum,
 * so a wide table (Add several books has eight columns) scrolls inside its own
 * box on a tablet rather than squeezing a field to 50px; a name or a title
 * gets room for one; a class or a quantity stays narrow.
 */
@media (min-width: 48rem) {
  .row-table .form-control,
  .row-table .form-select {
    min-width: 6.5rem;
  }

  .row-table__cell--narrow {
    width: 9rem;
  }

  .row-table__cell--wide .form-control,
  .row-table__cell--wide .form-select {
    min-width: 13rem;
  }
}

/*
 * Phase 10 — a student's change history: what happened, who, when, and each
 * field's value before and after.
 */
.history-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.history-list__entry {
  padding-bottom: var(--space-3);
  border-bottom: 0.0625rem solid var(--color-rule);
}

.history-list__entry:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.history-list__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
}

.history-list__meta {
  font-size: var(--text-sm);
  color: var(--color-muted-slate);
}

.history-list__changes {
  display: grid;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
}

.history-list__change {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: var(--space-2);
}

.history-list__change dt {
  font-weight: 600;
  color: var(--color-muted-slate);
}

.history-list__change dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.history-list__before {
  color: var(--color-muted-slate);
  text-decoration: line-through;
}


/* =======================================================================
 * Phase 11 — the visual refresh. Nothing moves: colour, surfaces, type and
 * icons only. Approved from a before/after mockup (.tooling/mockups/
 * dashboard-refresh.html). The tokens above carry most of it — the accent,
 * the sidebar, the page ground — and these rules carry the rest.
 * ======================================================================= */

/* Surfaces: a soft shadow instead of an outline. A hairline stays, faint, so
   two surfaces touching are still two. */
.surface,
.metric-block {
  border-color: rgb(16 38 58 / 0.07);
  box-shadow: var(--shadow-card);
}

.metric-block {
  border-radius: var(--radius-lg);
}

.page-header__title,
.page-title {
  letter-spacing: -0.01em;
}

/* The selected tab takes the accent, like the selected menu item. */
.section-tabs__link[aria-current="page"] {
  border-color: var(--color-accent);
  background: var(--color-accent);
}

/* Headline cards: an icon chip, a small tracked label, a coloured top edge by
   meaning — money in green, still to do amber, a problem red, people and
   stock on the move in the accent. */
.metric-block {
  border-top: 0.1875rem solid transparent;
}

.metric-block--money { border-top-color: var(--color-purchased); }
.metric-block--todo { border-top-color: var(--color-attention); }
.metric-block--problem { border-top-color: var(--color-shortfall); }
.metric-block--people,
.metric-block--movement { border-top-color: var(--color-accent); }

.metric-block__top {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.metric-chip {
  display: inline-grid;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  place-items: center;
  border-radius: var(--radius-md);
  font-size: 1.05rem;
}

.metric-block--money .metric-chip { background: var(--color-purchased-wash); color: var(--color-purchased); }
.metric-block--todo .metric-chip { background: var(--color-pending-wash); color: var(--color-pending); }
.metric-block--problem .metric-chip { background: var(--color-shortfall-wash); color: var(--color-shortfall); }
.metric-block--people .metric-chip,
.metric-block--movement .metric-chip { background: var(--color-accent-wash); color: var(--color-accent); }

.metric-block__label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.metric-block__figure {
  margin-top: var(--space-2);
  font-weight: 600;
}

/* Card links read as actions, not browser links. Still 44px tall. */
.metric-block__secondary a {
  color: var(--color-accent);
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: none;
}

.metric-block__secondary a:hover {
  color: var(--color-accent-strong);
  text-decoration: underline;
}

/* The chart: a thicker line, a gradient fill, amounts on the grid lines, a
   dot on each day with sales. */
.trend-chart .chart-line {
  stroke: var(--color-accent);
  stroke-width: 2.5;
}

.trend-chart .chart-area {
  fill: url(#trendFill);
  stroke: none;
}

.trend-chart .chart-grid-line {
  stroke: #e3e9f0;
  stroke-dasharray: 4 4;
}

.trend-chart .chart-label,
.trend-chart .chart-axis {
  fill: var(--color-muted-slate);
  font-family: var(--font-sans);
  font-size: 12px;
}

.trend-chart .chart-day {
  fill: var(--color-counter-white);
  stroke: var(--color-accent);
  stroke-width: 2;
}

.trend-chart .chart-dot {
  fill: var(--color-accent);
  stroke: var(--color-counter-white);
  stroke-width: 2.5;
}

.chart-total {
  margin: 0;
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.chart-total__figure {
  color: var(--color-depot-navy);
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: 600;
}

.surface__header--split {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

/* Progress: thicker bars, and the season's percentage stated large. */
.split-progress {
  height: 0.875rem;
}

.split-rail__percent {
  display: block;
  color: var(--color-purchased);
  font-family: var(--font-mono);
  font-size: 2.25rem;
  font-weight: 600;
  line-height: 1;
}

.progress-cell {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.progress-cell .split-progress {
  flex: 1;
  min-width: 5rem;
  height: 0.625rem;
}

.progress-cell__percent {
  min-width: 3.5ch;
  color: var(--color-purchased);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: right;
}

/* Table heads: a quiet shaded band with small tracked labels. */
.data-table thead th,
.data-table thead th.number-cell {
  /* Headings are words, even over a column of figures — and the figures
     list (.data-table .number-cell) would otherwise win on specificity. */
  font-family: var(--font-sans);
  background: var(--color-ledger-grey);
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

/* A dashboard panel with nothing in it is one line, not a tall card. */
.empty-state--compact {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 0 var(--space-3);
  padding: var(--space-2) 0;
  text-align: left;
}

.empty-state--compact .empty-state__mark {
  grid-row: span 2;
  width: 2.5rem;
  height: 2.5rem;
  margin: 0;
  font-size: var(--text-base);
}

.empty-state--compact .empty-state__title {
  margin: 0;
  font-size: var(--text-base);
}

.empty-state--compact .empty-state__copy {
  margin: 0;
}

/* Phase 11 — two surfaces side by side in a grid line up. The global
   ".surface + .surface" top margin is for surfaces stacked down a page; inside
   these grids it pushed the second card 1rem lower than the first. */
.dashboard-layout > .surface + .surface,
.dashboard-secondary-grid > .surface + .surface {
  margin-top: 0;
}

/* Phase 12 — the purchase form ---------------------------------------------
   Shared by the Sell screen and the student page. Four sections — book set,
   discount, payment, amount — each under its own heading and rule, so a
   counter reads it top to bottom instead of hunting across one long column. */
.purchase-form {
  display: grid;
  gap: var(--space-5);
}

.purchase-form__section {
  display: grid;
  gap: var(--space-3);
  min-width: 0;
}

.purchase-form__section + .purchase-form__section,
.purchase-form__section + .sticky-purchase-action {
  padding-top: var(--space-5);
  border-top: 0.0625rem solid var(--color-rule);
}

.purchase-form__heading {
  margin: 0;
  padding: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

/* The amount box is short — a discount is a few hundred rupees, not a line of
   prose — and its three companions sit beside it once there is room. */
.discount-amount {
  max-width: 14rem;
}

.discount-details {
  display: grid;
  gap: var(--space-3);
}

.discount-details[hidden] {
  display: none;
}

@media (min-width: 48rem) {
  .discount-details {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .discount-details__note {
    grid-column: 1 / -1;
  }
}

/* The summary sits inside its own section now, which draws the rule above. */
.purchase-form .sale-summary {
  padding-top: 0;
  border-top: 0;
}

.sale-summary__row[hidden] {
  display: none;
}

/* A summary whose every row is hidden (the group sale, paid in full) draws
   nothing, rather than a rule with nothing under it. */
.sale-summary:not(:has(> .sale-summary__row:not([hidden]))) {
  display: none;
}

/* A line's amount in a row table (loose items): a figure, right-aligned on a
   desktop like the rest of a figures column, read-only. */
.row-table__cell--figure {
  white-space: nowrap;
}

.row-table__figure {
  display: block;
  color: var(--color-depot-navy);
  font-weight: 600;
}

@media (min-width: 48rem) {
  .row-table__cell--figure {
    text-align: right;
  }
}

.balance-after {
  margin-top: var(--space-2);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.balance-after__figure {
  color: var(--color-depot-navy);
}

.sale-summary__row--discount dd {
  color: var(--color-purchased);
}

.sale-summary__row--owed dd {
  color: var(--color-shortfall);
}

.sale-summary__warning {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
}

/* The total beside the button, so the figure to say out loud is next to the
   tap that commits it — on a phone that is the fixed bar at the bottom. */
.sticky-purchase-action__bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.sticky-purchase-action__bar .btn {
  flex: 1 1 auto;
  width: auto;
}

/* Phase 13: Save & print sits beside the main button, at its own width. */
.sticky-purchase-action__bar .sticky-purchase-action__print {
  flex: 0 0 auto;
}

/* The invoice printed in place after "Save & print": loaded, never seen. */
.print-frame {
  position: fixed;
  left: -10000px;
  width: 1px;
  height: 1px;
  border: 0;
  opacity: 0;
}

.sticky-purchase-action__total {
  display: grid;
  line-height: 1.15;
}

.sticky-purchase-action__label {
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.sticky-purchase-action__figure {
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 600;
  white-space: nowrap;
}

/* The sale on the student's own page. */
.quick-sale__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
}

/* Phase 12 — the group sale, as numbered steps ------------------------------ */
.group-steps {
  display: grid;
  gap: var(--space-4);
}

.group-step__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.group-step__number {
  display: inline-grid;
  flex: none;
  width: 2rem;
  height: 2rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-accent-wash);
  color: var(--color-accent-strong);
  font-weight: 700;
}

.group-step__title {
  margin: 0;
  color: var(--color-depot-navy);
  font-size: var(--text-md);
  font-weight: 700;
}

.group-step__copy {
  margin: 0 0 var(--space-3);
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.group-sale-total {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-accent-wash);
  color: var(--color-depot-navy);
}

.group-sale-total__figure {
  font-size: var(--text-md);
  font-weight: 600;
}

/* A reassurance, not a warning — it was red, which read as something wrong. */
.required-note--calm {
  color: var(--color-muted-slate);
}

.check-count {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  font-weight: 400;
}

/*
 * Phase 12 — searchable dropdowns (Tom Select, loaded by app-enhancements.js
 * only where a long list needs it). Its stylesheet arrives after this one, so
 * these rules carry an extra `html` to win without !important.
 *
 * The control is the same white 44px field as every other. The open list shows
 * plenty at once: the theme's 200px box with 44px rows showed four options,
 * which is what "less upfront data" was. Rows are compact for a mouse and a
 * full 44px only where a finger is the pointer; type headings (Books,
 * Notebooks…) stick while scrolling; the typed letters are marked in each
 * match. Above the sticky top bar (1020) when open.
 */
html .ts-wrapper.form-select,
html .ts-wrapper.form-control {
  padding: 0;
  border-color: var(--color-field-border);
  background-color: var(--color-counter-white);
}

html .ts-wrapper.focus.form-select,
html .ts-wrapper.focus.form-control {
  border-color: var(--color-focus);
  box-shadow: var(--shadow-focus);
}

html .ts-wrapper .ts-control,
html .ts-wrapper.single.input-active .ts-control {
  min-height: calc(var(--tap-target) - 0.125rem);
  align-items: center;
  padding: 0.5rem 2.25rem 0.5rem 0.75rem;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  box-shadow: none;
  color: var(--color-ink);
  font-size: var(--text-base);
}

html .ts-wrapper .ts-control input {
  color: var(--color-ink);
  font-size: var(--text-base);
}

html .ts-wrapper .ts-control input::placeholder {
  color: var(--color-muted-slate);
}

html .ts-dropdown {
  z-index: 1060;
  margin-top: 0.25rem;
  overflow: hidden;
  border: 0.0625rem solid var(--color-field-border);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-raised);
  color: var(--color-ink);
  font-size: var(--text-base);
}

html .ts-dropdown .ts-dropdown-content {
  max-height: min(26rem, 60vh);
}

html .ts-dropdown .option {
  display: flex;
  min-height: 2.375rem;
  align-items: center;
  padding: 0.4rem 0.75rem;
  line-height: 1.3;
}

html .ts-dropdown .option + .option {
  border-top: 0.0625rem solid var(--color-ledger-grey);
}

html .ts-dropdown .optgroup-header {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 0.45rem 0.75rem;
  border-bottom: 0.0625rem solid var(--color-rule);
  background: var(--color-ledger-grey);
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

html .ts-dropdown .active {
  background: var(--color-accent-wash);
  color: var(--color-depot-navy);
}

html .ts-dropdown .selected {
  font-weight: 700;
}

html .ts-dropdown .highlight {
  border-radius: 0.125rem;
  background: rgb(227 160 8 / 0.28);
  color: inherit;
}

html .ts-dropdown .no-results {
  padding: 0.75rem;
  color: var(--color-muted-slate);
}

@media (pointer: coarse) {
  html .ts-dropdown .option {
    min-height: var(--tap-target);
  }
}

/*
 * Phase 12, client round 4 — denser on a desktop: the filter bar and the
 * record tables took too much of the screen. Only where the pointer is a
 * mouse and the screen is wide (62rem): a finger keeps every 44px target
 * (CLAUDE.md section 3), and the 360px audit, which emulates a phone, is not
 * affected. Type sizes stay as they are; the air between things is what goes.
 */
@media (min-width: 62rem) and (pointer: fine) {
  /*
   * Filters: one wrapping row. Fields share the width (the search a little
   * more), and the buttons sit at the row's end, bottom-aligned with the
   * fields — never on a row of their own under the first column, which is
   * what a grid did once the fields filled it.
   */
  .filter-panel {
    padding: 0 var(--space-3) var(--space-3);
  }

  .filter-grid {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2) var(--space-3);
    padding-top: var(--space-3);
  }

  .filter-grid > div:not(.filter-actions) {
    flex: 1 1 8rem;
    min-width: 8rem;
  }

  .filter-grid > div:has(> input[type="search"]) {
    flex: 1.6 1 11rem;
  }

  .filter-grid .form-label {
    margin-bottom: 0.125rem;
    font-size: 0.6875rem;
  }

  .filter-grid .form-control,
  .filter-grid .form-select {
    min-height: 2.25rem;
    padding-block: 0.3rem;
    font-size: var(--text-sm);
  }

  .filter-actions {
    display: flex;
    flex: 0 0 auto;
    gap: var(--space-2);
    margin-left: auto;
  }

  .filter-actions .btn {
    display: inline-flex;
    min-height: 2.25rem;
    align-items: center;
    padding: 0.3rem 0.9rem;
    font-size: var(--text-sm);
    white-space: nowrap;
  }

  /* Tables: tighter rows, and a row's buttons sized for a mouse. */
  .data-table th,
  .data-table td {
    padding: 0.45rem var(--space-3);
  }

  .data-table thead th,
  .data-table thead th.number-cell {
    padding-block: 0.5rem;
  }

  .data-table .btn,
  .data-table .btn-sm {
    min-height: 2rem;
    padding: 0.2rem 0.75rem;
    font-size: var(--text-sm);
  }

  .data-table .status-badge {
    min-height: 1.5rem;
  }

  .data-table .compact-id.d-block {
    margin-top: 0.125rem;
  }
}

/* The message after a sale or a payment, above the invoice — never printed. */
.invoice-flash:not(:has(.feedback-toast)) { display: none; }
.invoice-flash { max-width: 52rem; margin: 0 auto var(--space-4); }
@media print { .invoice-flash { display: none; } }

/* Phase 13: Loose books — the child's set, book by book, to tick. A row is the
   tap target (the label wraps the box and the title); the quantity sits at the
   end at a fixed width so the prices line up down the list. */
.set-pick {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--color-rule);
}

.set-pick__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 4.5rem;
  gap: 0.75rem;
  align-items: center;
  min-height: var(--tap-target);
  padding-block: 0.25rem;
  border-bottom: 1px solid var(--color-rule);
}

.set-pick__choice {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  min-width: 0;
  min-height: var(--tap-target);
  margin: 0;
  cursor: pointer;
}

.set-pick__choice .form-check-input {
  flex: 0 0 auto;
  margin: 0;
}

.set-pick__title {
  min-width: 0;
  overflow-wrap: anywhere;
}

.set-pick__code {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.set-pick__price {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.set-pick__qty {
  min-height: var(--tap-target);
  text-align: right;
}

/* Phase 13C: an adjustment's Add or Remove, as two tap-sized choices. */
.direction-choice {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
}

.direction-choice__option {
  display: inline-flex;
  gap: 0.5rem;
  align-items: center;
  min-height: var(--tap-target);
  margin: 0;
  cursor: pointer;
}

.direction-choice__option .form-check-input {
  margin: 0;
}

/* Phase 13C: the purchase screen's running bill total. */
.purchase-total {
  display: inline-flex;
  gap: 0.5rem;
  align-items: baseline;
}

.purchase-total__label {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.purchase-total__figure {
  font-weight: 700;
}

/* Phase 13D: schools as cards — the whole card opens the school. */
.school-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: 0.75rem;
  margin: 0;
  padding: 1rem;
  list-style: none;
}

.school-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-rule);
  border-radius: 0.5rem;
  background: var(--color-counter-white);
  box-shadow: var(--shadow-card);
}

.school-card__link {
  display: grid;
  gap: 0.35rem;
  min-height: var(--tap-target);
  padding: 0.9rem 1rem;
  color: inherit;
  text-decoration: none;
}

.school-card__link:hover,
.school-card__link:focus-visible {
  background: var(--color-accent-wash);
}

.school-card__head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  padding-right: 3.5rem;
}

.school-card__name {
  color: var(--color-depot-navy);
  font-weight: 700;
}

.school-card__meta {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.school-card__figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  font-size: var(--text-sm);
}

.school-card__edit {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
}

/* Phase 24: a class's roll, a heading per section above its rows. */
.roll-section {
  margin: 0;
  padding: var(--space-4) var(--space-4) var(--space-2);
}

/* Phase 26: the roll's selling cards, inset like the rows they replaced. */
.roll-cards {
  margin: 0 var(--space-4) var(--space-4);
}

/* Phase 24: a school's classes on the Students screen — the school cards'
   tiles, a class each, with its sections as links under the figures. */
.class-tiles {
  padding: 0;
}

.class-tile--empty .school-card__name,
.class-tile--empty .school-card__figures {
  color: var(--color-muted-slate);
}

.class-sections {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0;
  padding: 0 1rem 0.9rem;
  list-style: none;
}

.class-sections__link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--tap-target);
  padding: 0 0.75rem;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  color: var(--color-ink);
  font-size: var(--text-sm);
  text-decoration: none;
}

a.class-sections__link:hover,
a.class-sections__link:focus-visible {
  border-color: var(--color-accent);
  background: var(--color-accent-wash);
}

/* Phase 13D: a school's sets edited at once — the price column at a fixed
   width so the figures line up. */
.bulk-sets td {
  vertical-align: middle;
}

.bulk-sets__price,
.school-prices__input {
  width: 8rem;
  text-align: right;
}

.bulk-sets__name {
  display: block;
  font-weight: 600;
}

.bulk-sets__edit {
  font-size: var(--text-sm);
}

.school-sets__held {
  margin-left: 0.25rem;
  font-size: var(--text-xs);
}

/* Phase 13F: Excel / PDF / Print beside a list's own actions — one group, so
   the three wrap together rather than one at a time. */
.list-exports {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.list-exports .btn .bi {
  margin-right: 0.25rem;
}

/* Phase 14, client round 6: a search result is one line — the child's details,
   then Sell set / Loose books on the right — instead of the buttons on a row
   of their own under the card. The panel a button opens spans the full width
   underneath. A phone keeps the stacked card and its 44px buttons. */
@media (min-width: 48rem) {
  .selection-list__item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }

  .selection-list__item > .selection-list__button {
    padding-block: var(--space-3);
  }

  .selection-list__actions {
    flex-wrap: nowrap;
    padding: 0 var(--space-4) 0 0;
  }

  .selection-list__panel {
    grid-column: 1 / -1;
  }
}

@media (min-width: 62rem) and (pointer: fine) {
  .selection-list__item > .selection-list__button {
    padding-block: var(--space-2);
  }

  .selection-list__actions .btn {
    min-height: 2rem;
    padding: 0.2rem 0.7rem;
    font-size: var(--text-sm);
  }
}

/* Phase 14: "Not the one you want? Add a new student" under the results. */
.quick-student-toggle > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  color: var(--color-accent);
  font-weight: 600;
  cursor: pointer;
}

/* Phase 14: the stock count table. A narrow count box, the code under the
   title, and a row tinted by which way its count moved the stock. */
.stock-count__input {
  width: 6.5rem;
  margin-left: auto;
  text-align: right;
}

.stock-count__code {
  display: block;
  color: var(--color-muted-slate);
  font-size: var(--text-xs);
}

.stock-count__row--more [data-difference] {
  color: var(--color-purchased);
  font-weight: 700;
}

.stock-count__row--less [data-difference] {
  color: var(--color-shortfall);
  font-weight: 700;
}

/* Phase 14: stock on add — the supplier and bill belong to a purchase only. */
@supports selector(:has(*)) {
  .stock-on-add:not(:has(input[name="stock_mode"][value="purchase"]:checked)) .stock-on-add__purchase {
    display: none;
  }
}

/* Phase 14: an activity entry's full data, folded. Light: small, scrolled. */
.activity-details > summary {
  display: inline;
  color: var(--color-accent);
  cursor: pointer;
  font-size: var(--text-sm);
}

.activity-details pre {
  max-height: 16rem;
  margin: var(--space-2) 0 0;
  padding: var(--space-2);
  overflow: auto;
  border-radius: var(--radius-md);
  background: var(--color-ledger-grey);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: pre-wrap;
  word-break: break-word;
}

/*
 * Phase 15, client round 7: "out of alignment", and "the sell area and the
 * rest should be compact — we scroll too much while making a sale".
 *
 * The search results line up because the buttons have a column of their own,
 * the same width on every row whether a row offers Sell set or not (a child
 * who has bought shows only Loose books, and the narrower button area pulled
 * every column on that row to the right). On a desktop the list reads as one
 * table: the labels are shown on the first row only, as its header, and the
 * rows are joined rather than separate cards.
 */
@media (min-width: 48rem) {
  .selection-list__item {
    grid-template-columns: minmax(0, 1fr) 12.5rem;
  }

  .selection-list__actions {
    justify-content: flex-end;
  }
}

@media (min-width: 62rem) {
  .selection-list {
    gap: 0;
    overflow: hidden;
    border: 0.0625rem solid var(--color-rule);
    border-radius: var(--radius-md);
  }

  .selection-list > .selection-list__item {
    border: 0;
    border-bottom: 0.0625rem solid var(--color-rule);
    border-radius: 0;
  }

  .selection-list > .selection-list__item:last-child {
    border-bottom: 0;
  }

  .selection-list > .selection-list__item:not(:first-child) .selection-list__meta-label {
    display: none;
  }

  .selection-list__name {
    font-size: var(--text-base);
  }
}

/*
 * Compact, across the application, on a desktop with a mouse — the Phase 12
 * rule (62rem and pointer: fine) widened from filters and tables to every
 * screen. The spacing scale and the target size step down; type sizes do not.
 * A finger keeps every 44px target and the phone layout is untouched (the
 * 360px audit emulates a phone, so it measures what it always measured).
 */
@media (min-width: 62rem) and (pointer: fine) {
  :root {
    --space-3: 0.625rem;
    --space-4: 0.75rem;
    --space-5: 1rem;
    --space-6: 1.25rem;
    --space-7: 2rem;
    --tap-target: 2.25rem;
  }

  .form-control,
  .form-select {
    padding-block: 0.3rem;
  }

  .form-label {
    margin-bottom: 0.2rem;
  }

  .btn {
    padding-block: 0.3rem;
  }

  .page-header {
    margin-bottom: var(--space-4);
  }

  .data-table th,
  .data-table td {
    padding: 0.35rem var(--space-3);
  }

  .selection-list__item > .selection-list__button {
    padding-block: 0.4rem;
  }
}

/* Phase 15: the stock count's save bar — one line, kept at the bottom of the
   screen while the table scrolls. Wraps on a narrow screen. */
.stock-count-bar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: var(--space-4);
  padding: var(--space-2) var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  box-shadow: var(--shadow-card);
}

.stock-count-bar__date {
  width: 10rem;
}

.stock-count-bar__remarks {
  flex: 1 1 14rem;
  min-width: 0;
}

.stock-count-bar__summary {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

/* Phase 15: "How many sets came in" beside a class on the set import preview. */
.sets-received {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.sets-received__input {
  width: 6rem;
  text-align: right;
}

/*
 * Phase 16, client round 8: the purchase form, compacted further.
 *   - Payment (paid in full / part / not yet) on the left, Payment mode on
 *     the right, rather than one under the other.
 *   - The discount beside the totals it changes, in one row.
 *   - Room under "Mark as purchased", which ran straight into the next row.
 * From 62rem; a phone keeps the one-column form.
 */
.purchase-form__part {
  display: grid;
  gap: var(--space-3);
  min-width: 0;
  align-content: start;
}

@media (min-width: 62rem) {
  .purchase-form__payment {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    align-items: start;
    column-gap: var(--space-5);
  }

  .purchase-form__payment > .payment-received {
    align-content: start;
  }

  .purchase-form__money {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    column-gap: var(--space-6);
  }

  .sticky-purchase-action {
    padding-bottom: var(--space-4);
  }
}

/* Phase 16: a line that belongs to the whole payment row, not to one side. */
.purchase-form__payment > .purchase-form__full {
  grid-column: 1 / -1;
}

/* Phase 31: editing an invoice as an invoice — one table of what is on it,
   each book's quantity in a box, totals underneath. */
.sale-edit .sale-edit__qty {
  width: 5rem;
  margin-left: auto;
  text-align: right;
}

.sale-edit__set th {
  background: var(--color-ledger-grey);
  font-weight: 700;
}

.sale-edit__in-set td:first-child {
  padding-left: var(--space-6);
}

.sale-edit__line--off td:first-child label {
  color: var(--color-muted-slate);
  text-decoration: line-through;
}

.sale-edit__remove {
  width: 3rem;
  text-align: right;
}

.sale-edit__added select {
  min-width: 14rem;
}

.sale-edit__totals {
  display: grid;
  gap: var(--space-2);
  max-width: 26rem;
  margin: 0 0 0 auto;
}

.sale-edit__totals > div {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.sale-edit__totals dt {
  color: var(--color-muted-slate);
  font-weight: 400;
}

.sale-edit__totals dd {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 600;
}

.sale-edit__total {
  padding-top: var(--space-2);
  border-top: 0.0625rem solid var(--color-rule);
}

.sale-edit__total dd {
  color: var(--color-depot-navy);
  font-size: var(--text-lg);
}

.sale-edit__discount {
  width: 9rem;
}

/* Phase 32: a compact edit — the hint said once, the code on the title's
   line, a raised quantity marked as more, and on a desktop the totals in a
   column beside the table that stays in view while it scrolls. */
.sale-edit__hint {
  margin: 0;
  padding: var(--space-3) var(--space-4) 0;
}

.sale-edit td {
  vertical-align: middle;
}

.sale-edit .sale-edit__qty {
  width: 4.5rem;
}

.sale-edit__line--more td:first-child label {
  color: var(--color-depot-navy);
  font-weight: 600;
}

@media (min-width: 62rem) {
  .sale-edit__layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: var(--space-4);
    align-items: start;
  }

  .sale-edit__layout > .surface + .surface {
    margin-top: 0;
  }

  .sale-edit__aside {
    position: sticky;
    top: var(--space-4);
  }

  .sale-edit__totals {
    max-width: none;
  }

  .sale-edit__aside .field-grid--two {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Phase 32B: Update stock — one grid for every stock entry.

   What the entry is for is carried on the form as data-purpose, and anything
   that belongs to some purposes only names them in data-show. Six rules, one
   per purpose; with no script the server-chosen purpose still applies on
   load, and a later change simply shows everything, which the server reads
   by purpose anyway. */
.stock-move[data-purpose="purchase"] [data-show]:not([data-show~="purchase"]),
.stock-move[data-purpose="opening"] [data-show]:not([data-show~="opening"]),
.stock-move[data-purpose="add"] [data-show]:not([data-show~="add"]),
.stock-move[data-purpose="remove"] [data-show]:not([data-show~="remove"]),
.stock-move[data-purpose="send"] [data-show]:not([data-show~="send"]),
.stock-move[data-purpose="count"] [data-show]:not([data-show~="count"]) {
  display: none;
}

.purpose-choice {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 10.5rem), 1fr));
  gap: var(--space-2);
}

.purpose-choice__option {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 var(--space-2);
  align-items: center;
  min-height: var(--tap-target);
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border: 0.0625rem solid var(--color-field-border);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
  cursor: pointer;
}

.purpose-choice__option .form-check-input {
  grid-row: span 2;
  margin: 0;
}

.purpose-choice__label {
  font-weight: 600;
}

.purpose-choice__hint {
  color: var(--color-muted-slate);
  font-size: var(--text-sm);
}

.purpose-choice__option:has(.form-check-input:checked) {
  border-color: var(--color-accent);
  background: var(--color-accent-wash);
}

.stock-move__details {
  padding-top: 0;
}

.stock-grid td,
.stock-grid th {
  vertical-align: middle;
}

.stock-grid__class th {
  background: var(--color-ledger-grey);
  color: var(--color-depot-navy);
  font-weight: 700;
}

.stock-grid__set th {
  font-weight: 600;
}

.stock-grid__box {
  width: 5.5rem;
  margin-left: auto;
  text-align: right;
}

.stock-grid__from {
  color: var(--color-muted-slate);
}

.stock-grid__book.is-focus td {
  background: var(--color-accent-wash);
}

.stock-move__bar .purchase-total {
  margin-left: auto;
}

/* Four fields across on a desktop, two on a tablet. */
@media (min-width: 36rem) {
  .field-grid--four {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 62rem) {
  .field-grid--four {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Phase 32C: a school's sets edited on its page — one card per set. */
.set-cards__class + .set-cards__class {
  margin-top: var(--space-5);
}

.set-card {
  margin-bottom: var(--space-3);
  padding: var(--space-3);
  border: 0.0625rem solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-counter-white);
}

.set-card.is-changed {
  border-color: var(--color-accent);
}

.set-card--invalid {
  border-color: var(--color-shortfall, #b42318);
}

.set-card__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  align-items: center;
  margin-bottom: var(--space-2);
}

.set-card__name {
  flex: 1 1 14rem;
  margin: 0;
}

.set-card__name .form-control {
  font-weight: 600;
}

.set-card__price {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  margin: 0;
}

.set-card__price .form-control {
  width: 8rem;
  font-family: var(--font-mono);
  text-align: right;
}

.set-card__active {
  margin: 0;
}

.set-card__books td {
  vertical-align: middle;
}

.set-card .set-card__qty {
  width: 6rem;
  margin-left: auto;
  font-family: var(--font-mono);
  text-align: right;
}

.set-card__line.is-removed td:first-child {
  color: var(--color-muted-slate);
  text-decoration: line-through;
}

.set-card__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
}

.set-cards__new {
  margin-bottom: var(--space-3);
}

.set-cards__new > summary {
  min-height: var(--tap-target);
  padding-block: var(--space-2);
  color: var(--color-accent);
  font-weight: 600;
  cursor: pointer;
}

/* ==================================================================
   Phase 32E — compact: more on each screen, the same text size.

   Ishan: "more compact so that more info is shown on screen, but text
   shouldn't be small". Nothing here makes a font smaller (the filter labels
   go up), and a finger keeps its 44px targets; space is taken out of the
   gaps, the chrome and the cards that a table row does better.
   ================================================================== */

/* The fixed Bootstrap gaps on the screens (146 of them) follow the spacing
   tokens, so the compact desktop scale below reaches them too. */
.page-content .mb-3 { margin-bottom: var(--space-3) !important; }
.page-content .mb-4 { margin-bottom: var(--space-4) !important; }
.page-content .mt-3 { margin-top: var(--space-3) !important; }
.page-content .mt-4 { margin-top: var(--space-4) !important; }
.page-content .gap-3 { gap: var(--space-3) !important; }
.page-content .gap-4 { gap: var(--space-4) !important; }

/* An empty panel says so in less height. */
.empty-state {
  padding: var(--space-5) var(--space-4);
}

.empty-state__mark {
  width: 3rem;
  height: 3rem;
  margin-bottom: var(--space-3);
}

.page-header {
  margin-bottom: var(--space-3);
}

/* A phone: tighter cards, details three across where there is room. */
.mobile-record {
  padding: var(--space-3);
}

.mobile-record__details {
  margin: var(--space-2) 0;
}

@media (min-width: 30rem) {
  .mobile-record__details {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Three fields across from a tablet up. */
@media (min-width: 48rem) {
  .field-grid--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Set availability: a grid of compact cards rather than one per line. */
.set-availability {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
}

.set-availability-card {
  padding: var(--space-3);
}

.roll-section {
  padding: var(--space-3) var(--space-4) var(--space-1);
}

@media (min-width: 62rem) and (pointer: fine) {
  /* The chrome: a shorter top bar, and section tabs as underlined text. */
  .topbar {
    --topbar-row-height: 3rem;
  }

  .section-tabs {
    gap: 0 var(--space-1);
    margin-bottom: var(--space-3);
    border-bottom: 0.0625rem solid var(--color-rule);
  }

  .section-tabs__link,
  .section-tabs__link[aria-current="page"] {
    min-height: 2.25rem;
    margin-bottom: -0.0625rem;
    padding: 0 var(--space-3);
    border: 0;
    border-bottom: 0.125rem solid transparent;
    border-radius: 0;
    background: transparent;
    color: var(--color-depot-navy);
  }

  .section-tabs__link:hover {
    background: var(--color-accent-wash);
  }

  .section-tabs__link[aria-current="page"] {
    border-bottom-color: var(--color-accent);
    color: var(--color-accent);
  }

  /* Table rows on one line: a record's id beside its title, not under it. */
  .data-table td,
  .data-table th {
    line-height: 1.3;
  }

  .data-table td .compact-id.d-block {
    display: inline !important;
  }

  .data-table td .compact-id.d-block::before {
    content: " · ";
  }

  /* Schools and class tiles: one line each, two columns of them. */
  .school-cards {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 30rem), 1fr));
    gap: var(--space-2);
    padding: var(--space-3);
  }

  .school-card__link {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    align-items: baseline;
    padding: 0.5rem 0.75rem;
  }

  .school-card__head {
    padding-right: 0;
  }

  .school-card__edit {
    top: 50%;
    transform: translateY(-50%);
  }

  .school-card:has(.school-card__edit) .school-card__link {
    padding-right: 5rem;
  }

  .school-card__figures {
    margin-left: auto;
  }

  .surface__header,
  .surface__body,
  .surface__footer {
    padding: var(--space-3);
  }
}

/* Larger, not smaller: a filter label on a desktop is read, not decoded. */
@media (min-width: 62rem) and (pointer: fine) {
  .filter-grid .form-label {
    font-size: var(--text-xs);
  }
}
