﻿/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */

/* ============================================================
   ORGANIZATION THEME - Bootstrap overrides
   ------------------------------------------------------------
   Moved here from the <style> block in Views/Shared/_DetailLayout.cshtml
   (#95 review, 2026-09-01) so one stylesheet defines the palette for every
   page instead of a layout doing it inline. _ViewStart sets _DetailLayout
   for the whole application, so these rules already reached every page -
   this is a move, not a new coat of paint.

   Bootstrap's "info" is this application's primary action colour; it carries
   the One Ummah logo teal #416a64 established by #51. The !important
   flags are kept because Bootstrap's own states, such as
   .btn-info:not(:disabled):not(.disabled):active, outrank a bare class.
   ============================================================ */

/* ============================================================
   COLOUR SCHEME
   ------------------------------------------------------------
   Every colour in this file resolves through one of the custom
   properties below, so recolouring for another organisation is an
   edit to THIS BLOCK rather than a find and replace through two
   thousand lines. That is how the Masjid Jafar and One Ummah
   palettes were applied, and why near-identical values had already
   started to drift apart - the notes below flag the pairs worth
   collapsing once somebody can eyeball the pages they affect.

   In practice an organisation changes the CORE values and leaves
   the tints alone unless the new hue needs them re-derived.
   The neutrals are Bootstrap's own greys and do not vary by org.

   WHERE EACH COLOUR IS APPLIED
   Every property below is labelled with the places that use it,
   by page. Keep the label current when you add or remove a use.
     ALL           every page - Bootstrap overrides, form controls,
                   pagination, body and footer
     SIGN-UPS      Programs/SignUps, the admin list page (live), and
                   its style-guide template admin-list-page.html
     LIST-TPL      admin-list-page.html only (the payment drawer)
     SIGNUP-TPL    public-sign-up-page.html (#96)
     PROGRAMS-TPL  public-list-page.html (#97)
     DETAIL-TPL    admin-detail-page.html (#98, #99 flyer upload)
     CHECKOUT      Checkout/Index, the live checkout page (#124)
     CONFIRM       Programs/SignUpConfirmation (#125).
                   Its order and payment lines share the CHECKOUT
                   colours; only what it adds is labelled.
   "-TPL" means the style-guide template is the only place it shows
   today; those rules go live as the pages adopt the templates.
   ============================================================ */
:root {
    /* CORE
     swap these first. */
    /* Logo teal, established by #51. The app's primary action colour.
     ALL: primary buttons (.btn-info, .btn-outline-info, their disabled
       and active states), .text-info, .bg-info, .border-info,
       .badge-info, input focus border, ticked checkboxes and radios,
       pagination links and the active page.
     SIGN-UPS: metric values, the filter count bubble, quick filters
       (text, and the fill when active), active filter chip text, sort
       links and sort menu ticks, export menu icons, back link and row
       actions (kebab) on hover, show/hide toggles, family contact links,
       paid amounts, course names, the plan meter fill, session season
       text, links in the detail fields.
     LIST-TPL: classes segment of the payment bar.
     SIGNUP-TPL: description links and its "more" toggle, the edge of
       the student being edited, admin band and refund panel headings,
       entry step labels.
     PROGRAMS-TPL: the card's "more" link.
     DETAIL-TPL: drop zone border on hover and drag-over, its icon and
       the underlined "browse" word.
     ALL TEMPLATES: the default flyer's line art.
     CHECKOUT: the chosen payment option's border and radio.
       Also the payment drawer chevron and its focus ring, and the
       discount code clear X's focus ring. */
    --brand-primary: #416a64;
    /* SIGN-UPS: active quick filter on hover.
     PROGRAMS-TPL: the card's "more" link on hover and focus. */
    --brand-primary-hover: #5a3369;
    /* SIGN-UPS: active filter chip text on hover.
     Near-duplicate of --brand-primary-hover - consolidate later. */
    --brand-primary-active: #593269;
    /* ALL: primary button (.btn-info) hover fill and border. */
    --brand-primary-deep: #33534e;
    /* SIGN-UPS: the headline metric value (Total Paid).
     DETAIL-TPL: the advisory note under an upload. */
    --brand-primary-soft: #653b77;
    /* Brick. Anything that wants attention: owed, full, cancel, error.
     SIGN-UPS: Cancel sign-up and other danger menu items, the Due badge.
     LIST-TPL: sponsorship segment of the payment bar.
     SIGNUP-TPL: amount-owed total, refund amounts, the pending-change
       banner edge and heading, the change request label, full-session
       capacity text.
     PROGRAMS-TPL: seat bar and counts once a session is scarce or full.
     DETAIL-TPL: upload error text.
     CONFIRM: the Partially Paid, Past Due and Failed badge text. */
    --brand-secondary: #8a4a4a;
    /* SIGN-UPS: danger menu item text on hover and focus. */
    --brand-secondary-deep: #6f3b3b;
    /* Blue. LIST-TPL: fees segment of the payment bar. */
    --brand-accent: #608fb4;
    /* Tan. LIST-TPL: donation segment of the payment bar. */
    --brand-accent-warm: #afa083;
    /* Green, for money coming off or seats still open.
     SIGNUP-TPL: discount and credit order-line amounts.
     PROGRAMS-TPL: seat bar fill while seats are open.
     SIGN-UPS and templates: the Open badge text.
     CHECKOUT: discount amounts, the applied discount code tag,
       the Due Today badge text.
     CONFIRM: the Paid in Full and Paid badge text, the applied
       discount code tag. */
    --brand-positive: #4a6b52;
    /* teal TINTS
     grounds, borders and hovers, lightest first. */
    /* LIST-TPL: payment drawer cells and breakdown ground, the divider
       between payment segments.
     SIGNUP-TPL: admin band ground. */
    --brand-tint-25: #fbf9fc;
    /* SIGN-UPS: quick filter on hover.
     SIGNUP-TPL: refund checkbox on hover, refund amount input on focus.
     CHECKOUT: a payment option on hover. */
    --brand-tint-50: #faf6fb;
    /* SIGNUP-TPL: refund ceiling box, the paid/owed total box.
     PROGRAMS-TPL: session shelf ground. */
    --brand-tint-75: #f8f5f9;
    /* SIGN-UPS: row actions (kebab) button on hover, focus and open.
     DETAIL-TPL: flyer drop zone ground.
     CHECKOUT: the chosen payment option's ground. */
    --brand-tint-100: #f6f1f8;
    /* SIGN-UPS: session season pill, the active sort menu item, back link
       on hover, active filter chips.
     SIGNUP-TPL: refund panel heading on hover. */
    --brand-tint-125: #f5f1f7;
    /* SIGN-UPS: roster table header row, the list footer. */
    --brand-tint-150: #f5f3f7;
    /* ALL TEMPLATES: the default flyer backdrop fill.
     SIGNUP-TPL: admin band and refund panel headings.
     DETAIL-TPL: flyer drop zone on hover and focus. */
    --brand-tint-175: #f4eef7;
    /* Hairline row dividers.
     SIGN-UPS: filter menu footer, the control summary on phones.
     SIGNUP-TPL: contribution rows, order lines, payment lines, audit
       entries, entry steps; also the refund currency ground and a
       disabled refund input.
     CHECKOUT: order lines, payment schedule lines, the divider
       above the discount code.
       Also the divider inside the open payment drawer.
     CONFIRM: the divider above the action buttons. */
    --brand-tint-200: #f4f1f5;
    /* Empty tracks and image wells.
     SIGN-UPS: show/hide toggle on hover, the plan meter track.
     LIST-TPL: payment bar track.
     PROGRAMS-TPL: card image wells, the program group count pill.
     SIGNUP-TPL: flyer image well.
     DETAIL-TPL: upload row thumbnail.
     Near-duplicate of --brand-tint-175. */
    --brand-tint-225: #f3eef5;
    /* SIGNUP-TPL: refund line dividers, the Cancelled badge ground. */
    --brand-tint-250: #f1eef3;
    /* SIGN-UPS and templates: the Quiet badge ground.
     CONFIRM: the Next payment badge ground. */
    --brand-tint-275: #f0eef2;
    /* SIGN-UPS: student line and session heading dividers.
     LIST-TPL: payment breakdown border, the settled-amount divider.
     SIGNUP-TPL: course block left rule.
     CHECKOUT: the rule beside each student or attendee. */
    --brand-tint-300: #efebf1;
    /* SIGN-UPS: active filter chip on hover. */
    --brand-tint-325: #ece5ee;
    /* Focus glow.
     ALL: focus glow on inputs, custom checkboxes and radios, and their
       pressed state.
     DETAIL-TPL: drop zone while a file is dragged over it. */
    --brand-tint-350: #d9e8e5;
    /* The standard border.
     SIGN-UPS: metric cards, quick filters, the empty-state icon.
     SIGNUP-TPL: admin band and refund panel headings, refund checkboxes,
       refund ceiling box, refund line heading.
     PROGRAMS-TPL: session shelf and session rows, the seat bar track.
     DETAIL-TPL: upload rows and upload preview frames.
     CHECKOUT: payment option borders, the divider between
       programs, the rule above the Financial Summary, the rule above
       the total (which SIGNUP-TPL's .order-total picks up too), the
       empty checkout icon. */
    --brand-tint-375: #e9e0ed;
    /* SIGN-UPS: session season pill border. */
    --brand-tint-400: #e5dde8;
    /* LIST-TPL: divider above the discount line in the payment breakdown.
     Near-duplicate of --brand-tint-400. */
    --brand-tint-425: #e4dde8;
    /* SIGN-UPS and templates: the Quiet badge border.
     CONFIRM: the Next payment badge border. */
    --brand-tint-450: #e2dde6;
    /* SIGN-UPS: metric tile icons. */
    --brand-tint-475: #e2d3e8;
    /* The stronger border.
     SIGN-UPS: row actions (kebab) button border on hover, focus and open.
     SIGNUP-TPL: admin band and refund panel borders, the description
       scrollbar thumb.
     PROGRAMS-TPL: seat bar fill once a session has ended.
     DETAIL-TPL: flyer drop zone dashed border. */
    --brand-tint-500: #d7cddc;
    /* Focus ring and muted outline.
     ALL: focus border on an unticked custom checkbox or radio.
     SIGN-UPS: quick filter focus outline, back link border on hover,
       active filter chip border.
     SIGNUP-TPL: the dot between a session's day and time.
     CHECKOUT: payment option radio focus outline. */
    --brand-tint-550: #cdbcd4;
    /* SIGN-UPS: icons beside the detail field labels. */
    --brand-tint-600: #bcb1c1;
    /* SIGN-UPS: the small value under a metric. */
    --brand-tint-650: #a896b0;
    /* Muted teal-grey text and icons.
     SIGN-UPS: metric notes, flag icons, metric strip icons.
     SIGNUP-TPL: program fact icons, the order note icon, a cancelled
       student's amount, disabled and nothing-paid refund lines.
     PROGRAMS-TPL: session time icons, card fact labels, group titles,
       the shelf title, seat totals.
     CHECKOUT: student and event fact icons. */
    --brand-tint-700: #aba3ae;
    /* SIGN-UPS and templates: the row actions (kebab) icon and its menu
       icons.
     DETAIL-TPL: drop zone hint, upload row details, preview captions. */
    --brand-tint-750: #8d8894;
    /* BRICK TINTS
     the secondary hue's grounds. */
    /* SIGNUP-TPL: change request panel ground. */
    --brand-secondary-tint-25: #fdf8f7;
    /* SIGN-UPS: danger menu item on hover and focus.
     SIGNUP-TPL: the amount-owed total box. */
    --brand-secondary-tint-50: #f7efef;
    /* SIGN-UPS and templates: the Due badge ground.
     SIGNUP-TPL: the pending-change banner ground.
     CONFIRM: the Partially Paid and Past Due badge ground. */
    --brand-secondary-tint-75: #f7edea;
    /* SIGNUP-TPL: divider above the change request's decision buttons. */
    --brand-secondary-tint-100: #f2e4e0;
    /* SIGN-UPS and templates: the Due badge border.
     SIGNUP-TPL: pending-change banner and change request panel borders.
     CONFIRM: the Partially Paid and Past Due badge border. */
    --brand-secondary-tint-150: #ecd7d0;
    /* POSITIVE TINTS
     grounds for the green. */
    /* SIGN-UPS and templates: the Open badge ground.
     CHECKOUT: the Due Today badge and applied discount code tag
       ground.
     CONFIRM: the Paid in Full and Paid badge ground, the applied
       discount code tag ground. */
    --brand-positive-tint-50: #edf3ee;
    /* SIGN-UPS and templates: the Open badge border.
     CHECKOUT: the Due Today badge and applied discount code tag
       border.
     CONFIRM: the Paid in Full and Paid badge border, the applied
       discount code tag border. */
    --brand-positive-tint-150: #cfdfd3;
    /* NEUTRALS
     shared across organisations. */
    /* Bootstrap body colour, used where text must read darkest.
     SIGN-UPS: counts in "sorted by" and the result count, family,
       student and roster card names, detail field values.
     LIST-TPL: payment breakdown totals and amounts.
     SIGNUP-TPL: program fact values, refund labels and totals, change
       request details.
     PROGRAMS-TPL: card titles, session names.
     DETAIL-TPL: drop zone title, bold upload row details.
     CHECKOUT: program and event titles, student names, the
       payment due today, the discount code clear X on hover.
       Also the amount on the folded payment drawer.
     CONFIRM: order fact values. */
    --ink: #212529;
    /* Secondary body text.
     SIGN-UPS: family contact name, flags.
     LIST-TPL: payment labels.
     SIGNUP-TPL: program description, audit toggle and entries.
     PROGRAMS-TPL: session times, card excerpt and facts, group count,
       seat counts, closed sessions.
     DETAIL-TPL: drop zone text, preview captions. */
    --ink-muted: #495057;
    /* SIGN-UPS and templates: the Quiet badge text.
     CONFIRM: the Next payment badge text. */
    --ink-soft: #615d5d;
    /* ALL: site footer ground. */
    --ink-softer: #58595B;
    /* Bootstrap text-muted: labels, captions and meta text.
     SIGN-UPS: metric labels, "sorted by", result count, table headers,
       empty state, card meta, contact line, student meta, course
       session, plan caption, field labels, filter menu labels.
     LIST-TPL: payment breakdown title, discount and settled lines.
     SIGNUP-TPL: flyer dates, fact labels, student meta, student picker
       meta, unnamed and cancelled student names, detail terms, session
       options label, capacity and hint, waiver text, contribution
       prompt, order-line sub text and pending lines, order note, the
       paid heading, payment total label, payment due and sub lines,
       admin band note, audit counts, times and roles, pending banner
       note, change request time, terms and note, refund notes and
       labels, the Cancelled badge text.
     PROGRAMS-TPL: page subline, card facts, session times, seat counts
       for ended and upcoming sessions, seat notes, closed cards' title
       and excerpt.
     CHECKOUT: order-line sub text, the student count, event
       facts, section labels, the discount code label, payment option
       sub text, later payments in the schedule, the order note, the
       discount code clear X, the empty checkout message.
       Also the chosen option on the folded payment drawer.
     CONFIRM: order fact labels. */
    --muted: #6c757d;
    /* Bootstrap divider.
     SIGN-UPS: roster card divider, top of the list footer.
     SIGNUP-TPL: flyer caption, refund steps and actions, the paid order
       line. */
    --rule: #e9ecef;
    /* Bootstrap input border.
     SIGN-UPS: search button border.
     SIGNUP-TPL: refund amount input border and its currency divider. */
    --rule-strong: #ced4da;
    /* SIGN-UPS: "no plan" text. */
    --rule-mid: #adb5bd;
    /* ALL: Bootstrap's .border-top and .border-bottom utilities. */
    --rule-plain: #e5e5e5;
    /* White.
     ALL: text on the active page number.
     SIGN-UPS: metric cards, quick filters, text on the filter count
       bubble and on an active quick filter.
     SIGNUP-TPL: flyer banner, refund amount input.
     PROGRAMS-TPL: session rows.
     DETAIL-TPL: upload rows. */
    --surface: #fff;
    /* ALL: the page background (body). */
    --surface-alt: #f5f5f5;
}

.navbar-custom {
  background-color: white;
}

.navbar-custom .navbar-brand,
.navbar-custom .navbar-text {
  color: black;
}

.navbar-custom .navbar-nav .nav-item .nav-link {
  color: black;
}

.bg-cover {
  background-attachment: static;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
}

.footer-custom {
  background-color: var(--ink-softer);
  color: white;
}

/* Primary action */
.btn-info {
  background-color: var(--brand-primary) !important;
  border-color: var(--brand-primary) !important;
  box-shadow: none !important;
}

.btn-info:hover {
  background-color: var(--brand-primary-deep) !important;
  border-color: var(--brand-primary-deep) !important;
}

.btn-info:disabled {
  background-color: var(--brand-primary) !important;
  border-color: var(--brand-primary) !important;
}

.btn-info:active {
  background-color: var(--brand-primary) !important;
  border-color: var(--brand-primary) !important;
}

.text-info {
  color: var(--brand-primary) !important;
}

.bg-info {
  background-color: var(--brand-primary) !important;
}

.border-info {
  border-color: var(--brand-primary) !important;
}

.badge-info {
  background-color: var(--brand-primary) !important;
}

/* Secondary action */
.btn-outline-info {
  color: var(--brand-primary);
  border-color: var(--brand-primary);
  box-shadow: none !important;
}

.btn-outline-info:hover,
.btn-outline-info:active {
  background-color: var(--brand-primary) !important;
  border-color: var(--brand-primary) !important;
}

.form-control:focus {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 0.2rem var(--brand-tint-350);
}

/* Checkboxes and radios in the site's colour rather than the browser's or
   Bootstrap's blue. Two kinds of control need two fixes: a native input takes
   accent-color; a Bootstrap 4 custom control paints its own box and needs the
   checked and focus rules overridden. Sits here with the button and input
   overrides so every page gets it, not just the one that asked. */
input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--brand-primary);
}

.custom-control-input:checked ~ .custom-control-label::before {
  background-color: var(--brand-primary);
  border-color: var(--brand-primary);
}

.custom-control-input:focus ~ .custom-control-label::before {
  box-shadow: 0 0 0 0.2rem var(--brand-tint-350);
}

.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
  border-color: var(--brand-tint-550);
}

.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
  background-color: var(--brand-tint-350);
  border-color: var(--brand-tint-350);
}

a.navbar-brand {
  white-space: normal;
  text-align: center;
  word-break: break-all;
}

body {
    background-color: var(--surface-alt);
}

/* Sticky footer styles
-------------------------------------------------- */
html {
  font-size: 14px;
}
@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

.border-top {
  border-top: 1px solid var(--rule-plain);
}
.border-bottom {
  border-bottom: 1px solid var(--rule-plain);
}

.box-shadow {
  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}

button.accept-policy {
  font-size: 1rem;
  line-height: inherit;
}

/* Sticky footer styles
-------------------------------------------------- */
html {
  position: relative;
  min-height: 100%;
}

body {
  /* Margin bottom by footer height */
  margin-bottom: 60px;
}
.footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  white-space: nowrap;
  /* Set the fixed height of the footer here */
  height: 60px;
  line-height: 60px; /* Vertically center the text there */
}

/* Metrics summary panel
   Bordered tiles rather than filled ones, so the panel carries the house
   teal without four blocks of solid colour competing with the list.
-------------------------------------------------- */
.metric-card {
    border: 1px solid var(--brand-tint-375);
    background-color: var(--surface);
}

.metric-card .card-body {
  padding: .875rem 1rem;
}

/* The one number that matters most gets the heavier border */
.metric-value {
    font-size: clamp(1.05rem, 2.4vw, 1.5rem);
    font-weight: 700;
    line-height: 1.15;
    color: var(--brand-primary);
    white-space: nowrap;
}

/* Total paid is the headline figure, marked by a stronger figure colour
   rather than a border - the border made one tile read as selected */
.metric-card-primary .metric-value {
    color: var(--brand-primary-soft);
}

.metric-value-sub {
    font-size: .95rem;
    font-weight: 600;
    color: var(--brand-tint-650);
}

.metric-label {
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  line-height: 1.25;
  margin-top: .15rem;
}

.metric-note {
  font-size: .68rem;
  color: var(--brand-tint-700);
  line-height: 1.25;
  margin-top: .1rem;
}

.metric-icon {
    font-size: 1.5rem;
    color: var(--brand-tint-475);
    flex: none;
    margin-left: .5rem;
}

/* The tiles and the sub panel share one four column grid, so the sub panel's
   columns land directly under the tiles' instead of wrapping ragged. Inside
   each sub panel item a fixed icon / label / value track keeps the figures on
   a common edge however long the label is. */
.metrics-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: .75rem;
  margin-bottom: .75rem;
}

/* No horizontal padding here on purpose. The sub panel card spans exactly the
   same width as the tile grid, so leaving the sides unpadded makes both grids
   resolve to identical column tracks. The inset that keeps text off the card
   edge moves onto the item below, where it matches .metric-card .card-body's
   1rem - which is what puts sub panel text on the same edge as tile text. */
.metrics-strip-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: .25rem .75rem;
  padding: .6rem 0;
}

/* Icon, label and value sit together as one group rather than being pushed to
   the far edges of the column. Stretching the label to fill the track left the
   figure marooned across the card from the icon it belonged to. The group is
   left aligned and the leftover width falls at the end of the column. */
.metrics-strip-item {
  display: grid;
  grid-template-columns: 1.15rem auto auto minmax(0, 1fr);
  align-items: baseline;
  gap: .4rem;
  min-width: 0;
  padding: 0 1rem;
}

/* Soaks up the slack after the value, so the trio stays tight */
.metrics-strip-item::after {
  content: "";
}

.metrics-strip-item > i {
  justify-self: center;
  font-size: .82rem;
}

.metrics-strip-label {
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  /* Nothing here truncates. An item that cannot fit its row moves to the next
     one instead - see .metrics-strip-grid below. A clipped label is a label
     that has stopped doing its job, and "Net Sign-Up Amount" is exactly the
     one that would lose its meaning first. */
  white-space: nowrap;
}

/* Digits line up down a column rather than drifting with glyph width */
.metric-value,
.metrics-strip-item strong {
  font-variant-numeric: tabular-nums;
}

/* Below lg the fixed four column grid is what forced the choice between
   overlapping and truncating - a column is a fixed share of the width whether
   or not its contents fit. So under 992px the sub panel stops being a grid and
   starts FLOWING: each item takes exactly the width its icon, label and figure
   need, as many sit on a row as genuinely fit, and the rest move down.

   min-width: max-content is what guarantees it. It refuses to let an item
   shrink below its own content, so the only way flex can resolve an overfull
   row is to wrap - which is precisely the behaviour asked for, and it means
   nothing is ever clipped at any width. */
@media (max-width: 991.98px) {
  .metrics-strip-grid {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem 1.1rem;
  }

  .metrics-strip-item {
    flex: 0 1 auto;
    min-width: max-content;
    grid-template-columns: 1.15rem auto auto;
    padding: 0 1rem;
  }

  /* The slack soaker is a grid device; in flow it would stretch every item */
  .metrics-strip-item::after {
    display: none;
  }
}

/* Tablets and phones in landscape. Wide enough to keep four tiles in a row,
   nowhere near wide enough to keep desktop type in them once the figures grow
   past a few hundred dollars. The icons STAY - they are part of how the panel
   reads, not decoration to be dropped - so the figure is what steps down, and
   the icon steps down with it rather than disappearing. */
@media (min-width: 576px) and (max-width: 991.98px) {
  .metric-card .card-body {
    padding: .65rem .75rem;
  }

  .metric-icon {
    font-size: 1.15rem;
    margin-left: .35rem;
  }

  .metric-value {
    font-size: 1.2rem;
  }

  .metric-value-sub {
    font-size: .85rem;
  }

  .metric-label {
    font-size: .62rem;
    letter-spacing: .03em;
  }

  .metric-note {
    font-size: .62rem;
  }
}

/* Below 576px the tiles are still two across rather than stacking, so every
   metric stays in one block at the top of the page and the list is reachable
   without scrolling past it. The icons go, since the width is better spent
   on the figures. */
@media (max-width: 575.98px) {
  .metrics-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .5rem;
  }

  .metric-card .card-body {
    padding: .5rem .6rem;
  }

  /* The icons were dropped here once to buy width for the figures. They are
     back: the tile scales instead, and the icon scales with it. */
  .metric-icon {
    font-size: 1rem;
    margin-left: .3rem;
  }

  .metric-value {
    font-size: 1.05rem;
  }

  .metric-value-sub {
    font-size: .8rem;
  }

  .metric-label {
    font-size: .6rem;
    letter-spacing: .03em;
  }

  .metric-note {
    font-size: .6rem;
  }

  .metrics-strip-item {
    font-size: .85rem;
    padding: 0 .6rem;
  }

  .metrics-strip-label {
    font-size: .6rem;
  }
}

/* List control panel
   The controls that wrap an administrative list: search, filters, sorting,
   the record count and pagination. Built for the Program Sign Ups page under
   issue #342 and intended to be reused by every admin list page.
-------------------------------------------------- */
.list-control-panel .card-body {
  padding: .75rem 1rem .5rem;
}

.control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}

.control-search {
  flex: 1 1 220px;
  min-width: 0;
}

/* The panel is a column of rows rather than one wrapping flex row, so a row
   claims its own line by existing rather than by fighting for 100% width. */
.control-stack {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

/* The last row before the list: how many records and in what order on the
   left, Export on the right. Kept tight underneath - the point of putting
   Export down here was to sit against the records it writes out, and a band
   of empty space above the table undoes that. */
.control-summary {
  margin-bottom: -.25rem;
}

/* The order the list is in, said in the same words the Sort menu uses so the
   state and the control that sets it cannot drift apart. It earns its place
   most below lg, where the card list has no column headers and this is the
   only thing saying what order you are looking at. */
.sorted-by {
  font-size: .88rem;
  color: var(--muted);
}

/* A separator rather than a gap, so the count and the order read as one
   sentence about the list instead of two labels that happen to be adjacent */
.sorted-by::before {
  content: "\00b7";
  margin: 0 .5rem 0 .1rem;
  opacity: .55;
}

.sorted-by strong {
  color: var(--ink);
  font-weight: 600;
}

.sorted-by i {
  font-size: .75rem;
  opacity: .7;
  margin-left: .2rem;
}

/* Filters, Sort and Export are one set of controls, so they are one button:
   same size, same weight, same icon-then-label order. Only the menus that
   drop out of them differ. */
.ctl-btn {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
}

.ctl-btn .ctl-btn-label {
  line-height: 1;
}

/* How many criteria are narrowing the list. It carries the whole state of the
   menu once the labels drop away on a phone, so it is not decoration. */
.ctl-toggle-count {
  display: inline-block;
  min-width: 1.1rem;
  padding: 0 .25rem;
  border-radius: 999px;
  background: var(--brand-primary);
  color: var(--surface);
  font-size: .7rem;
  line-height: 1.35;
  text-align: center;
}

/* Search runs itself rather than borrowing Apply, which now belongs to the
   filter menu */
.ctl-search-go {
  border-color: var(--rule-strong);
}

/* Icon beside each preset, with the caption left-aligned under its label */
.dropdown-item.export-option {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
}

.dropdown-item.export-option i {
  width: 1rem;
  margin-top: .15rem;
  text-align: center;
  color: var(--brand-primary);
}

.dropdown-item.export-option small {
  line-height: 1.2;
}

/* Quick filters. Each one is a label wrapping its own checkbox, so it is a
   criterion you set and unset like the class above it - not a link that
   navigates the moment you touch it. Nothing in the menu commits until Apply. */
.quick-filter {
  position: relative;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--brand-tint-375);
  background: var(--surface);
  color: var(--brand-primary);
  border-radius: 20px;
  padding: .25rem .8rem;
  font-size: .82rem;
  white-space: nowrap;
  cursor: pointer;
  margin-bottom: 0;
  gap: .4rem;
}

/* The chip is the control; the box itself only has to stay reachable by
   keyboard and report its state to assistive tech. */
.quick-filter input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

.quick-filter input[type="checkbox"]:focus-visible ~ .chip-label,
.quick-filter:focus-within {
  outline: 2px solid var(--brand-tint-550);
  outline-offset: 1px;
}

.quick-filter:hover {
  background: var(--brand-tint-50);
  color: var(--brand-primary);
  text-decoration: none;
}

.quick-filter.active {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
  color: var(--surface);
}

.quick-filter.active:hover {
  background: var(--brand-primary-hover);
  color: var(--surface);
}

/* The count previews what tapping the chip would give, so it is quieter than
   the label it belongs to */
.quick-filter .count {
  opacity: .65;
  margin-left: .35rem;
}

.result-count {
  font-size: .88rem;
  color: var(--muted);
}

.result-count strong {
  color: var(--ink);
}

/* Sortable column headers */
.roster th {
  white-space: nowrap;
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--muted);
}

.roster td {
  vertical-align: middle;
  font-size: .9rem;
}

.sort-link,
.sort-link:hover {
  color: inherit;
  text-decoration: none;
}

.sort-link:hover {
  color: var(--brand-primary);
}

.sort-link i {
  opacity: .45;
  margin-left: .3rem;
}

.sort-link.sorted i {
  opacity: 1;
  color: var(--brand-primary);
}

/* Bootstrap's pagination is primary blue, the one stock component that breaks
   the house teal on these pages */
.pagination .page-link {
  color: var(--brand-primary);
}

.pagination .page-item.active .page-link {
  background-color: var(--brand-primary);
  border-color: var(--brand-primary);
  color: var(--surface);
}

.list-empty-state {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--muted);
}

.list-empty-state i {
  font-size: 2rem;
  color: var(--brand-tint-375);
  display: block;
  margin-bottom: .6rem;
}

/* Card list below lg - the table's seven columns are unreadable on a phone,
   where names wrap to two lines and the flags scroll off the edge */
.roster-card {
  border-bottom: 1px solid var(--rule);
  padding: .65rem .85rem;
}

.roster-card:last-child {
  border-bottom: 0;
}

.roster-card-name {
  font-weight: 650;
  font-size: .98rem;
  line-height: 1.25;
  color: var(--ink);
}

.roster-card-meta {
  font-size: .8rem;
  color: var(--muted);
  line-height: 1.35;
}

/* Below md the bottom bar folds into the list card, so a phone shows one
   continuous surface instead of two stacked cards */
.list-foot {
  border-top: 1px solid var(--rule);
  padding: .6rem .85rem;
}

@media (max-width: 767.98px) {
  .list-control-panel .card-body {
    padding: .6rem .7rem;
  }

  .control-row {
    gap: .4rem;
  }


  .quick-filter {
    font-size: .76rem;
    padding: .2rem .6rem;
  }
}

/* Family drawer
   The family row is the summary; its students slide out underneath. Animating
   grid-template-rows 0fr -> 1fr lets the drawer open to its natural height
   without a hard-coded max-height that would clip a family with four children.
-------------------------------------------------- */
.roster-toggle-col {
  width: 2.2rem;
}

.disclose {
  border: 0;
  background: none;
  padding: .15rem .35rem;
  color: var(--brand-primary);
  cursor: pointer;
  line-height: 1;
  border-radius: 3px;
}

.disclose:hover {
  background: var(--brand-tint-225);
}

/* Suppress the browser's OWN ring first. Left to itself Chrome draws
   -webkit-focus-ring-color, which on Windows follows the OS accent colour -
   that is where the orange box around the change history toggle came from,
   and it is why the colour was nowhere in this stylesheet. Keyboard users
   still get a ring, from the :focus-visible rule below. */
.disclose:focus {
  outline: none;
}

.disclose:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 1px;
}

.disclose i {
  transition: transform .18s ease;
  display: inline-block;
}

.disclose[aria-expanded="true"] i {
  transform: rotate(90deg);
}

.family-name {
  font-weight: 650;
  color: var(--ink);
}

/* Who signed up and how to reach them, on the row itself. Wraps rather than
   truncates - a phone number cut in half is worse than a second line. */
.family-contact-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .1rem .7rem;
  font-size: .78rem;
  color: var(--muted);
}

.family-contact-name {
  font-weight: 600;
  color: var(--ink-muted);
}

.family-contact-inline a {
  color: var(--brand-primary);
  white-space: nowrap;
}

.family-contact-inline a i {
  margin-right: .25rem;
  opacity: .75;
}

.family-paid {
  font-weight: 650;
  color: var(--brand-primary);
}

.drawer-cell {
  padding: 0 !important;
  border-bottom: 0 !important;
  background: var(--brand-tint-25);
}

.drawer-grid {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .2s ease;
}

.drawer-row.open .drawer-grid {
  grid-template-rows: 1fr;
}

.drawer-inner {
  overflow: hidden;
}

.drawer-pad {
  padding: .75rem 1rem .9rem 2.6rem;
}

.student-line {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.3fr) minmax(0, 1fr);
  gap: .35rem 1rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--brand-tint-300);
  font-size: .85rem;
}

.student-line:last-child {
  border-bottom: 0;
}

.student-line .sname {
  font-weight: 650;
  color: var(--ink);
}

.student-line .smeta {
  color: var(--muted);
  font-size: .78rem;
}

.course-name {
  font-weight: 600;
  color: var(--brand-primary);
}

/* The session the class runs in. The course description and its capacity used
   to sit here, but both just restated the class name. */
.course-session {
  color: var(--muted);
  font-size: .78rem;
}

.flag {
  font-size: .78rem;
  color: var(--ink-muted);
}

.flag i {
  width: 1rem;
  color: var(--brand-tint-700);
}

/* Held to a readable width - left to fill the cell the bar stretches so wide
   that a quarter filled stops reading as a proportion */
.roster .plan-meter {
  max-width: 190px;
}

.plan-meter {
  height: 6px;
  border-radius: 3px;
  background: var(--brand-tint-225);
  overflow: hidden;
}

.plan-meter > span {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--brand-primary);
}

.plan-caption {
  font-size: .7rem;
  color: var(--muted);
  margin-top: .2rem;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}

.plan-none {
  font-size: .82rem;
  color: var(--rule-mid);
}

/* Follows the table/card switch above - these are the CARD LIST's drawer
   proportions, so they have to start wherever the card list starts. */
@media (max-width: 991.98px) {
  .drawer-pad {
    padding: .6rem .85rem .75rem;
  }

  .student-line {
    grid-template-columns: 1fr;
    gap: .15rem;
    padding: .55rem 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .drawer-grid,
  .disclose i {
    transition: none;
  }
}

/* Session header
   Borrows the metric tiles' label style exactly - .68rem, uppercase, .06em
   tracking - so the top of the page and the panels below read as one object.
-------------------------------------------------- */
.session-head .card-body {
  padding: .85rem 1rem;
}

.session-head-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem .7rem;
  padding-bottom: .6rem;
  margin-bottom: .7rem;
  border-bottom: 1px solid var(--brand-tint-300);
}

.session-title {
  font-size: 1.05rem;
  font-weight: 650;
  margin: 0;
}

.session-season {
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--brand-primary);
  background: var(--brand-tint-125);
  border: 1px solid var(--brand-tint-400);
  border-radius: 3px;
  padding: .05rem .4rem;
}

.session-fields {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: .6rem 1.25rem;
}

.field-label {
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: .35rem;
}

.field-label i {
  color: var(--brand-tint-600);
}

.field-value {
  font-size: .82rem;
  color: var(--ink);
}

.field-value a {
  color: var(--brand-primary);
}

.field-dim {
  display: block;
  color: var(--muted);
}

@media (max-width: 991.98px) {
  .session-fields {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 575.98px) {
  .session-fields {
    grid-template-columns: 1fr;
  }
}

/* Filters menu
   One component at every width: the desktop control and the phone's Filters
   group are the same dropdown, rather than chips on one and a collapse on the
   other.
-------------------------------------------------- */
.filter-menu {
  min-width: 15rem;
  padding: .5rem;
}

.filter-menu-label {
  font-size: .66rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  margin: .35rem .1rem .3rem;
}

.filter-menu-label:first-child {
  margin-top: 0;
}

.filter-menu .ctl-class {
  width: 100%;
}

.filter-menu .quick-filter {
  display: flex;
  width: 100%;
  margin-bottom: .25rem;
}

.filter-menu .quick-filter .count {
  margin-left: auto;
}

/* Apply sits with the criteria it commits, at the foot of the menu they are
   in, rather than out on the control row where it read as a button belonging
   to the whole panel. */
.filter-menu-foot {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .5rem;
  padding-top: .5rem;
  border-top: 1px solid var(--brand-tint-200);
}

.filter-menu-foot .ctl-apply {
  margin-left: auto;
}

.filter-menu-clear {
  padding-left: 0;
}

/* Sort menu
   Built like the filter menu so the two buttons beside each other open the
   same kind of thing - but its options commit on click, the way the column
   headers always have. There is nothing to batch: two orders never compose.
-------------------------------------------------- */
.sort-menu {
  min-width: 13rem;
  padding: .5rem;
}

.dropdown-item.sort-option {
  display: flex;
  align-items: center;
  gap: .6rem;
  border-radius: 4px;
  padding: .35rem .5rem;
  font-size: .85rem;
}

.dropdown-item.sort-option i {
  width: 1rem;
  text-align: center;
  color: var(--brand-primary);
  opacity: .45;
}

/* The order in force, and which way it runs - the icon is the direction
   arrow the matching column header shows. */
.dropdown-item.sort-option.active {
  background: var(--brand-tint-125);
  color: var(--brand-primary);
  font-weight: 600;
}

.dropdown-item.sort-option.active i {
  opacity: 1;
}

/* Page head
   The title and the way back out. The return link is navigation rather than
   an action on the records, so it is built like the panel's own controls
   instead of the solid default button, which outweighed everything below it.
-------------------------------------------------- */
.list-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin: 1rem 0 .75rem;
}

.list-page-title {
  margin: 0;
  min-width: 0;
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
}

.back-link i {
  font-size: .75rem;
  opacity: .7;
}

.back-link:hover {
  background: var(--brand-tint-125);
  border-color: var(--brand-tint-550);
  color: var(--brand-primary);
}

.back-link:hover i {
  opacity: 1;
}

/* What is narrowing the list, now the chips are behind a button */
.active-filters {
  margin-top: .45rem;
}


.active-filter {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  border: 1px solid var(--brand-tint-550);
  background: var(--brand-tint-125);
  color: var(--brand-primary);
  border-radius: 999px;
  padding: .15rem .55rem;
  font-size: .78rem;
}

.active-filter:hover {
  background: var(--brand-tint-325);
  color: var(--brand-primary-active);
  text-decoration: none;
}

.active-filter i {
  font-size: .7rem;
  opacity: .7;
}

/* Fused frame
   The controls, the table header and the footer share one card with no seam
   between them, so the panel reads as attached to the list it acts on.
-------------------------------------------------- */
.roster thead th {
  background: var(--brand-tint-150);
}

.list-foot {
  background: var(--brand-tint-150);
  border-top: 0;
}

/* Control panel on small viewports
   Same three buttons in the same order as on a desktop - they only shed their
   labels. The row held its shape but not its alignment before, because the
   controls were a mix of a collapse toggle, a dropdown and a submit button of
   three different builds; they are one build now, so they line up.
-------------------------------------------------- */
@media (max-width: 767.98px) {
  .list-control-panel .card-body {
    padding: .5rem .55rem;
  }

  .control-stack {
    gap: .4rem;
  }

  .control-row {
    gap: .35rem;
  }

  /* Search shares row one with Filters and Sort, which are icon-only down
     here, so it takes the width they leave rather than claiming a line */
  .ctl-search {
    flex: 1 1 auto;
  }

  /* Filters, Sort and Export go icon only - and only these three. Each is
     recognisable by its icon, names itself again at the top of the menu it
     opens, and carries a title, so its word is pure repetition once the row
     runs out of width.

     Nothing else on the page sheds its label. Everywhere else the label is
     not repeating an icon beside it - it is the only thing saying what you
     are looking at, and a money figure under an ambiguous icon is worse than
     one that costs a line. */
  .ctl-btn .ctl-btn-label {
    display: none;
  }

  .ctl-btn {
    gap: 0;
    padding-left: .5rem;
    padding-right: .5rem;
  }

  /* The count is the only thing left saying a filter is on once the label is
     gone, so it keeps its place beside the icon */
  .ctl-btn .ctl-toggle-count {
    margin-left: .35rem;
  }

  /* The menus are as wide as the viewport allows rather than as wide as the
     button that opened them. All three are anchored to their button's RIGHT
     edge (.dropdown-menu-right) so they open inwards - Filters and Sort sit
     at the end of row one, and a menu growing rightwards from there runs off
     the screen. */
  .filter-menu,
  .sort-menu {
    min-width: 13rem;
  }

  /* Inside the menu the chips have room for their words at any width */
  .quick-filter {
    padding: .3rem .5rem;
    font-size: .8rem;
    border-radius: 6px;
    gap: .3rem;
  }

  .control-summary {
    padding-top: .45rem;
    border-top: 1px solid var(--brand-tint-200);
  }

  /* The count and the order stop sharing a line down here, and a separator
     between two lines is just an orphaned dot at the start of the second */
  .sorted-by::before {
    content: none;
  }

  /* The title and the way back out stay on one line - the link is two or
     three words and the heading is short */
  .list-page-head {
    margin: .75rem 0 .6rem;
  }

  .list-page-title {
    font-size: 1.1rem;
  }

  /* Tighter list on a phone - the card padding was doing the job twice */
  .roster-card {
    padding: .5rem .6rem;
  }

  .list-foot {
    padding: .5rem .6rem;
  }
}

/* Apply and "Clear all filters" take their teal from the organization theme
   at the top of this file, so nothing is restated here. The metrics-strip
   icons are the one place that should NOT - they are decoration beside a
   figure, and full strength teal four times over pulls the eye off the
   numbers. */
.metrics-strip .text-info {
  color: var(--brand-tint-700) !important;
}

/* ============================================================
   #95 - status badges, per row actions, and the payment breakdown
   ------------------------------------------------------------
   Colours are the One Ummah logo palette already established on this page
   by #51: teal #416a64 primary, blue #608fb4, brick #8a4a4a, tan #afa083.
   The breakdown bar reuses .plan-meter's proportions so the drawer and the row
   read as the same instrument answering two questions.
   ============================================================ */

/* ---- badges ---- */

.roster-badges {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
  margin-top: .3rem;
}

.roster-badge {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .01em;
  padding: .2rem .5rem;
  font-variant-numeric: tabular-nums;
}

/* ---- per row actions ---- */

.roster-actions-col {
  width: 1%;
}

.roster-actions-cell {
  text-align: right;
  white-space: nowrap;
}

/* Quiet until wanted - the menu is the row's least used affordance and should
   not compete with the family name for attention */
.row-actions-toggle {
  border-color: transparent;
  color: var(--brand-tint-750);
  padding: .1rem .45rem;
  line-height: 1.4;
}

.row-actions-toggle:hover,
.row-actions-toggle:focus,
.row-actions.show .row-actions-toggle {
  border-color: var(--brand-tint-500);
  background: var(--brand-tint-100);
  color: var(--brand-primary);
}

.row-actions-menu {
  min-width: 12.5rem;
  font-size: .85rem;
  padding: .3rem 0;
}

.row-actions-menu .dropdown-item {
  padding: .4rem .9rem;
  display: flex;
  align-items: center;
}

.row-actions-menu .dropdown-item i {
  width: 1.1rem;
  margin-right: .5rem;
  text-align: center;
  color: var(--brand-tint-750);
}

.row-actions-menu .dropdown-item:active i {
  color: inherit;
}

.dropdown-item-danger {
  color: var(--brand-secondary);
}

.dropdown-item-danger:hover,
.dropdown-item-danger:focus {
  background: var(--brand-secondary-tint-50);
  color: var(--brand-secondary-deep);
}

/* A disabled item still has to say why, so it keeps its cursor and tooltip
   rather than becoming inert to the pointer */
.row-actions-menu .dropdown-item.disabled {
  pointer-events: auto;
  cursor: not-allowed;
  opacity: .55;
}

.roster-card-actions {
  margin-left: .35rem;
}

/* ---- payment breakdown, in the drawer ---- */

.pay-breakdown {
  border: 1px solid var(--brand-tint-300);
  border-radius: .3rem;
  background: var(--brand-tint-25);
  padding: .6rem .75rem .65rem;
  margin-bottom: .75rem;
}

.pay-breakdown-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .75rem;
  margin-bottom: .4rem;
}

.pay-breakdown-title {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
}

.pay-breakdown-total {
  font-size: .95rem;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* flex-grow carries the raw amounts, so the segments divide the bar exactly
   and there is no rounding remainder to hide in the last one */
.pay-bar {
  display: flex;
  height: 8px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--brand-tint-225);
}

.pay-seg {
  display: block;
  min-width: 2px;
}

.pay-seg + .pay-seg {
  border-left: 1px solid var(--brand-tint-25);
}

.seg-classes { background: var(--brand-primary); }
.seg-fees { background: var(--brand-accent); }
.seg-sponsorship { background: var(--brand-secondary); }
.seg-donation { background: var(--brand-accent-warm); }

.pay-legend {
  list-style: none;
  margin: .5rem 0 0;
  padding: 0;
  font-size: .78rem;
}

.pay-legend li {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .12rem 0;
}

.pay-key {
  width: .55rem;
  height: .55rem;
  border-radius: .12rem;
  flex: 0 0 auto;
}

.pay-label {
  color: var(--ink-muted);
}

.pay-amount {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Taken off before the total exists, so it is set apart from the segments that
   add up to it */
.pay-discount {
  border-top: 1px dashed var(--brand-tint-425);
  margin-top: .25rem;
  padding-top: .3rem !important;
  color: var(--muted);
}

.pay-discount .pay-amount {
  color: var(--muted);
}

.pay-settled {
  margin-top: .45rem;
  padding-top: .4rem;
  border-top: 1px solid var(--brand-tint-300);
  font-size: .78rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* On a phone the drawer is already narrow - drop the left indent the desktop
   drawer uses to clear the disclosure arrow */
@media (max-width: 767.98px) {
  .pay-breakdown {
    padding: .55rem .6rem .6rem;
  }
}

/* ---- drawer: students left, money in the space the badges left behind ---- */

/* The third column of .student-line was almost always empty - one badge or
   nothing - so the drawer had a column of unused width running down it while
   the breakdown sat in a band above, adding height. This puts the breakdown in
   that column instead: same content, no extra vertical space. */
.drawer-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: 0 1.25rem;
  align-items: start;
}

.drawer-split .student-line {
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
}

.drawer-money .pay-breakdown {
  margin-bottom: 0;
}

/* Under lg the side column would squeeze the student names, so it drops back
   under them - stacked is better than cramped */
@media (max-width: 991.98px) {
  .drawer-split {
    grid-template-columns: minmax(0, 1fr);
  }

  .drawer-money {
    margin-top: .75rem;
  }
}

/* ---- badges, toned to the house palette ---- */

/* Stock badge-warning and badge-secondary are Bootstrap's yellow and grey, and
   next to this page's bordered white tiles and #416a64 teal they read as
   somebody else's UI dropped in. These are the same idea as .metric-card: a
   pale ground, a thin border, and the ink doing the work - so a badge sits ON
   the page rather than on top of it.

   Colours are the One Ummah logo palette already established here by #51:
   brick #8a4a4a for the one that wants attention, grey #615d5d for the one
   that is merely a fact. */

.roster-badge {
  border: 1px solid transparent;
}

/* Money outstanding. Brick rather than red - it is a thing to notice, not an
   error, and this page shows it on perfectly healthy rows. */
.roster-badge.badge-due {
  background-color: var(--brand-secondary-tint-75);
  border-color: var(--brand-secondary-tint-150);
  color: var(--brand-secondary);
}

/* A fact about the record, carrying no urgency at all */
.roster-badge.badge-quiet {
  background-color: var(--brand-tint-275);
  border-color: var(--brand-tint-450);
  color: var(--ink-soft);
}

/* ============================================================
   #96 - public sign up page
   ------------------------------------------------------------
   Global names, not page scopes - a second page that needs a flyer well
   or an order summary should reuse these rather than restate them.
   Palette is the One Ummah set already in this file: teal #416a64,
   brick #8a4a4a, and the pale grounds used by the metric tiles.
   ============================================================ */

/* ---- flyer banner ---- */

/* The container fixes the height, and the banner upload is restricted to the
   ratio it holds, so a conforming image fills it exactly. Anything that gets
   here without meeting that rule is scaled to fit inside it, WHOLE, rather
   than cropped - a flyer carries text, and a crop that loses a line of it is
   a lie about what was uploaded. See .flyer-media below for why that ratio no
   longer changes with the viewport. */
.flyer-banner {
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}

/* ONE RATIO AT EVERY WIDTH (#98 review, 2026-09-22), which is what lets the
   upload be restricted to a single resolution. This well used to be 16:9 with
   a flat 320px height above lg: fixed height against a fluid container meant
   its shape slid from 2.16:1 at the lg breakpoint to 3.47:1 on a wide screen,
   so no one upload resolution was ever right for it and "restrict to what
   displays cleanly" had no answer.

   3:1 lands the desktop banner at 370px across a full 1110px container, close
   to the 320px the old fixed height was chosen for, and stays a banner rather
   than a hero - a 16:9 ratio alone gives over 600px there and pushes the form
   off the first screen, which is what the fixed height was avoiding.

   The cost is a short strip on a phone (about 115px at 390px wide). That is
   the honest rendering of a 3:1 banner at that width; the alternative is
   cropping its sides, which is what restricting the upload exists to avoid. */
.flyer-media {
  position: relative;
  aspect-ratio: 3 / 1;
  overflow: hidden;
  background: var(--brand-tint-225);
}

.flyer-media > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  display: block;
}

/* The drawn default. Colours come from here rather than the markup, so
   another palette carries into the placeholder without a new asset. */
.flyer-default {
  width: 100%;
  height: 100%;
  display: block;
}

.flyer-default-ground {
  fill: var(--brand-tint-175);
}

.flyer-default-ink {
  stroke: var(--brand-primary);
  opacity: .45;
}

.flyer-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem .75rem;
  padding: .55rem .85rem;
  border-top: 1px solid var(--rule);
}

.flyer-dates {
  font-size: .85rem;
  color: var(--muted);
}

/* ---- program summary ---- */

/* ---- program description ----
   Stored as rich HTML, so it arrives with its own paragraphs, bold runs
   and links. The rules below give that markup a house style instead of
   letting whatever the administrator pasted set the spacing. Measure is
   capped so a long description stays readable rather than running the
   full width of the card. */
.program-description {
  color: var(--ink-muted);
  max-width: 62ch;

  /* Type scales with the viewport rather than stepping at a breakpoint.
     A phone gets slightly smaller text, which fits more words per line,
     so the block stays roughly the same SHAPE instead of growing tall -
     the same description is about two lines shorter at 390px than it
     would be at a fixed size. Bounded at both ends so it never becomes
     unreadable on a watch-sized viewport or oversized on a monitor. */
  font-size: clamp(.85rem, .8rem + .25vw, .95rem);
  line-height: 1.5;
}

.program-description p,
.program-description ul,
.program-description ol {
  margin-bottom: .6rem;
}

.program-description ul,
.program-description ol {
  padding-left: 1.2rem;
}

.program-description > *:last-child {
  margin-bottom: 0;
}

.program-description a {
  color: var(--brand-primary);
  text-decoration: underline;
}

/* Long descriptions clamp rather than render in full: the seeded Summer
   Camp row is six paragraphs and at full height it pushes the sign-up
   form off the first screen. Five lines is enough to judge whether the
   programme is the right one and decide to read on. */
.program-description-wrap:not(.open) .program-description {
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Open, it is STILL A BOX. Height is a share of the viewport rather than
   whatever the administrator pasted, so the six-paragraph Summer Camp row
   cannot push the sign-up form off a phone. Anything past the box scrolls
   inside it - the page length does not move, which is the point. */
.program-description-wrap.open .program-description {
  max-height: 40vh;
  overflow-y: auto;
  padding-right: .5rem;
  overscroll-behavior: contain;
}

/* A quiet edge so it reads as a container with more inside it, rather
   than text that happens to stop. */
.program-description-wrap.open .program-description::-webkit-scrollbar {
  width: 6px;
}

.program-description-wrap.open .program-description::-webkit-scrollbar-thumb {
  background: var(--brand-tint-500);
  border-radius: 3px;
}

.program-description-more {
  border: 0;
  background: none;
  padding: 0;
  margin-top: .45rem;
  color: var(--brand-primary);
  font-size: .85rem;
  font-weight: 600;
  cursor: pointer;
}

.program-description-more:hover {
  text-decoration: underline;
}

/* Same pairing as .disclose - kill the browser ring, then draw our own
   for keyboard users only. */
.program-description-more:focus {
  outline: none;
}

.program-description-more:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

.program-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: .9rem 1.5rem;
}

.program-fact {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-width: 0;
}

.program-fact-label {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.program-fact-label i {
  color: var(--brand-tint-700);
  margin-right: .25rem;
}

.program-fact-value {
  font-size: .88rem;
  color: var(--ink);
}

/* ---- student cards ---- */

.signup-student-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: .65rem;
  padding: .6rem .85rem;
}

.signup-student-ident {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.signup-student-name {
  font-weight: 600;
  line-height: 1.25;
}

.signup-student-meta {
  font-size: .78rem;
  color: var(--muted);
}

.signup-student-amount {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.signup-student-body {
  padding: .25rem .85rem 1rem 2.6rem;
}

/* The student being worked on gets a quiet left edge, which disappears once
   the form is complete. */
.signup-student-editing {
  border-left: 3px solid var(--brand-primary);
}

.detail-pairs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: .5rem 1.25rem;
  margin: 0 0 .75rem;
}

.detail-pairs dt {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.detail-pairs dd {
  margin: 0;
  font-size: .88rem;
}

/* ---- course, and the sessions under it ---- */

/* The indent and the rule are the only things saying these sessions belong to
   the grade above them rather than to the student in general. */
.course-block {
  border-left: 2px solid var(--brand-tint-300);
  padding: 0 0 .35rem .8rem;
  margin-bottom: .75rem;
}

.session-options {
  padding-top: .1rem;
}

.session-options-label {
  display: block;
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .3rem;
}

.session-option {
  padding-top: .15rem;
  padding-bottom: .15rem;
}

.session-capacity {
  font-size: .75rem;
  color: var(--muted);
  margin-left: .4rem;
  font-variant-numeric: tabular-nums;
}

/* Full stays visible and disabled rather than vanishing - a session that
   disappears reads as a bug, one marked Full reads as an answer. */
.session-capacity-full {
  color: var(--brand-secondary);
  font-weight: 600;
}

.session-empty-hint {
  font-size: .82rem;
  color: var(--muted);
  margin: 0;
}

.course-add {
  padding-left: 0;
}

.waiver-text {
  font-size: .82rem;
  color: var(--muted);
  margin: .15rem 0 .4rem;
  max-width: 68ch;
}

.student-add {
  border-style: dashed;
}

/* ---- contributions ---- */

.contribution {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: .25rem .75rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--brand-tint-200);
}

.contribution:last-child {
  border-bottom: 0;
}

.contribution-prompt {
  grid-column: 1;
  font-size: .78rem;
  color: var(--muted);
  margin: 0;
  padding-left: 1.5rem;
}

.contribution-amount {
  grid-column: 2;
  grid-row: 1 / span 2;
  width: 8.5rem;
}

/* ---- order summary ---- */

/* Sticky only where there is height to spare. On a phone it sits below the
   form and stays there. */
@media (min-width: 992px) {
  .order-summary {
    position: sticky;
    top: 1rem;
  }
}

.order-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: .4rem 0;
  border-bottom: 1px solid var(--brand-tint-200);
  font-size: .88rem;
}

.order-line-label {
  min-width: 0;
}

.order-line-sub {
  font-size: .75rem;
  color: var(--muted);
  display: block;
}

.order-line-value {
  white-space: nowrap;
}

/* Not yet paid for, so the total reads as a quote rather than a bill */
.order-line-pending .order-line-label,
.order-line-pending .order-line-value {
  color: var(--muted);
}

.order-line-credit .order-line-value {
  color: var(--brand-positive);
}

.order-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: .6rem 0 .75rem;
  font-weight: 700;
  font-size: 1.05rem;
}

.order-note {
  font-size: .78rem;
  color: var(--muted);
  margin: 0 0 .6rem;
}

.order-note i {
  color: var(--brand-tint-700);
}

/* ---- order groups ----
   #124. One block per program, event or order in a summary, broken down to
   the student or attendee. Built for Checkout; the confirmation page and
   email (#125, #126) mirror it, so nothing here is Checkout-specific. */
.order-group + .order-group {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--brand-tint-375);
}

.order-group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .25rem .75rem;
  margin-bottom: .25rem;
}

.order-group-title {
  margin: 0;
  font-weight: 700;
  color: var(--ink);
  min-width: 0;
}

.order-group-meta {
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
}

.order-group-facts {
  display: flex;
  flex-wrap: wrap;
  gap: .15rem 1rem;
  font-size: .8rem;
  color: var(--muted);
  margin-bottom: .25rem;
}

.order-group-facts i {
  color: var(--brand-tint-700);
  margin-right: .35rem;
}

/* One student or attendee type. The indent ties the lines to the name
   above them without a box around each person. */
.order-person {
  margin-top: .75rem;
  padding-left: .75rem;
  border-left: 2px solid var(--brand-tint-300);
}

/* The icon hangs, so a long name wraps under the name rather than the icon */
.order-person-name {
  display: flex;
  align-items: baseline;
  gap: .35rem;
  font-weight: 600;
  font-size: .9rem;
  color: var(--ink);
}

.order-person-name i {
  flex: none;
  color: var(--brand-tint-700);
}

/* A running figure: a student's net, the net sign-up amount. Heavier than
   a line, lighter than the total. */
.order-line-subtotal {
  font-weight: 600;
  border-bottom: 0;
}

/* Heads a run of lines inside a card, such as Contributions */
.order-section-label {
  margin-top: .75rem;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

.order-total {
  border-top: 2px solid var(--brand-tint-375);
  margin-top: .25rem;
}

/* Straight after a line, the total's rule takes the place of the line's
   own, so the two read as one */
.order-line + .order-total {
  margin-top: -1px;
}

/* The Financial Summary closes the Sign-Up Summary card: one column that
   adds up, leaving the other to Payment */
.order-financial {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--brand-tint-375);
}

/* ---- order facts and actions ----
   #125. The few facts that answer "did it work, and where do I stand" on
   a completed sign-up, label over value, as many to a row as fit. */
.order-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .75rem 1.25rem;
  margin: 0;
}

.order-fact {
  min-width: 0;
}

.order-fact dt {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .15rem;
}

.order-fact dd {
  margin: 0;
  font-size: .95rem;
  color: var(--ink);
  overflow-wrap: anywhere;
}

/* Secondary first, primary last, against the right edge. On touch screens
   (below 992px, as checkout) each button is a 44px target; on a phone each
   also takes a full row. */
.order-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: .5rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--brand-tint-200);
}

@media (max-width: 991.98px) {
  .order-actions .btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

@media (max-width: 575.98px) {
  .order-actions .btn {
    flex: 1 1 100%;
    display: flex;
  }
}

/* ---- discount code ---- */
.order-code {
  padding-top: .75rem;
  border-top: 1px solid var(--brand-tint-200);
}

/* Holds the field and its clear X. Takes the field's place in the input
   group, so it carries the field's flex and squared right edge. */
.order-code-field {
  position: relative;
  flex: 1 1 auto;
  width: 1%;
  min-width: 0;
}

.order-code-field .form-control {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/* Bootstrap draws the invalid X as a picture that can't be clicked; this
   button stands in its place and clears the code */
.order-code-field .form-control.is-invalid {
  background-image: none;
  padding-right: 2.5rem;
}

.order-code-clear {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 2.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.order-code-clear[hidden] {
  display: none;
}

.order-code-clear:hover {
  color: var(--ink);
}

.order-code-clear:focus {
  outline: 0;
}

.order-code-clear:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -4px;
  border-radius: 3px;
}

.order-code-label {
  font-size: .8rem;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: .25rem;
}

/* The applied code, beside its Discount Code line. Drawn like the Due
   Today badge, in the green that discounts use. */
.order-code-tag {
  display: inline-block;
  margin-left: .35rem;
  padding: 0 .4rem;
  border: 1px solid var(--brand-positive-tint-150);
  border-radius: .25rem;
  background: var(--brand-positive-tint-50);
  color: var(--brand-positive);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .04em;
  vertical-align: .05em;
}

/* A 44px tap target on phones, the field and its button together */
@media (max-width: 991.98px) {
  .order-code .form-control,
  .order-code .btn {
    height: 44px;
  }
}

/* ---- empty checkout ----
   Nothing pending, usually a Back or refresh after paying. Reuses the list
   pages' empty state; the way out is a full-size tap target on phones. */
@media (max-width: 991.98px) {
  .checkout-empty .btn {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ---- payment options ----
   A radio choice drawn as two selectable cards. The radio stays visible:
   it is what a keyboard lands on and what a screen reader announces. */
.pay-options {
  display: grid;
  gap: .5rem;
  margin-bottom: .75rem;
}

.pay-option {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  margin: 0;
  padding: .7rem .8rem;
  border: 1px solid var(--brand-tint-375);
  border-radius: .35rem;
  cursor: pointer;
}

.pay-option:hover {
  background-color: var(--brand-tint-50);
}

.pay-option.is-selected {
  border-color: var(--brand-primary);
  background-color: var(--brand-tint-100);
  box-shadow: inset 0 0 0 1px var(--brand-primary);
}

.pay-option-input {
  flex: none;
  margin-top: .3rem;
  accent-color: var(--brand-primary);
}

.pay-option-input:focus-visible {
  outline: 2px solid var(--brand-tint-550);
  outline-offset: 2px;
}

.pay-option-body {
  display: block;
  min-width: 0;
  flex: 1;
}

.pay-option-title {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  font-weight: 700;
  font-size: .92rem;
}

.pay-option-title .money {
  white-space: nowrap;
}

.pay-option-sub {
  display: block;
  font-size: .78rem;
  color: var(--muted);
  margin-top: .15rem;
}

.pay-schedule {
  margin-bottom: .75rem;
}

.pay-schedule .order-section-label {
  margin-top: 0;
  margin-bottom: .25rem;
}

/* The payment taken today leads the schedule; later ones stay muted */
.payment-line-today {
  font-weight: 600;
  color: var(--ink);
}

.payment-line-today .payment-line-sub {
  font-weight: 400;
}

.pay-consent {
  font-size: .82rem;
  margin-bottom: .75rem;
}

/* The provider partials (PayPal, Stripe) bring their own card. Inside the
   Payment card that would be a card within a card, so it is flattened. */
.pay-details .card {
  border: 0;
}

.pay-details .card-body {
  padding: 0;
}

/* The provider's section title (Billing Details, Payment Card Details)
   heads its fields, so it sits a step above them. The field labels take
   the page's own label style, the one on Discount Code. Stripe draws its
   labels in its own frame; the partial passes the same style through. */
.pay-details .card-title {
  font-size: 1.05rem;
  font-weight: 600;
  margin-bottom: .5rem;
}

.pay-details label,
.pay-details .font-weight-bold {
  font-size: .8rem;
  font-weight: 600 !important;
  color: var(--muted);
  margin-bottom: .25rem;
}

/* ---- payment drawer ----
   #124, phones and portrait tablets. Once the columns stack, the payment
   card is most of the page, so it folds down to one row - the chosen
   option and its amount - and opens in place. Built on the Sign-Ups family
   drawer (.drawer-grid, .drawer-inner) so it opens to its natural height.
   On a wide screen it is always open and the row gives way to the plain
   heading. */
.pay-drawer-toggle {
  display: none;
}

@media (max-width: 991.98px) {
  .pay-drawer-heading {
    display: none;
  }

  .pay-drawer-toggle {
    display: flex;
    align-items: center;
    gap: .75rem;
    width: 100%;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }

  /* Bootstrap outlines every focused button; keep the ring for the
     keyboard only */
  .pay-drawer-toggle:focus {
    outline: 0;
  }

  .pay-drawer-toggle:focus-visible {
    outline: 2px solid var(--brand-primary);
    outline-offset: 2px;
    border-radius: 3px;
  }

  /* Reads as the card heading it stands in for */
  .pay-drawer-title {
    flex: 1;
    font-size: 1.25rem;
    font-weight: 500;
    line-height: 1.2;
  }

  .pay-drawer-summary {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    font-size: .75rem;
    line-height: 1.25;
    color: var(--muted);
    text-align: right;
  }

  .pay-drawer-summary .money {
    font-size: 1rem;
    font-weight: 700;
    color: var(--ink);
  }

  .pay-drawer-toggle i {
    color: var(--brand-primary);
    transition: transform .18s ease;
  }

  .pay-drawer.open .pay-drawer-toggle i {
    transform: rotate(180deg);
  }

  .pay-drawer.open .drawer-grid {
    grid-template-rows: 1fr;
  }

  /* Clipped only at the card's own edge, not the content's: input focus
     glows and the Stripe partial's row gutters reach into the card padding.
     Out of the tab order while folded. */
  .pay-drawer .drawer-inner {
    margin: 0 -1.25rem;
    padding: 0 1.25rem;
    visibility: hidden;
    transition: visibility 0s linear .2s;
  }

  .pay-drawer.open .drawer-inner {
    visibility: visible;
    transition: none;
  }

  .pay-drawer-pad {
    margin-top: .6rem;
    padding-top: .9rem;
    border-top: 1px solid var(--brand-tint-200);
  }

  /* Pay and Complete Checkout are touch targets like any other, drawer
     or not */
  .pay-card .btn {
    min-height: 44px;
  }
}

@media (min-width: 992px) {
  .pay-drawer .drawer-grid {
    grid-template-rows: 1fr;
    transition: none;
  }

  .pay-drawer .drawer-inner {
    overflow: visible;
  }
}

@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {
  .pay-drawer-toggle i {
    transition: none;
  }
}

/* A second line inside a button, for the amount it will collect. Quieter
   than the label so the verb still reads first. */
.btn-subline {
  display: block;
  font-size: .74rem;
  font-weight: 400;
  opacity: .85;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
}

.money {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* ---- payments ---- */

.payment-line {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: .82rem;
  padding: .3rem 0;
  border-bottom: 1px solid var(--brand-tint-200);
}

.payment-line-due {
  color: var(--muted);
}

.payment-receipt {
  display: inline-block;
  font-size: .8rem;
  margin-top: .5rem;
}

/* ---- administrator band ---- */

/* Banded rather than sprinkled through the page. An administrator is doing a
   different job from a parent, and destructive controls should not sit inside
   the form a parent is filling in. */
.admin-band {
  border: 1px solid var(--brand-tint-500);
  border-radius: 4px;
  background: var(--brand-tint-25);
  overflow: hidden;
}

.admin-band-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .85rem;
  background: var(--brand-tint-175);
  border-bottom: 1px solid var(--brand-tint-375);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--brand-primary);
}

.admin-band-note {
  margin-left: auto;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--muted);
}

.admin-band-body {
  padding: .5rem .85rem .85rem;
}


/* ---- cancel and refund panel ----
   Review points 4, 5 and 6. Cancelling a place and giving money back are two
   decisions, so the panel keeps them as two numbered steps that do not depend
   on each other, and every money figure is an input rather than a total the
   administrator has to accept. */

.refund-panel {
  border-color: var(--brand-tint-500);
}

/* The head is the disclosure control, so it carries button resets on top
   of .disclose - which is styled for a bare chevron, not a full-width bar. */
.refund-panel-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--brand-tint-375);
  border-radius: 0;
  text-align: left;
  padding: .5rem .85rem;
  background: var(--brand-tint-175);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--brand-primary);
}

.refund-panel-head:hover {
  background: var(--brand-tint-125);
}

/* Closed, the head is the whole card - it should not look like a lid. */
.refund-panel:not(.open) .refund-panel-head {
  border-bottom-color: transparent;
}

.refund-step + .refund-step {
  margin-top: 1.25rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
}

.refund-note {
  color: var(--muted);
  font-size: .82rem;
  margin: .3rem 0 .7rem;
  max-width: 60ch;
}

.refund-note-last {
  margin: .7rem 0 0;
}

/* The whole row is the target, not just the box. */
.refund-check {
  display: flex;
  align-items: flex-start;
  gap: .55rem;
  padding: .4rem .55rem;
  margin-bottom: .25rem;
  border: 1px solid var(--brand-tint-375);
  border-radius: 4px;
  cursor: pointer;
  font-size: .88rem;
}

.refund-check:hover {
  background: var(--brand-tint-50);
}

.refund-check-body {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.refund-check-body small {
  color: var(--muted);
}

/* What was actually received. Stated before any figure can be typed, because
   it is the number every line below is bounded by. */
.refund-ceiling {
  background: var(--brand-tint-75);
  border: 1px solid var(--brand-tint-375);
  border-radius: 4px;
  padding: .6rem .75rem;
  margin: .55rem 0 .9rem;
}

.refund-ceiling-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-weight: 600;
  color: var(--ink);
}

.refund-ceiling p {
  margin: .3rem 0 0;
  font-size: .8rem;
  color: var(--muted);
  max-width: 62ch;
}

.refund-lines {
  display: flex;
  flex-direction: column;
}

.refund-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 6rem 7.5rem;
  align-items: center;
  gap: .75rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--brand-tint-250);
}

.refund-line-head {
  padding-bottom: .3rem;
  border-bottom: 1px solid var(--brand-tint-375);
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

.refund-line-head span:not(:first-child),
.refund-line-paid {
  text-align: right;
}

.refund-line-label {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
  font-size: .88rem;
  color: var(--ink);
}

.refund-line-label small {
  color: var(--muted);
}

.refund-line-paid {
  color: var(--muted);
  font-size: .88rem;
}

/* A refund figure is typed, so it looks typed. */
.refund-line-input {
  display: flex;
  align-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface);
}

.refund-currency {
  padding: .2rem .35rem;
  background: var(--brand-tint-200);
  border-right: 1px solid var(--rule-strong);
  color: var(--muted);
  font-size: .82rem;
}

.refund-line-input input {
  border: 0;
  width: 100%;
  min-width: 0;
  padding: .2rem .4rem;
  text-align: right;
  font-size: .88rem;
  font-variant-numeric: tabular-nums;
}

.refund-line-input input:focus {
  outline: none;
  background: var(--brand-tint-50);
}

.refund-line-input input:disabled {
  background: var(--brand-tint-200);
  color: var(--brand-tint-700);
}

.refund-line-none .refund-line-label,
.refund-line-none .refund-line-paid {
  color: var(--brand-tint-700);
}

.refund-line-total {
  border-bottom: 0;
  padding-top: .7rem;
  font-weight: 600;
  color: var(--ink);
}

.refund-line-total .refund-line-input {
  border: 0;
  background: none;
  justify-content: flex-end;
  padding-right: .4rem;
  font-variant-numeric: tabular-nums;
}

.refund-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;  /* secondary, then primary, against the right edge */
  gap: .5rem;
  margin-top: 1.1rem;
  padding-top: .9rem;
  border-top: 1px solid var(--rule);
}

/* A cancelled place keeps its card so the history stays legible, but it is
   plainly no longer part of what is being paid for. */
.badge-cancelled {
  background: var(--brand-tint-250);
  color: var(--muted);
}

.signup-student-cancelled .signup-student-name {
  text-decoration: line-through;
  color: var(--muted);
}

.signup-student-cancelled .signup-student-amount {
  color: var(--brand-tint-700);
  text-decoration: line-through;
}

/* Three columns of money do not fit a phone. The label keeps the full width
   and the two figures sit under it, still right aligned to each other. */
@media (max-width: 575.98px) {
  .program-description-wrap:not(.open) .program-description {
    -webkit-line-clamp: 4;
  }

  .program-description-wrap.open .program-description {
    max-height: 46vh;
  }

  .refund-line {
    grid-template-columns: minmax(0, 1fr) 7.5rem;
    row-gap: .3rem;
  }

  .refund-line-head {
    display: none;
  }

  .refund-line-label {
    grid-column: 1 / -1;
  }

  .refund-line-paid::before {
    content: "Paid ";
    color: var(--brand-tint-700);
  }
}

/* ---- audit log ---- */

/* Review point 7: the change history IS the admin section now - the
   "Administrator" bar above it said the same thing twice. The toggle
   carries the not-visible-to-the-member note, so the count sits beside
   the label and the note keeps the right edge. */
.audit-log {
  border-top: 0;
  margin-top: 0;
  padding-top: 0;
}

.audit-toggle .audit-count {
  margin-left: .35rem;
}

.audit-toggle {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  font-size: .85rem;
  font-weight: 600;
  color: var(--ink-muted);
}

.audit-count {
  margin-left: auto;
  font-weight: 400;
  font-size: .78rem;
  color: var(--muted);
}

.audit-entries {
  padding: .5rem 0 .25rem 1.6rem;
}

/* Three columns on a desktop, stacked on a phone. The role is what makes the
   log scannable - it separates what the family did from what we did. */
.audit-entry {
  display: grid;
  grid-template-columns: 11rem 12rem minmax(0, 1fr);
  gap: .15rem .9rem;
  padding: .4rem 0;
  border-bottom: 1px solid var(--brand-tint-200);
  font-size: .82rem;
}

.audit-entry:last-child {
  border-bottom: 0;
}

.audit-when {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.audit-actor {
  font-weight: 600;
}

.audit-role {
  display: block;
  font-weight: 400;
  font-size: .72rem;
  color: var(--muted);
}

.audit-what {
  color: var(--ink-muted);
}

@media (max-width: 767.98px) {
  .signup-student-head {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    row-gap: .35rem;
  }

  /* Badges drop to their own line rather than squeezing the name */
  .signup-student-badges {
    grid-column: 2 / -1;
  }

  .signup-student-body {
    padding-left: 1rem;
  }

  .contribution {
    grid-template-columns: minmax(0, 1fr);
  }

  .contribution-amount {
    grid-column: 1;
    grid-row: auto;
    width: 100%;
    max-width: 10rem;
    margin-left: 1.5rem;
  }

  .audit-entry {
    grid-template-columns: minmax(0, 1fr);
  }

  .audit-entries {
    padding-left: .5rem;
  }
}

/* ---- sign-up information ---- */

/* Mirrors the family line on the admin sign-up list page so a household reads
   the same way in a list and on this page. It is also where SIGN-UP LEVEL
   actions live: actions sit beside the thing they act on, one kebab each,
   never a second route to the same action. */
.signup-info-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .75rem;
}

.signup-info-ident {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
  min-width: 0;
}

/* ---- a student not yet named ---- */

/* The heading stands in for a name until one is typed. Muted and italic so it
   reads as a prompt rather than as somebody actually called "New student". */
.signup-student-unnamed {
  color: var(--muted);
  font-style: italic;
  font-weight: 500;
}

/* ---- paid so far, inside the same card as the cost ---- */

/* Cost and payment are one conversation - what it comes to, what has come in,
   what is left - so they share a card and are separated by a rule instead of
   by two card edges. */
.order-paid {
  border-top: 1px solid var(--rule);
  margin-top: .35rem;
  padding-top: .6rem;
}

.order-paid-head {
  display: block;
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .4rem;
}

/* Paid, Refunded, Owed - read together, so set side by side rather than
   stacked. Owed is the figure that decides what happens next and carries
   the colour the old Remaining line had. */
.payment-totals {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .4rem;
  margin-bottom: .8rem;
}

.payment-total {
  display: flex;
  flex-direction: column;
  gap: .05rem;
  padding: .45rem .55rem;
  border-radius: .35rem;
  background: var(--brand-tint-75);
  min-width: 0;
}

.payment-total-label {
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

.payment-total-value {
  font-size: .92rem;
  white-space: nowrap;
}

.payment-total-owed {
  background: var(--brand-secondary-tint-50);
}

.payment-total-owed .payment-total-value {
  color: var(--brand-secondary);
}

/* One transaction per line: when and how on the first line, what it was
   for beneath it. A refund is money going back out and is coloured as
   such rather than left to the minus sign alone. */
.payment-line-what {
  min-width: 0;
}

/* The amount never wraps; the description on the left gives way instead */
.payment-line .money {
  white-space: nowrap;
}

.payment-line-sub {
  font-size: .74rem;
  color: var(--muted);
}

.payment-line-refund .money {
  color: var(--brand-secondary);
}

@media (max-width: 767.98px) {
  .signup-info-head {
    flex-wrap: wrap;
  }
}

/* ============================================================
   #96 round three - entry order, session detail, change requests
   ============================================================ */

/* ---- numbered entry steps ---- */

/* Who first, then what. The card asks which child, then their details, then
   the grade, then the sessions that grade runs in. Grade used to come first,
   which put the child's name four blocks down - backwards, because a parent is
   adding a CHILD and only then saying what grade they are in. */
.entry-step {
  padding-bottom: .9rem;
  margin-bottom: .9rem;
  border-bottom: 1px solid var(--brand-tint-200);
}

.entry-step-last,
.entry-step:last-child {
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: 0;
}

.entry-step-label {
  display: block;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--brand-primary);
  margin-bottom: .5rem;
}

/* ---- choosing a child already on file ---- */

/* A returning family has its children on record as household members, so the
   card offers them rather than asking for a name and date of birth to be
   retyped every year. "Someone new" is the fallback, not the default. */
.student-picker {
  display: grid;
  gap: .1rem;
}

.student-picker-option {
  padding: .3rem 0 .3rem 1.75rem;
}

.student-picker-option .custom-control-label {
  font-weight: 500;
  cursor: pointer;
}

.student-picker-meta {
  display: block;
  font-weight: 400;
  font-size: .76rem;
  color: var(--muted);
}

/* ---- when a session actually runs ---- */

/* A season name on its own means nothing to a parent deciding whether they can
   make it. The dates and the times ARE the decision, so they sit on the option
   itself rather than behind a click. */
.session-option .custom-control-label {
  cursor: pointer;
}

.session-name {
  display: inline-block;
  font-weight: 600;
}

.session-when {
  display: block;
  font-size: .78rem;
  color: var(--ink-muted);
  line-height: 1.45;
}

.session-when i {
  color: var(--brand-tint-700);
}

.session-when-sep {
  color: var(--brand-tint-550);
  margin: 0 .3rem;
}

/* Capacity drops to its own line now that the dates sit above it */
.session-option .session-capacity {
  display: block;
  margin-left: 0;
}

/* ---- open requests ---- */

/* A member asks, an administrator answers, and the request has to be visible
   to both or it sits unanswered. The banner is a queue for the administrator
   and a receipt for the member. */
.pending-banner {
  border: 1px solid var(--brand-secondary-tint-150);
  border-left: 3px solid var(--brand-secondary);
  border-radius: 4px;
  background: var(--brand-secondary-tint-75);
}

.pending-banner-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem;
  padding: .5rem .8rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--brand-secondary);
}

.pending-banner-head i {
  opacity: .8;
}

.pending-banner-note {
  margin-left: auto;
  font-weight: 400;
  font-size: .78rem;
  color: var(--muted);
}

/* The request itself, on the student it concerns, so the ask and the record it
   applies to are read together. */
.request-panel {
  border: 1px solid var(--brand-secondary-tint-150);
  border-radius: 4px;
  background: var(--brand-secondary-tint-25);
  padding: .7rem .85rem;
  margin-bottom: .9rem;
}

.request-panel-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .35rem .75rem;
  margin-bottom: .5rem;
}

.request-kind {
  font-size: .82rem;
  font-weight: 600;
  color: var(--brand-secondary);
}

.request-when {
  font-size: .75rem;
  color: var(--muted);
}

/* What the member asked, in their words. A request reduced to "change
   requested" tells an administrator nothing they can act on. */
.request-detail {
  display: grid;
  gap: .3rem .9rem;
  margin: 0 0 .7rem;
  font-size: .85rem;
}

.request-detail > div {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: .9rem;
}

.request-detail dt {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  padding-top: .15rem;
}

.request-detail dd {
  margin: 0;
  color: var(--ink);
}

/* ADMIN ONLY. A member sees the panel above without these, which is what turns
   it from a queue item into a receipt. */
.request-decide {
  display: flex;
  align-items: center;
  justify-content: flex-end;  /* secondary, then primary, against the right edge */
  flex-wrap: wrap;
  gap: .5rem .75rem;
  padding-top: .6rem;
  border-top: 1px solid var(--brand-secondary-tint-100);
}

.request-decide-note {
  font-size: .74rem;
  color: var(--muted);
  flex: 1 1 12rem;
}

@media (max-width: 767.98px) {
  .request-detail > div {
    grid-template-columns: minmax(0, 1fr);
    gap: .1rem;
  }

  .pending-banner-note {
    margin-left: 0;
    flex-basis: 100%;
  }

  .request-decide-note {
    flex-basis: 100%;
  }
}

/* ============================================================
   #97 - public list page
   ------------------------------------------------------------
   Detail rows for the public Programs list. Global names, so the
   same row can carry any public listing that pairs an image with a
   description and a strip of facts. The flyer default (.flyer-default
   and the girih pattern) is shared with the sign-up page's banner.
   ============================================================ */

.page-subline {
  font-size: .85rem;
  color: var(--muted);
  margin: .1rem 0 0;
}

.program-list {
  display: flex;
  flex-direction: column;
  gap: .85rem;
}

/* Thumbnail and body. The thumbnail is fixed so the rows line up down
   the page; the body takes whatever is left. The actions are not a
   column of their own - they live on the title line inside the body,
   which is what keeps them level with the title at every width. */
.program-card {
  display: grid;
  grid-template-columns: 14.5rem minmax(0, 1fr);
  gap: 0 1.15rem;
  align-items: center;
  padding: .9rem;
}

/* ---- the well ----
   Same idea as .flyer-media on the sign-up page, and like it this well now
   holds ONE ratio at every width (#98 review, 2026-09-22) - it used to open
   to 21:9 below sm, which meant a thumbnail restricted to 16:9 would still
   have been cropped on a phone. The thumbnail upload is restricted to that
   ratio, so a conforming image fills the well exactly; anything that gets
   here without meeting the rule is scaled to fit WHOLE rather than cropped,
   and the backdrop fills what it leaves over. Either way every row in the
   list is the same height. */
.program-card-media {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 3px;
  background: var(--brand-tint-225);
}

.program-card-media > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  display: block;
}

/* .flyer-default already fills its box; the pattern it paints with is
   defined once at page level, not inside each card. */

/* ---- the body ---- */
.program-card-body {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  min-width: 0;
}

/* Title on the left, actions on the right, and the two never wrap onto
   separate lines - the title can break across lines, the actions cannot
   be pushed under it. This one rule is why the kebab reads in the same
   place on a desktop, a tablet and a phone. */
.program-card-titleline {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: .5rem .75rem;
}

/* The name and its badge, wrapping between themselves as they need to. */
.program-card-titles {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem .5rem;
}

.program-card-title {
  font-size: 1.05rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink);
}

/* Stripped to text server-side and clamped to two lines here, so a
   four-word description and a nine-line one land the same height. */
.program-card-excerpt {
  font-size: .88rem;
  color: var(--ink-muted);
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Read more lifts the clamp in place. The paragraph stays a box so the
   transition between the two states does not reflow the facts below
   it any more than the extra lines require. */
.program-card-excerpt.is-open {
  -webkit-line-clamp: unset;
  line-clamp: unset;
}

/* The toggle is a text button, not a link - it changes the page, it
   does not leave it. Sized to the excerpt it controls, and only shown
   by the page script when there is actually more to read. */
.program-card-more {
  align-self: flex-start;
  margin: -.2rem 0 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: .82rem;
  font-weight: 600;
  color: var(--brand-primary);
  cursor: pointer;
}

.program-card-more:hover,
.program-card-more:focus {
  color: var(--brand-primary-hover);
  text-decoration: underline;
  outline: none;
}

/* Label then value, so the same fact can be read down the page. */
.program-card-facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .2rem 1rem;
  font-size: .8rem;
  color: var(--muted);
}

.program-card-facts li {
  display: flex;
  align-items: baseline;
  gap: .35rem;
  white-space: nowrap;
}

.program-card-facts .fact-k {
  color: var(--brand-tint-700);
}

.program-card-facts b {
  color: var(--ink-muted);
  font-weight: 600;
}

/* ---- the page header ----
   Was a col-sm-8 / col-sm-4 row, which stacks below 576px and drops the
   Setup button under the title. A flex row that never wraps keeps the
   button on the title's line at every width. */
.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: nowrap;
}

.page-head-titles {
  min-width: 0;
}

.page-head-action {
  flex: 0 0 auto;
  white-space: nowrap;
}

.page-head-action .page-head-action-label {
  margin-left: .5rem;
}

/* ---- the groups ----
   Open, Upcoming and Closed. A quiet rule with the count beside it: the
   groups are signposts, not headlines, and the cards below them should
   stay the loudest thing on the page. */
.program-group + .program-group {
  margin-top: 1.6rem;
}

.program-group-title {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 0 0 .65rem;
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--brand-tint-700);
}

.program-group-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.35rem;
  padding: 0 .35rem;
  border-radius: 999px;
  background: var(--brand-tint-225);
  color: var(--ink-muted);
  font-size: .72rem;
  letter-spacing: 0;
}

/* Closed is a <details>, so the fold costs no JavaScript. The marker is
   replaced by a chevron that turns, and the whole summary is the target. */
.program-group-title-fold {
  cursor: pointer;
  list-style: none;
}

.program-group-title-fold::-webkit-details-marker {
  display: none;
}

.program-group-title-fold::before {
  content: "\f054"; /* fa-chevron-right */
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free";
  font-weight: 900;
  font-size: .62rem;
  transition: transform .15s ease;
}

.program-group-fold[open] .program-group-title-fold::before {
  transform: rotate(90deg);
}

.program-group-fold[open] .program-group-title-fold {
  margin-bottom: .65rem;
}

/* ---- the sessions ----
   The heart of the row. One line per session, because seats, dates and
   the sign-up window all belong to the session and not to the program.

   THE SHELF. The sessions sit on their own tinted surface inside the
   card, each on a white chip. Without it the block reads as a table
   bolted onto the bottom of a card, its rules fighting the card's own
   border; a card containing a shelf of things is what it actually is.

   Four columns: what it is called, when it runs, how many seats are
   left, and the one thing to do about it. The seats column is fixed so
   the figures line up down the card and can be compared at a glance -
   that comparison is the whole reason the block exists. */
.session-shelf {
  margin-top: .55rem;
  padding: .5rem .65rem;
  background: var(--brand-tint-75);
  border: 1px solid var(--brand-tint-375);
  border-radius: 4px;
}

.session-shelf-title {
  margin: 0 0 .35rem;
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--brand-tint-700);
}

.session-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}

.session-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) 8rem;
  align-items: center;
  gap: .1rem .85rem;
  padding: .45rem .6rem;
  background: var(--surface);
  border: 1px solid var(--brand-tint-375);
  border-radius: 3px;
  font-size: .82rem;
}

.session-name {
  font-weight: 600;
  color: var(--ink);
  min-width: 0;
}

.session-when {
  color: var(--muted);
  min-width: 0;
}

/* ---- the capacity rail ----
   The registrar's 20/30 made visual: fullness is a proportion, and a
   proportion is read faster as a length than as arithmetic between two
   numbers. The width is set inline because Razor computes it per
   session - spots taken over TotalSpots.

   The rail alone would be ambiguous, because a nearly-full bar looks
   like progress toward something good. The count beside it is what
   disambiguates, so the count never shrinks to make room for the rail. */
.session-seats {
  min-width: 0;
}

.session-seats-count {
  display: block;
  color: var(--ink-muted);
  white-space: nowrap;
  /* so the figures line up down the card */
  font-variant-numeric: tabular-nums;
}

.session-seats-count b {
  font-weight: 700;
}

.session-seats-of {
  color: var(--brand-tint-700);
  font-weight: 400;
}

.seats-rail {
  display: block;
  height: 3px;
  margin-top: .25rem;
  border-radius: 999px;
  background: var(--brand-tint-375);
  overflow: hidden;
}

/* Semantic, and deliberately not the brand hue - teal stays on the
   buttons, so the rail never competes with the thing to click. */
.seats-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand-positive);
}

/* Nearly gone. Brick, the same colour badge-due already uses for money
   outstanding - a thing to notice, not an error. */
.session-seats-scarce .seats-fill {
  background: var(--brand-secondary);
}

.session-seats-scarce .session-seats-count {
  color: var(--brand-secondary);
}

.session-seats-scarce .session-seats-of {
  color: var(--brand-secondary);
  opacity: .8;
}

/* Full - still an active session, there is simply no room. It keeps the
   scarce colour at the end of its run, so "3 left" and "Full" read as
   the same story at two points, and it stays on its white chip because
   its window is open. What it does not get is a button. */
.session-seats-full .seats-fill,
.session-seats-full .session-seats-count {
  color: var(--brand-secondary);
}

.session-seats-full .seats-fill {
  background: var(--brand-secondary);
}

.session-seats-full .session-seats-of {
  color: var(--brand-secondary);
  opacity: .8;
}

/* Ended - the rail stays, because how full it got remains the fact
   being reported, but it stops carrying any urgency. This is the state
   that goes grey; full does not. */
.session-seats-ended .seats-fill {
  background: var(--brand-tint-500);
}

.session-seats-ended .session-seats-count,
.session-seats-soon .session-seats-count {
  color: var(--muted);
  font-weight: 400;
}

/* Not open yet: there is no count to act on, so there is no rail. */
.session-seats-soon .seats-rail {
  display: none;
}

/* Not open yet, but the seats are known: the figure stays on the count
   line, level with every other row, and the date the window opens goes
   underneath in the slot the rail occupies on a live session. Written as
   one phrase it runs along the dates instead, which on a phone leaves it
   floating against nothing. */
.session-seats-note {
  display: block;
  margin-top: .15rem;
  font-size: .76rem;
  color: var(--muted);
  white-space: nowrap;
}

/* A session that is over or not yet open keeps its line - a parent
   needs to see that the Saturday class existed, or is coming - but it
   gives up its white chip so it stops competing with the ones that are
   live. A FULL session is live, so it keeps the chip (see above). */
.session-row-closed,
.session-row-upcoming {
  background: transparent;
  border-style: dashed;
}

.session-row-closed .session-name {
  color: var(--ink-muted);
  font-weight: 500;
}

/* ---- the actions ----
   On the title line, right-aligned, secondary first and primary last.
   Sign Up is a program-level action - the sign-up page is one form per
   program with the sessions as checkboxes - so the one button sits here
   and the session rows carry none. The kebab holds the secondaries: a
   member's history, and for an administrator the two management items
   above it. The primary stays a visible button for every role. */
.program-card-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .4rem;
}

.program-card-actions .btn {
  white-space: nowrap;
}

/* A place is available: the one positive signal on the page. */
.roster-badge.badge-open {
  background-color: var(--brand-positive-tint-50);
  border-color: var(--brand-positive-tint-150);
  color: var(--brand-positive);
}

/* ---- a closed program ----
   The card steps back as a whole so a finished term cannot be mistaken
   for something still on offer while scrolling. */
.program-card-closed .program-card-media {
  filter: grayscale(1);
  opacity: .55;
}

.program-card-closed .program-card-title,
.program-card-closed .program-card-excerpt {
  color: var(--muted);
}

/* Below lg the thumbnail gives up width. The session rows lose the
   separate "when" column first - the dates fold under the session name,
   which is where they read anyway. */
@media (max-width: 991.98px) {
  .program-card {
    grid-template-columns: 11rem minmax(0, 1fr);
    gap: .6rem 1rem;
  }

  .session-row {
    grid-template-columns: minmax(0, 1fr) 9.5rem;
  }

  .session-name {
    grid-column: 1;
  }

  .session-when {
    grid-column: 1;
    grid-row: 2;
  }

  .session-seats {
    grid-column: 2;
    grid-row: 1 / span 2;
  }
}

/* On a phone the card stacks: the well opens to full width at a shallower
   ratio so it reads as a banner rather than a block, and the title line
   - actions included - comes straight under it. The session rows keep
   the two-column shape from below lg - name and dates stacked on the
   left, seats on the right - which is a better use of a narrow row than
   three stacked lines; the seats column just stops being fixed width. */
@media (max-width: 575.98px) {
  .program-card {
    grid-template-columns: minmax(0, 1fr);
    gap: .75rem;
    padding: .75rem;
  }

  .program-card-facts li {
    white-space: normal;
  }

  /* One seats column shared by every session in the card. Each row is
     its own chip and so its own grid, and left alone each one sizes
     that column to its own text - the figures then start at a
     different x on every line. Subgrid hands the rows the list's
     columns, so the widest cell sets the width once and the figures
     line up down the card the way they do at full width. */
  .session-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: .35rem .75rem;
  }

  .session-row {
    grid-column: 1 / -1;
    /* the per-row sizing, for anything that cannot take subgrid */
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-columns: subgrid;
    gap: .15rem .75rem;
    padding: .5rem .6rem;
  }

  /* The count carries nowrap so a figure never breaks across lines mid
     phrase. On a phone the column is too narrow to honour that for the
     longer phrases, so they are allowed to wrap here - the figure itself
     still holds together. */
  .session-seats-count {
    white-space: normal;
  }

  /* The header keeps one line by dropping the button's label rather than
     the button - the icon alone is unambiguous in the corner it has
     always occupied, and the title stops being squeezed. */
  .page-head-action .page-head-action-label {
    display: none;
  }
}

/* ============================================================
   #98 - image upload
   ------------------------------------------------------------
   A drop zone and the row that replaces it once a file is on hand.
   Global names: nothing here says flyer or program, so the same
   component can take a member photo or a receipt scan. The behaviour
   lives in style-guide/js/admin-detail-page.js and binds to data-upload
   attributes, not to these classes.
   ============================================================ */

/* ---- the zone ----
   A <label> around a real file input, so a click anywhere inside it
   opens the picker with no script. position: relative is for the input
   stacked underneath it. */
.dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .2rem;
  min-height: 9.5rem;
  margin: 0;
  padding: 1.25rem;
  text-align: center;
  border: 2px dashed var(--brand-tint-500);
  border-radius: 4px;
  background: var(--brand-tint-100);
  color: var(--ink-muted);
  cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease;
}

/* Hover, keyboard focus on the input underneath, and a file being
   dragged over it all read the same way: the zone wakes up. The dragged
   state goes one step further and solidifies the border, because at
   that moment it is a target, not an invitation. */
.dropzone:hover,
.dropzone:focus-within {
  border-color: var(--brand-primary);
  background: var(--brand-tint-175);
}

.dropzone.is-over {
  border-style: solid;
  border-color: var(--brand-primary);
  background: var(--brand-tint-350);
}

/* The input is not display:none - that would take it out of the tab
   order and the label would be the only way in. It sits under the label
   at zero opacity, the same size, so keyboard focus lands on it and
   :focus-within above lights the zone. */
.dropzone-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.dropzone-icon {
  font-size: 1.75rem;
  color: var(--brand-primary);
  margin-bottom: .25rem;
}

.dropzone-title {
  font-size: .95rem;
  font-weight: 600;
  color: var(--ink);
}

.dropzone-title u {
  color: var(--brand-primary);
  text-decoration-color: var(--brand-primary);
  text-underline-offset: 2px;
}

/* One title per breakpoint: dragging is a desktop verb. */
.dropzone-title-tap {
  display: none;
}

.dropzone-hint {
  font-size: .8rem;
  color: var(--brand-tint-750);
}

/* Rejected file. Brick, because it is a thing to notice and fix, not a
   failure of the page - and it sits where the zone was so it reads as
   the zone's answer. */
.dropzone-error {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: .5rem 0 0;
  font-size: .85rem;
  color: var(--brand-secondary);
}

/* ---- the note ----
   Advice under an image that was ACCEPTED but will not fill its area -
   the wrong shape, or short of the recommended size. Muted rather than
   brick, because nothing went wrong: the image is in and will show. */
.upload-note {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  margin: .5rem 0 0;
  font-size: .85rem;
  color: var(--brand-primary-soft);
}

/* ---- the row ----
   What replaces the zone once a file is on hand: a small well with the
   whole image scaled to fit, the file's name and size, and the two
   things to do about it. Secondary first, primary last. */
.upload-row {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 1rem;
  align-items: center;
  padding: .75rem;
  border: 1px solid var(--brand-tint-375);
  border-radius: 4px;
  background: var(--surface);
}

/* Fit, never crop. A flyer carries text, and a thumbnail that loses a
   line of it misreports what was uploaded. Wherever the image does not
   reach, the blurred backdrop fills in - see FITTED-IMAGE BACKDROP
   below; the tint is what shows before anything is picked.

   The well takes the shape the component accepts: the upload script
   sets --upload-ratio from data-upload-ratio, so a 3:1 banner shows as
   a strip rather than as a strip with blurred bands above and below
   it. 16:9 is the fallback for a component that declares no ratio. */
.upload-row-thumb {
  position: relative;
  aspect-ratio: var(--upload-ratio, 16 / 9);
  border-radius: 3px;
  overflow: hidden;
  background: var(--brand-tint-225);
}

.upload-row-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
}

.upload-row-meta {
  min-width: 0;
  font-size: .85rem;
}

.upload-row-meta b {
  display: block;
  color: var(--ink);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.upload-row-meta span {
  display: block;
  color: var(--brand-tint-750);
  font-size: .8rem;
  font-variant-numeric: tabular-nums;
}

/* In the panel header, level with the title, the way every other
   panel carries its Edit - so the section matches the ones around it
   and the buttons hold still when the row below them appears. */
.upload-actions {
  display: flex;
  gap: .4rem;
}

.upload-actions .btn {
  white-space: nowrap;
}

/* On a phone the row stacks: well, then meta. The header actions keep
   their line beside the title. The zone's title swaps to the touch
   verb. */
@media (max-width: 575.98px) {
  .dropzone {
    min-height: 7.5rem;
    padding: 1rem;
  }

  .dropzone-title-drag {
    display: none;
  }

  .dropzone-title-tap {
    display: inline;
  }

  .upload-row {
    grid-template-columns: minmax(0, 1fr);
    gap: .6rem;
  }
}

/* ============================================================
   #99 - upload preview
   ------------------------------------------------------------
   One frame under each drop zone, showing the picked image the way the
   public will see it. It is not an imitation: the thumbnail panel's frame
   IS .program-card-media and the banner panel's IS .flyer-media, at the
   width each well has on its own page, so whatever rule the well follows -
   ratio, breakpoints, default pattern - the preview follows by definition.
   Change the well and the preview cannot drift.

   ONE FRAME PER PANEL since the thumbnail and banner became separate,
   restricted uploads (#98 review, 2026-09-22). While a single flyer had to
   serve both wells, a panel showed both frames so the administrator could
   see the compromise; now each image has one destination and there is no
   compromise to show. The captions lost their per-breakpoint variants at
   the same time, because neither well changes shape with the viewport any
   more - that is what made a single accepted resolution possible.
   ============================================================ */

.upload-preview {
  margin-top: 1rem;
}

.upload-preview-cap {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .5rem;
  margin-bottom: .35rem;
  font-size: .8rem;
  color: var(--ink-muted);
}

.upload-preview-cap b {
  font-weight: 600;
}

.upload-preview-cap span {
  color: var(--brand-tint-750);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The wells carry a thin border here that they do not carry on their
   own pages, because here they sit on a white card rather than inside
   one, and an empty tinted rectangle needs an edge to read as a frame.

   content-box, because aspect-ratio follows box-sizing. Under the site's
   border-box the ratio took the border in, the space INSIDE it came out
   a fraction taller than 16:9 or 3:1, and a conforming image - fitted
   whole - left a half-pixel gap at each side where the dimmed backdrop
   showed as a dark hairline. Drawn outside the ratio, the border leaves
   the inside the exact shape the upload is restricted to, and the
   max-widths below measure the image area, the same width the well has
   on its own page. */
.upload-preview .program-card-media,
.upload-preview .flyer-media {
  box-sizing: content-box;
  border: 1px solid var(--brand-tint-375);
}

/* The thumbnail frame is held to the width the list card gives it -
   14.5rem at lg and above, 11rem below that, full width on a phone - so
   the preview is the real size rather than a stretched one. The banner
   frame takes the panel's full width, which is what it has on the
   sign-up page, so it needs no cap. */
.upload-preview .program-card-media {
  max-width: 14.5rem;
}

@media (max-width: 991.98px) {
  .upload-preview .program-card-media {
    max-width: 11rem;
  }
}

@media (max-width: 575.98px) {
  .upload-preview .program-card-media {
    max-width: none;
  }
}

/* ============================================================
   FITTED-IMAGE BACKDROP
   ------------------------------------------------------------
   Every image well on this build fits rather than crops, because a
   flyer carries text and a crop that loses a line of it misreports
   what was uploaded. The cost is that a flyer whose shape does not
   match the frame leaves the well's ground showing - at the sides for
   a portrait, above and below for a wide banner.

   This fills that space with the same image, blurred and dimmed,
   behind the one that fits. The frame reads as one picture instead of
   a picture parked on a slab, and nothing is hidden: the whole flyer
   is still there, at full size, in front. Where the upload already
   matches the frame the backdrop is covered completely and costs
   nothing, so it needs no measuring and no second state.

   Global name: a well says has-media-backdrop and puts an
   img.media-backdrop before the image it backs. Nothing here says
   flyer or program.
   ============================================================ */

/* Typed as img.media-backdrop rather than .media-backdrop alone, to
   match the specificity of the wells' own `> img` rules - those set
   object-fit: contain, and a bare class would lose to them. */
img.media-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* Scaled past the frame because a blur samples beyond the element's
     own edge; at 1:1 the border fades out and leaves a pale halo where
     the ground still shows through. */
  transform: scale(1.12);
  filter: blur(18px) brightness(.68) saturate(1.15);
  z-index: 0;
  /* It is the same picture as the one in front, so it is decoration:
     no pointer target, and aria-hidden in the markup keeps a screen
     reader from hearing the alt text twice. */
  pointer-events: none;
}

/* The fitted image is in normal flow, and an absolutely positioned
   backdrop would paint over it whatever its z-index. Lifting it into
   the same stacking context puts it back in front. */
.has-media-backdrop > img:not(.media-backdrop) {
  position: relative;
  z-index: 1;
}

/* ---- Program custom-fields section (issue #116) ---- */

/* Subheading rule in the house palette (brand-tint-375) rather than the plain
   grey the global .border-bottom uses - nothing in the UI is a flat grey. */
.cf-subheading {
  border-bottom: 1px solid #e9e0ed;
}

.cf-subheading-note {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* .cf-subheading already draws the rule under the level heading; without
   this, Bootstrap's default border-top on the first table row draws a
   second line right underneath it. */
.cf-table-wrap table thead th {
  border-top: 0;
}

/* Config badges follow the same idiom as .roster-badge: a pale ground, a thin
   border, and the ink doing the work, in the One Ummah logo palette. */
.cf-badge {
  border: 1px solid transparent;
  font-weight: 600;
}

/* Required - pale teal, the identity hue */
.cf-badge.cf-required {
  background-color: #f4eef7;
  border-color: #e9e0ed;
  color: #416a64;
}

/* Optional / Inactive - quiet grey, a fact carrying no urgency */
.cf-badge.cf-optional,
.cf-badge.cf-inactive {
  background-color: #f0eef2;
  border-color: #e2dde6;
  color: #615d5d;
}

/* Active - the positive/available signal */
.cf-badge.cf-active {
  background-color: #edf3ee;
  border-color: #cfdfd3;
  color: #4a6b52;
}

/* Same palette as .cf-badge, as plain colored text for the mobile card's
   meta line, which reads as a compact fact list rather than badges. */
.cf-meta-required,
.cf-meta-active {
  font-weight: 600;
}

.cf-meta-required {
  color: #416a64;
}

.cf-meta-optional,
.cf-meta-inactive {
  color: #615d5d;
}

.cf-meta-active {
  color: #4a6b52;
}

/* The search bar grows to fill the row instead of stopping at a fixed
   width; the note keeps its own space alongside it, with a gap between
   them at any viewport now that the bar can grow right up to it. */
.cf-map-search {
  flex: 1 1 auto;
  margin-right: .75rem;
}

.cf-map-note {
  flex: 0 0 auto;
}

@media (max-width: 767.98px) {
  .cf-map-note {
    margin-top: .5rem;
  }
}

/* Map Custom Fields modal picker list - checkbox + label, with the field's
   name/type as a meta line underneath. Same layout at every viewport. */
.cf-pick-row {
  display: flex;
  align-items: flex-start;
  border-bottom: 1px solid var(--rule);
  padding: .65rem .85rem;
}

.cf-pick-row:last-child {
  border-bottom: 0;
}

.cf-pick-check {
  flex: 0 0 auto;
  margin-right: .6rem;
  padding-top: .15rem;
}

.cf-pick-body {
  flex: 1 1 auto;
  min-width: 0;
}

.cf-pick-label {
  font-weight: 650;
  font-size: .98rem;
  color: var(--ink);
}

.cf-pick-meta {
  font-size: .8rem;
  color: var(--muted);
  margin-top: .15rem;
}

/* Drag handle for reordering mapped custom fields in edit mode. Spacing comes
   from Bootstrap utilities in the markup; these are the parts no utility covers:
   the grab cursor, and touch-action so a drag on a phone moves the card instead
   of scrolling the page. */
.cf-grip {
  color: #aba3ae;
  cursor: grab;
  touch-action: none;
}

/* In edit mode the grip is pinned to the left of the order cell (the cell is
   .position-relative), out of the flow, so it doesn't push the chip off the
   Display Order column's center. Bootstrap 4 has no offset utilities for this. */
.cf-order-cell .cf-grip {
  position: absolute;
  left: .3rem;
  top: 50%;
  transform: translateY(-50%);
}

/* .cf-cards-wrap sits inside a normally-padded .card-body alongside the title,
   subheadings, and desktop table, but .roster-card already carries its own
   inset (see .roster-card). Left as-is, the two paddings stack and squeeze the
   cards' available width on a phone - other roster-card lists on this site
   avoid this by putting them in a zero-padding card-body instead, so cancel
   the surrounding card-body padding here to get the same net result without
   touching padding the rest of this card still needs. */
.cf-cards-wrap {
  margin-left: -1.25rem;
  margin-right: -1.25rem;
}

/* A level with no fields renders an empty card list for edit mode to fill with
   pending cards - keep it from taking up the mb-4 gap until it has one. */
.cf-cards-wrap:empty {
  display: none;
}

/* Leading rail of a custom-field card, in the slot the family roster-card gives
   its .disclose chevron (same box: padding, line-height, radius, brand ink) so
   the text column lines up the same way. It is the display order - a fact, not
   a control - so it sits on a tint instead of taking .disclose's hover/focus. */
.cf-card-order {
  flex: 0 0 auto;
  min-width: 1.6rem;
  padding: .15rem .35rem;
  line-height: 1;
  border-radius: 3px;
  background: var(--brand-tint-225);
  color: var(--brand-primary);
  font-size: .8rem;
  font-weight: 650;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
