/*
  bento-grid.css — THE PRIMER
  ===========================================================================
  Structural layer ONLY. Zero color. Zero decorative styling of any kind
  (D-TPL-SHIP-02, D-TPL-NOT-01, D-TPL-TOK-03). Typography here is SHAPE only
  (scale, weight, rhythm, family stack) — never color; color is Render-layer.

  This file's content is identical in shape and intent across every BU
  Ecosystem template that ships a visual surface. It is loaded first; pull
  every render file off and the application still works, in greyscale (the
  "is it skin-related?" diagnostic — see bu-framework skill).

  Construction metaphor vocabulary used throughout this system:
    slab, bay, panel, reveal, brick, rail, transom, mullion, soffit, gate.

  Accessibility (WCAG 2.1 AA, non-negotiable):
    - Interactive controls MUST have a minimum 44px touch target (both
      dimensions). This is noted at every interactive class below.
    - Interactive controls MUST use a maximum 5px border-radius. No pill
      buttons. Cards/panels are exempt from the radius cap.
    - Do not rely on color alone to convey state; that is a Render-layer
      concern in any case, since this file carries no color.

  A spun page must read as a structured product page in greyscale — a nav
  rail, a hero region, aligned columns, styled buttons and forms — not as
  unstyled text. See docs/srs BUP-SRS-001-S07 greyscale-proof definition.
  ===========================================================================
*/

:root {
  /* Structural scale only — no color tokens live here. Spacing/sizing scale
     is expressed in rem so it respects user font-size preferences. */
  --structure-gap-xs: 0.25rem;
  --structure-gap-sm: 0.5rem;
  --structure-gap-md: 1rem;
  --structure-gap-lg: 1.5rem;
  --structure-gap-xl: 2rem;
  --structure-gap-2xl: 3rem;

  --structure-radius-max: 5px; /* Law: interactive controls, max 5px. No pill buttons. */
  --structure-radius-panel: 8px; /* Panels/cards are exempt from the interactive cap. */
  --structure-touch-min: 44px; /* Law: 44px minimum touch target on every interactive control. */

  /* z-index stack: three layers only, kept intentionally sparse. */
  --structure-z-base: 1;
  --structure-z-rail: 10;
  --structure-z-overlay: 50;

  /* Typography SHAPE scale — sizes/weights/rhythm only, never color. System
     font stack: no Google Fonts / CDN font loads anywhere (D-TPL laws). */
  --structure-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --structure-font-family-mono: ui-monospace, SFMono-Regular, "Cascadia Mono",
    "Consolas", monospace;

  --structure-text-xs: 0.75rem;
  --structure-text-sm: 0.875rem;
  --structure-text-base: 1rem;
  --structure-text-md: 1.125rem;
  --structure-text-lg: 1.375rem;
  --structure-text-xl: 1.75rem;
  --structure-text-2xl: 2.25rem;
  --structure-text-3xl: 3rem;

  /* Form-label step (13px), ported verbatim from the admin Primer
     (WT-0915-047 student half, REQ-UNIFORM-002: the copies must match). It sits
     deliberately BETWEEN -xs and -sm: a form label is a caption, subordinate to
     the 16px input it sits above, never near a heading's size (D-16 / owner
     2026-08-29: "I don't know why it has header text in field box"). */
  --structure-text-label: 0.8125rem;

  --structure-leading-tight: 1.15;
  --structure-leading-base: 1.55;

  --structure-weight-normal: 400;
  --structure-weight-medium: 500;
  --structure-weight-bold: 700;

  --structure-container-max: 1200px;
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--structure-font-family);
  font-size: var(--structure-text-base);
  line-height: var(--structure-leading-base);
  font-weight: var(--structure-weight-normal);
}

/* -----------------------------------------------------------------------
   Typography rhythm — shape only (size, weight, spacing). Color is
   Render-layer (see render-*.css text-role tokens).
   ----------------------------------------------------------------------- */
h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--structure-gap-sm) 0;
  font-weight: var(--structure-weight-bold);
  line-height: var(--structure-leading-tight);
  overflow-wrap: break-word;
}

h1 { font-size: var(--structure-text-3xl); }
h2 { font-size: var(--structure-text-2xl); }
h3 { font-size: var(--structure-text-xl); }
h4 { font-size: var(--structure-text-lg); }
h5 { font-size: var(--structure-text-md); }
h6 { font-size: var(--structure-text-base); }

p {
  margin: 0 0 var(--structure-gap-md) 0;
}

p:last-child {
  margin-bottom: 0;
}

small, .bu-text-sm {
  font-size: var(--structure-text-sm);
}

ul, ol {
  margin: 0 0 var(--structure-gap-md) 0;
  padding-left: var(--structure-gap-lg);
}

a {
  font: inherit;
}

/* Form controls inherit the typeface (WT-0915-048, student half of the admin
   1ccd65a fix). Browsers give <button>, <input>, <select> and <textarea> their
   own UA face unless told otherwise, so every student button rendered Arial
   beside the skin's face: the Quest Log tabs and the "?" trigger in Arial
   16-17.6px next to Orbitron headings and Rajdhani body, the Skill Tree popover
   Close in Arial 12.8px, the /login button in Arial. This is the element-level
   floor, so a bare <button>, a .bu-control, a .bu-btn and an unclassed <select>
   all follow whatever face the skin sets on body. FAMILY ONLY: size and weight
   stay with each component class. Specificity (0,0,1), so any class rule or
   skin face override still wins (R-0823-01: the Primer owns inheritance, the
   skin owns the face). Zero colour. */
button,
input,
select,
textarea,
optgroup {
  font-family: inherit;
}

/* The button inside <input type="file"> is a pseudo-element of the input, not a
   <button>, so the rule above cannot be relied on to reach it (WT-0915-048). */
input[type="file"]::file-selector-button {
  font-family: inherit;
}

/* -----------------------------------------------------------------------
   .bu-slab — the outermost structural container of a page or region.
   ----------------------------------------------------------------------- */
.bu-slab {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  width: 100%;
  max-width: 100%;
  position: relative;
  z-index: var(--structure-z-base);
  /* Mobile-first hard requirement (R-0709-07 / gamification research §2.0
     "one vertical scroll axis only"): no element on this page may ever
     force sideways page scroll. This is a safety net at the outermost
     container, on top of the per-component width caps below. */
  overflow-x: hidden;
}

/* -----------------------------------------------------------------------
   .bu-main — the page content region inside a layout's .bu-slab, sibling to
   .bu-rail. Plain block-level flow (NOT itself a grid) so that each page can
   supply its own .bu-bay-grid(s) without nesting a grid inside a grid — a
   grid-in-grid nesting silently collapses inner bays to a single 1-column
   width because the outer grid treats the inner root as one item.
   ----------------------------------------------------------------------- */
.bu-main {
  flex: 1;
  display: block;
  width: 100%;
}

/* -----------------------------------------------------------------------
   .bu-bay-1 .. .bu-bay-12 — a 12-column bento grid. Bays are the structural
   column-span units a .bu-panel occupies.
   ----------------------------------------------------------------------- */
.bu-bay-grid {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr's implicit minimum is
     auto (max-content of the child), which lets a single long unbroken
     word blow a column past its intended fractional width. minmax(0, 1fr)
     keeps every column genuinely equal-shareable. */
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--structure-gap-lg);
  padding: var(--structure-gap-lg) var(--structure-gap-xl);
  width: 100%;
  max-width: var(--structure-container-max);
  margin: 0 auto;
  /* WT-0915-021 — paired panels in one row MUST be equal height (owner ruling
     R-0709-07, uniform boxes). This copy still said `start`, so the Locker's
     Promotion Velocity (199px) sat beside a 150px Attendance Streak; the public
     Primer copy already carries `stretch` for exactly this ruling. Every
     .bu-panel is a flex column, so a stretched cell only gives it more room at
     the bottom; content is not distorted. */
  align-items: stretch;
}

.bu-bay-1  { grid-column: span 1; }
.bu-bay-2  { grid-column: span 2; }
.bu-bay-3  { grid-column: span 3; }
.bu-bay-4  { grid-column: span 4; }
.bu-bay-5  { grid-column: span 5; }
.bu-bay-6  { grid-column: span 6; }
.bu-bay-7  { grid-column: span 7; }
.bu-bay-8  { grid-column: span 8; }
.bu-bay-9  { grid-column: span 9; }
.bu-bay-10 { grid-column: span 10; }
.bu-bay-11 { grid-column: span 11; }
.bu-bay-12 { grid-column: span 12; }

/* Breakpoints: bays collapse to full width below tablet width so the grid
   never forces horizontal scroll on small viewports. */
@media (max-width: 768px) {
  .bu-bay-grid {
    padding: var(--structure-gap-md);
  }

  .bu-bay-1, .bu-bay-2, .bu-bay-3, .bu-bay-4, .bu-bay-5, .bu-bay-6,
  .bu-bay-7, .bu-bay-8, .bu-bay-9, .bu-bay-10, .bu-bay-11, .bu-bay-12 {
    grid-column: span 12;
  }
}

/* -----------------------------------------------------------------------
   .bu-panel — a structural card/region. Every .bu-panel MUST have a
   .bu-reveal as its first child (the panel's structural header/intro slot).
   Panels are exempt from the interactive border-radius cap.
   ----------------------------------------------------------------------- */
.bu-panel {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
  padding: var(--structure-gap-lg);
  border-radius: var(--structure-radius-panel);
  position: relative;
  z-index: var(--structure-z-base);
  min-width: 0; /* grid item safety: don't let content force the column wider */
  /* Appearance (background, border color, elevation) is a Render-layer
     concern — see render-*.css. This file assigns no color. */
}

.bu-panel > .bu-reveal:first-child {
  display: block;
  width: 100%;
  margin-bottom: var(--structure-gap-xs);
}

/* NEW-STU-04 — a bay-grid nested inside a panel must not add a second gutter:
   the panel's own padding already is the inset. The Locker's Trophy Case put a
   .bu-bay-grid inside its .bu-panel, so the Gold/Silver/Bronze columns started
   32px right of every other panel's content edge (x202 vs x170 at 1440; x73 vs
   x57 at 375). Same repair the admin pass made in 1ccd65a ("double gutters
   removed where a bay-grid nested inside a panel re-added its padding").
   Structure only. */
.bu-panel .bu-bay-grid {
  padding: 0;
}

/* -----------------------------------------------------------------------
   .bu-brick — the smallest structural building block (a list item, a
   stat tile, a single content atom within a panel).
   ----------------------------------------------------------------------- */
.bu-brick {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-sm) 0;
}

/* WT-0915-060 — a list that is also a brick lost its marker gutter: .bu-brick's
   `padding: sm 0` zeroed the ul's padding-left, so the Locker's Attendance
   History bullets hung outside the panel's content edge (li at x170 = the
   edge). A brick list keeps the Primer's list indent. */
ul.bu-brick,
ol.bu-brick {
  padding-left: var(--structure-gap-lg);
}

/* -----------------------------------------------------------------------
   .bu-metric — a compact stat/number tile (dashboards, value props).
   ----------------------------------------------------------------------- */
.bu-metric {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
}

.bu-metric-value {
  font-size: var(--structure-text-2xl);
  font-weight: var(--structure-weight-bold);
  line-height: var(--structure-leading-tight);
}

.bu-metric-label {
  font-size: var(--structure-text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* -----------------------------------------------------------------------
   .bu-rail — a persistent structural strip (navigation rail, header bar,
   footer bar). Sits above base content, below overlays, in the z-stack.
   Top nav shape: brand/logo left, links right, single horizontal row.
   ----------------------------------------------------------------------- */
.bu-rail {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
  padding: var(--structure-gap-sm) var(--structure-gap-xl);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--structure-z-rail);
  min-height: 70px;
  width: 100%;
}

/* .bu-slab that carries a fixed .bu-rail as its first child needs top
   padding so fixed-rail content never overlaps the page's own content —
   the rail is removed from document flow by position:fixed. Applied via
   .bu-has-fixed-rail on the .bu-slab wrapper (see MainLayout). Harvested
   from the reference layout authority. */
.bu-slab.bu-has-fixed-rail {
  padding-top: 70px;
}

/* #431 (QA 902cb3e): on a phone the guardian rail wraps (Sign out above "MY CHILDREN"), grows well past the 70px reserved above and
   covered the first lines of the page. Same repair the console uses: below the wrap breakpoint the rail returns to normal flow and
   takes its own real height. Scoped to the guardian layout so the student Locker Room chrome is untouched. */
@media (max-width: 768px) {
  .bu-guardian-chrome > .bu-rail {
    position: static;
  }

  .bu-slab.bu-has-fixed-rail.bu-guardian-chrome {
    padding-top: 0;
  }
}

/* #442: on a phone the metric grid is one column with justify-items:center, so each tile shrank to its own text width and the tiles sat
   in a ragged, staggered column. Inside the guardian layout every tile takes the full column width. (The fixed-header half of
   #442 is the #431 rule above, shipped in 31bc486 - the child page uses the same guardian layout.) */
.bu-guardian-chrome .bu-metric-grid {
  justify-items: stretch;
}

.bu-guardian-chrome .bu-metric-grid > .bu-metric {
  align-items: center;
  text-align: center;
}

/* The guardian child card's two links ("View Details" / "Manage Consent") stacked with no gap. */
.bu-guardian-chrome .bu-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--structure-gap-sm);
}

/* -----------------------------------------------------------------------
   .bu-gate — an overlay/modal structural surface. Sits at the top of the
   z-stack, above rails and base content.
   ----------------------------------------------------------------------- */
.bu-gate {
  position: fixed;
  inset: 0;
  z-index: var(--structure-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--structure-gap-md);
}

/* GATE SURFACES — the shared gate pattern, ported verbatim from the admin
   Primer (1ccd65a) so the student /login is the same card as admin sign-in
   (WT-0915-046 student half; REQ-UNIFORM-001: the Primer copies match).
   Structure only, no colour. */

/* WT-0915-046 — the student /login measured 1014px tall in a 900px viewport:
   the page root is a nested .bu-slab (min-height:100vh) inside MainLayout's own
   .bu-slab (also 100vh, plus the 70px rail padding and the 44px footer), and a
   gate's only child is position:fixed, so the nested slab has nothing to size
   to. Opting it out lets the layout's slab own exactly one viewport. */
.bu-fit-slab {
  min-height: 0;
}

/* WCAG 2.1 SC 1.4.10 (reflow): .bu-gate is position:fixed, so a card taller than
   the viewport (an error banner on a small phone, or 400% zoom) was clipped
   with no way to reach it. Scroll inside the overlay instead. */
.bu-gate {
  overflow-y: auto;
}

/* WT-0915-046 — the card was a fixed 265px at x580 (1440) with 215px inputs: it
   is a flex item of .bu-gate, and .bu-bay-4 is `grid-column: span 4`, which
   means nothing in a flex row, so the card shrink-wrapped its inputs. Full
   width inside the gate's gutters, capped at 26rem; margin:auto centres it AND
   keeps it reachable when it overflows. A modal surface, not a page column. */
.bu-gate-card {
  width: 100%;
  max-width: 26rem;
  margin: auto;
  overflow-wrap: break-word;
}

/* -----------------------------------------------------------------------
   .bu-transom / .bu-mullion / .bu-soffit — structural dividers.
   Transom: a horizontal structural divider (above a section).
   Mullion: a vertical structural divider (between columns).
   Soffit: a structural footer/underside region of a panel or slab.
   ----------------------------------------------------------------------- */
.bu-transom {
  width: 100%;
  height: 1px;
  border: none;
  margin: var(--structure-gap-md) 0;
}

.bu-mullion {
  width: 1px;
  align-self: stretch;
  margin: 0 var(--structure-gap-md);
}

.bu-soffit {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  min-height: var(--structure-touch-min);
}

/* -----------------------------------------------------------------------
   Navigation — brand left, link row right, horizontal single line.
   Structural only; render supplies color/hover/active appearance.
   ----------------------------------------------------------------------- */
.bu-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: var(--structure-gap-md);
}

.bu-nav-brand {
  display: inline-flex;
  align-items: center;
  font-size: var(--structure-text-md);
  font-weight: var(--structure-weight-bold);
  text-decoration: none;
  min-height: var(--structure-touch-min);
  letter-spacing: 0.02em;
}

/* .bu-nav-links sits in the CENTER of the rail (brand left, actions right) —
   flex:1 plus centered content lets it claim the middle third without a
   third explicit grid column. Harvested from the reference layout authority
   (fixed header: logo left / nav center / CTA right). */
.bu-nav-links {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  gap: var(--structure-gap-lg);
  list-style: none;
  margin: 0;
  padding: 0;
}

.bu-nav-links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--structure-touch-min);
  text-decoration: none;
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* WT-0915-006 / SRS 6B REQ-STUTABS-004 AC-004.3 — the current section must be
   marked by something other than colour. NavMenu now emits aria-current="page"
   (and .active) on the matching link; both skins' a.active rules only change
   colour, so the Primer adds the non-colour half: an underline, which is
   shape, drawn in currentColor. Zero colour. */
.bu-nav-links a[aria-current="page"] {
  text-decoration-line: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.4em;
}

/* WT-0915-059 — the Locker's Quest Log tablist reuses .bu-nav-links, whose
   header-rail job is to sit CENTRED; inside a panel that put the three tabs at
   x522-902 under an H2 at x170 (1440). A tablist starts on the panel's content
   edge and wraps when it runs out of room. */
.bu-nav-links[role="tablist"] {
  justify-content: flex-start;
  flex-wrap: wrap;
}

.bu-nav-actions {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-sm);
}

@media (max-width: 768px) {
  .bu-nav {
    flex-wrap: wrap;
  }

  .bu-nav-links {
    order: 3;
    flex-basis: 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--structure-gap-md);
  }

  /* WT-0915-058 — once the links wrap to their own line, an EMPTY actions slot
     still opened a first flex line, and the row gap under it made the student
     header 77px tall at 375 against the slab's fixed 70px top padding, so 7px
     of the first panel's top border sat under the header. An actions slot with
     nothing in it takes no line. */
  .bu-nav-actions:not(:has(*)) {
    display: none;
  }
}

/* -----------------------------------------------------------------------
   Hero region — eyebrow label, headline, lede, action row. Structural
   shape harvested from the reference layout authority's hero slab.
   ----------------------------------------------------------------------- */
.bu-hero {
  text-align: center;
  padding: var(--structure-gap-2xl) var(--structure-gap-xl);
}

.bu-hero-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.3em;
  margin-bottom: var(--structure-gap-lg);
}

.bu-hero-headline {
  font-size: clamp(2rem, 5vw, 3.75rem);
  font-weight: var(--structure-weight-bold);
  line-height: 1.05;
  letter-spacing: 0.01em;
  margin: 0 auto var(--structure-gap-lg);
  max-width: 18ch;
}

.bu-hero-lede {
  max-width: 60ch;
  margin: 0 auto var(--structure-gap-xl);
  font-size: var(--structure-text-md);
  line-height: var(--structure-leading-base);
}

.bu-hero-actions {
  display: flex;
  gap: var(--structure-gap-md);
  justify-content: center;
  flex-wrap: wrap;
}

/* -----------------------------------------------------------------------
   Interactive controls — structural rules only. Colors are Render-layer.
   Law: max 5px border-radius (no pill buttons); 44px minimum touch target.
   ----------------------------------------------------------------------- */
.bu-control {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--structure-touch-min);
  min-width: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-base);
  font-weight: var(--structure-weight-medium);
  text-decoration: none;
  line-height: 1;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
}

.bu-control:disabled,
.bu-control[disabled] {
  cursor: not-allowed;
}

/* NEW-STU-01 — a focus ring must keep 3:1 against what is next to it (WCAG
   1.4.11). Fight Night's ring is a 2px #0047AB outline at 2px offset, about
   2.4:1 on its near-black frosted panel (1.9:1 under .bu-panel:focus-within),
   and its value is fidelity-locked. This fills the 2px offset gap with the
   control's own ink, so every skin's ring gains a high-contrast inner band
   (white on Fight Night, the dark text ink on greyscale) without any skin
   value changing. currentColor is a keyword, not a colour (Primer zero-colour
   law, same reasoning as D-0823-A below); a skin that draws its own focus
   shadow still wins on specificity. */
.bu-control:focus-visible {
  box-shadow: 0 0 0 2px currentColor;
}

/* WT-0915-059 / REQ-STUTABS-004 AC-004.3 — the selected tab looked identical to
   the other two (same fill, border and weight). Non-colour selected state:
   heavier weight plus an underline, both shape. */
.bu-control[role="tab"][aria-selected="true"] {
  font-weight: var(--structure-weight-bold);
  text-decoration-line: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.3em;
}

/* -----------------------------------------------------------------------
   .bu-btn / .bu-btn-secondary — inline-block button shape (BENTO-CORE
   inventory: primary solid, secondary bordered-transparent). Built on top
   of .bu-control's touch-target/radius law. Structural only.
   ----------------------------------------------------------------------- */
.bu-btn,
.bu-btn-secondary {
  /* WT-0915-047 (student half of 1ccd65a) — the line-height below used to be
     (touch-min - padding - 2px), which assumes a 1px border; a skin that draws
     2px rails got a 46px button beside 44px .bu-control siblings. Centring the
     label in a flex box makes the height skin-independent: every variant lands
     on the 44px floor whatever rails a skin draws. Structure only. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto; /* inside a flex CTA row: hug content width, never stretch */
  width: fit-content;
  min-height: var(--structure-touch-min);
  /* SRSV-06-17: the 44px law is BOTH dimensions; .bu-control declared both,
     the button family declared height only. */
  min-width: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-lg);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-base);
  font-weight: var(--structure-weight-medium);
  text-align: center;
  text-decoration: none;
  line-height: 1.25;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
}

.bu-btn:disabled,
.bu-btn-secondary:disabled {
  cursor: not-allowed;
}

/* -----------------------------------------------------------------------
   Form controls — full-width fields, stacked vertical rhythm (BENTO-CORE
   inventory: full-width fields, 0.5rem stacked gap).
   ----------------------------------------------------------------------- */
.bu-field {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  margin-bottom: var(--structure-gap-md);
}

/* A form label is a caption above its input: 13px, medium (D-16). This copy
   still said 14px (--structure-text-sm), so student /login and Mat labels read
   14px/500 while admin sign-in read 13px/500 (WT-0915-047 student half;
   admin Primer :836-840). */
.bu-field label {
  font-size: var(--structure-text-label);
  font-weight: var(--structure-weight-medium);
  line-height: 1.3;
}

/* Form help / hint line — a muted sentence placed as a DIRECT child of
   .bu-field, under its input. Ported from the admin Primer (:875-879): one
   fixed step BELOW the 13px/500 label, 12px regular, so it can never render
   larger or bolder than the label above it. Specificity (0,2,1) beats
   .bu-text-sm / .bu-text-muted whatever their order; :not(label) keeps a
   muted label at label scale. Colour stays with .bu-text-muted in the render
   layer. */
.bu-field > .bu-text-muted:not(label) {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-normal);
  line-height: var(--structure-leading-base);
}

.bu-field input[type="text"],
.bu-field input[type="email"],
.bu-field input[type="tel"],
.bu-field input[type="password"],
.bu-field input[type="number"],
.bu-field select,
.bu-field textarea,
input.bu-input,
select.bu-input,
textarea.bu-input {
  width: 100%;
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-family: inherit;
  font-size: var(--structure-text-base);
}

.bu-field-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--structure-gap-sm);
  min-height: var(--structure-touch-min);
}

.bu-field-row input[type="checkbox"],
.bu-field-row input[type="radio"] {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.bu-field-error {
  font-size: var(--structure-text-sm);
  margin: 0;
}

/* =========================================================================
   HARVESTED COMPONENT SHAPES — ported from the owner's approved reference
   skeleton (BeltUp Online legacy layout authority, bu-skin-skeleton.css),
   structure only, every class/token renamed to current law on the way in.
   Colors for every class below live exclusively in render-*.css.
   ========================================================================= */

/* --- Muted/small text role (color assigned in render) -------------------- */
.bu-text-muted {
  font-size: 0.85rem;
}

/* --- Section / panel heading shapes -------------------------------------- */
.bu-section-heading {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: var(--structure-gap-lg);
}

.bu-panel-title {
  font-size: 0.9rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: 1.25rem;
}

.bu-panel-sublabel {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: var(--structure-gap-sm);
}

.bu-panel-headline {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: var(--structure-gap-xl);
}

.bu-panel-footnote {
  margin-top: var(--structure-gap-md);
  font-size: 0.85rem;
  letter-spacing: 1px;
  text-transform: uppercase;
}

.bu-command-header {
  text-align: center;
  margin-bottom: var(--structure-gap-sm);
}

.bu-command-label {
  font-size: 0.8rem;
  font-weight: var(--structure-weight-bold);
  letter-spacing: 8px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-command-title {
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin: 0;
}

/* --- Circular gauge (SVG stroke-dash percentage indicator) ---------------- */
.bu-gauge {
  position: relative;
  width: 100px;
  height: 100px;
}

.bu-gauge svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* WT-0915-060 — an EMPTY gauge (no ring drawn inside it) is a 100x100 blank:
   the Locker's Trophy Case stacked three of them above "No ... trophies yet".
   A gauge with its ring markup still renders; an empty one takes no space. */
.bu-gauge:empty {
  display: none;
}

.bu-gauge-value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: var(--structure-weight-bold);
  font-size: var(--structure-text-base);
}

.bu-gauge-row {
  display: flex;
  gap: var(--structure-gap-lg);
  justify-content: center;
  flex-wrap: wrap;
}

.bu-gauge-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--structure-gap-sm);
}

.bu-gauge-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* --- 75/25 progression bar (relative track, absolute fill) ---------------- */
.bu-skill-row {
  margin-bottom: var(--structure-gap-md);
}

.bu-skill-row:last-child {
  margin-bottom: 0;
}

.bu-status-labels {
  display: flex;
  justify-content: space-between;
  width: 100%;
  font-size: 0.85rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  margin-bottom: 0.35rem;
}

.bu-progress-track {
  position: relative;
  height: 12px;
  border-radius: 4px;
  width: 100%;
  overflow: hidden;
}

.bu-track-hard {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
}

/* --- Media panel (thumbnail left, content right) --------------------------- */
.bu-media-panel {
  flex-direction: row;
  gap: var(--structure-gap-xl);
  align-items: center;
  flex-wrap: wrap;
}

.bu-media-frame {
  width: 180px;
  height: 110px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.bu-play-circle {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.bu-play-icon {
  width: 22px;
  height: 22px;
}

.bu-media-content {
  flex: 1;
  min-width: 240px;
}

.bu-media-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: var(--structure-gap-sm);
}

.bu-media-title {
  font-size: 1.25rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: var(--structure-gap-sm);
}

.bu-media-lede {
  margin-bottom: var(--structure-gap-md);
  line-height: 1.5;
}

.bu-badge-tag {
  display: inline-block;
  padding: 0.4rem 0.8rem;
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 1px;
  text-transform: uppercase;
}

/* --- Metric grid (three-up stat cells with center dividers) ---------------- */
.bu-metric-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  align-items: center;
  justify-items: center;
  text-align: center;
}

.bu-metric-cell {
  padding: var(--structure-gap-lg) var(--structure-gap-md);
  text-align: center;
}

.bu-metric-cell .bu-metric-value {
  font-size: 3.5rem;
  line-height: 1;
}

@media (max-width: 720px) {
  .bu-metric-grid {
    grid-template-columns: 1fr;
  }
}

/* WT-0915-060 — a two-metric grid split its row into three 362px columns
   (Locker Attendance History: 2 metrics, an empty third column). Two metrics
   share the row evenly; the phone rule above still stacks them. */
@media (min-width: 721px) {
  .bu-metric-grid:has(> :nth-child(2):last-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --- Pillar panels (label / title / body) ---------------------------------- */
.bu-pillar-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-pillar-title {
  font-size: clamp(1.25rem, 2vw, 1.75rem);
  font-weight: var(--structure-weight-bold);
  line-height: 1.15;
  margin-bottom: var(--structure-gap-md);
}

.bu-pillar-body {
  line-height: 1.6;
}

/* --- Corner panels (label / title / body / bottom-anchored actions) --------- */
.bu-corner-panel {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.bu-corner-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
}

.bu-corner-title {
  font-size: 1.75rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin: 0;
}

.bu-corner-body {
  line-height: 1.6;
  margin: 0;
}

.bu-corner-actions {
  margin-top: auto;
  padding-top: var(--structure-gap-md);
}

/* --- Slat (thin horizontal work-item strip: level | info | action) --------- */
.bu-slat {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--structure-gap-md);
  padding: 0.75rem var(--structure-gap-md);
}

.bu-slat-level {
  font-weight: var(--structure-weight-bold);
  font-size: 0.95rem;
  letter-spacing: 1px;
  text-align: center;
}

.bu-slat-info h4 {
  text-transform: uppercase;
  font-size: 0.9rem;
  letter-spacing: 1px;
  margin: 0 0 0.35rem 0;
}

.bu-slat-info p {
  margin: 0 0 0.4rem 0;
  font-size: var(--structure-text-xs);
  letter-spacing: 0.5px;
}

.bu-slat .bu-progress-track {
  height: 4px;
}

.bu-slat-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

/* --- Status dot (pulse indicator) ------------------------------------------- */
.bu-status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}

@keyframes bu-alert-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.35); opacity: 0.6; }
}

.bu-status-dot.is-alert {
  animation: bu-alert-pulse 1.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .bu-status-dot.is-alert {
    animation: none;
  }
}

/* --- Stealth link (uppercase, quiet) ----------------------------------------- */
.bu-link-stealth {
  text-decoration: none;
  font-weight: var(--structure-weight-bold);
  font-size: 0.8rem;
  text-transform: uppercase;
}

/* --- Global footer (brand column + link columns + copyright bar) ------------- */
.bu-footer {
  margin-top: var(--structure-gap-2xl);
  padding: var(--structure-gap-2xl) var(--structure-gap-xl) 0;
  position: relative;
  z-index: var(--structure-z-rail);
}

.bu-footer-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--structure-gap-lg);
  max-width: var(--structure-container-max);
  margin: 0 auto;
}

.bu-footer-brand {
  grid-column: span 6;
}

.bu-footer-links {
  grid-column: span 2;
}

.bu-footer-tagline {
  margin-top: 0.75rem;
  max-width: 32ch;
  line-height: 1.5;
}

.bu-footer-heading {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 2px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-footer-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
}

.bu-footer-list a {
  font-size: 0.85rem;
}

.bu-footer-copyright {
  max-width: var(--structure-container-max);
  margin: var(--structure-gap-xl) auto 0;
  padding: 1.25rem 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--structure-text-xs);
}

.bu-footer-lab-link {
  opacity: 0.5;
  text-decoration: none;
  font-size: var(--structure-text-xs);
}

@media (max-width: 900px) {
  .bu-footer-brand {
    grid-column: span 12;
  }

  .bu-footer-links {
    grid-column: span 4;
  }
}

@media (max-width: 600px) {
  .bu-footer-links {
    grid-column: span 12;
  }
}

/* =========================================================================
   LOCKER ROOM GAME-PROGRESS SCREEN — structural component shapes
   (BUP-RULING-LOCKERROOM-GAMESCREEN_2026-07-06 + addendum; coordinator
   refinement 2026-07-06: componentize every control). Structure only, zero
   color, per this file's own law — appearance for every class below lives
   exclusively in a future render-*.css pass. Each shape below backs exactly
   one Blazor component in BeltUp.Components.Student/Components/Locker/, so a
   skin retargets the SAME markup via these class/data-* hooks without any
   structural fork (D-S07-FW-01).
   ========================================================================= */

/* --- LockerCallout: the one reusable idle-tile + popover primitive used by
   every section (skill node, quest, rank chip context, etc.). Idle face is
   a button (>=44px touch target via .bu-control-shaped sizing); popover is
   absolutely positioned off the tile and hidden unless data-open="true". -- */
.bu-callout {
  display: inline-flex;
  flex-direction: column;
  position: relative;
  min-width: 0;
}

.bu-callout-face {
  display: inline-flex;
  align-items: center;
  gap: var(--structure-gap-xs);
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  /* WT-0915-014 — the face is a <button>, and a skin that does not style it
     (Fight Night today) left the UA button paint in place: a light-grey
     rgb(240,240,240) tile with black text on the dark arena, and the white
     .bu-badge-tag chips on that pale fill at about 1.13:1. Same fix the Dojo's
     .bu-bucket-header carries (D-0823-A, below): reset the UA paint to the
     CSS keywords so the face takes the panel's own ink and border colour falls
     to currentColor. Keywords, not colour literals, so the zero-colour law
     holds; any skin that does style the face (greyscale) still wins on
     specificity. Skin-specific node paint stays the skin's job. */
  background: none;
  color: inherit;
  border-color: currentColor;
  font: inherit;
  cursor: pointer;
  width: 100%;
  text-align: left;
}

.bu-callout-glyph {
  font-weight: var(--structure-weight-bold);
  line-height: 1;
  flex-shrink: 0;
}

.bu-callout-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-medium);
}

/* Popover: hidden by default (data-open="false"), shown on toggle. Anchored
   below the face tile. A later polish pass may switch this to hover-intent
   on pointer devices per the ruling's "hover pop-over" language; tap-toggle
   is the honest baseline for a structure-only pass since it also works on
   touch with no extra JS.
   WIDTH LAW (2026-07-09 overflow fix): no fixed min-width — a fixed 220px
   min-width previously let a popover overflow a narrow parent tile (e.g. a
   150px skill-node medallion) past the 375px viewport edge, forcing sideways
   page scroll (R-0709-07 box law violation). max-width is capped against the
   viewport itself so the popover NEVER exceeds available screen width on any
   device, matching the "no horizontal overflow" mobile-first hard requirement. */
.bu-callout-popover {
  display: none;
  position: relative;
  margin-top: var(--structure-gap-xs);
  padding: var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
  border-width: 1px;
  border-style: solid;
  z-index: var(--structure-z-overlay);
  width: max-content;
  min-width: 0;
  max-width: min(320px, calc(100vw - 2 * var(--structure-gap-xl)));
}

.bu-callout-popover[data-open="true"] {
  display: block;
}

/* NEW-STU-02 — the popover Close is now rendered as a .bu-control
   (LockerCallout.razor), so it takes the 44x44 floor and the skin's control
   face at every width; it was a .bu-link-stealth at 60x21 on desktop. */
.bu-callout-dismiss {
  margin-top: var(--structure-gap-sm);
}

/* --- HUD header: identity strip (2026-07-09 rebuild). Kitchen-sink header
   pattern per the ruling — student identity block left, current-rank chip
   right, single row on wide viewports, stacks on phone. Game-HUD density:
   replaces the previous stack of separate h1/h2/p elements with one compact
   strip so identity never eats a full screen of vertical space. ---------- */
.bu-locker-hud {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
}

.bu-locker-hud-id {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  min-width: 0;
}

.bu-locker-hud-name {
  margin: 0;
  font-size: var(--structure-text-xl);
}

.bu-locker-hud-school {
  margin: 0;
  font-size: var(--structure-text-sm);
}

.bu-locker-hud-styles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--structure-gap-xs);
}

@media (max-width: 600px) {
  .bu-locker-hud {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* --- HudHeader (bu-hud-header, R1 2026-07-17 D-HEADER-HUD restore): the Locker
   Room shelf identity strip — avatar + current-rank chip left, honest readiness
   status row right. Structure only, zero color (skin paint is a later pass); see
   HudHeader.razor for the full UI-0034/UI-0035 citation trail and the readiness
   honest-neutral-state note. Same row/wrap/stack shape as .bu-locker-hud /
   .bu-dojo-header above — deliberately its own named class (not reused) because the
   owner ruling names bu-hud-header specifically as the restored component. --------- */
.bu-hud-header {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
}

.bu-hud-header-id {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--structure-gap-sm);
  min-width: 0;
}

.bu-hud-header-avatar {
  width: 48px;
  height: 48px;
  min-width: 48px;
  border-radius: 50%;
  flex-shrink: 0;
}

.bu-hud-header-status {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--structure-gap-xs);
  flex-shrink: 0;
}

/* The school own belt colour (data, set inline as --bu-belt from DagNodes.BeltColorHex): the avatar ring and a small belt bar before the
   rank name. Only present when the school set a colour, so a locker without one looks exactly as before. The name stays as text, so the
   colour is never the only signal; the bar keeps a hairline frame so a white or black belt stays visible on any skin. */
.bu-hud-header-avatar[data-belt="set"] {
  border: 3px solid var(--bu-belt);
}

.bu-belt-bar {
  display: inline-block;
  width: 1.75rem;
  height: 0.625rem;
  border-radius: 2px;
  background-color: var(--bu-belt);
  outline: 1px solid currentColor;
  flex-shrink: 0;
}

.bu-hud-header-attendance {
  margin-left: var(--structure-gap-sm);
}

@media (max-width: 600px) {
  .bu-hud-header {
    flex-direction: column;
    align-items: flex-start;
  }

  .bu-hud-header-status {
    flex-wrap: wrap;
  }
}

/* --- RankChip: current-rank (lg, header) + side-ladder rung (sm, rail)
   shapes (2026-07-09 rebuild). REPLACES the old bu-brick-column composition
   (which stacked every field vertically and stretched each rung into a
   giant card) with a single compact HORIZONTAL row per rung — a real
   "progress rail" of small steps, not a repeated tall-card list. ---------- */
.bu-rank-chip {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-xs) var(--structure-gap-sm);
  border-radius: var(--structure-radius-max);
  min-height: 36px;
}

.bu-rank-chip[data-size="lg"] {
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
}

.bu-rank-chip[data-size="lg"] .bu-badge-tag {
  font-size: var(--structure-text-md);
  padding: 0.6rem 1rem;
}

.bu-rank-chip-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-medium);
}

/* Rail wrapper: tight vertical stack of compact rungs, no per-rung card
   padding/border — the rail reads as one continuous progress ladder. ----- */
.bu-rank-ladder {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* --- SkillTree -> SkillPath: a single vertical progression PATH, not a
   stack of full-width dashed boxes (2026-07-09 rebuild fixing the reported
   defect: nodes rendering as "full-width dashed-border rows"). Root cause
   was `.bu-skill-tier-node { flex: 1 1 220px }` — flex-grow:1 stretched a
   lone node to fill the entire tier row width. Nodes are now a FIXED
   compact slot width (never grow), tiers no longer carry their own boxed
   border, and a single connector rail runs down the left edge with one tick
   per tier — the Duolingo-style "single path, tiered, always in view"
   precedent from BUP-UI-GAMIFICATION-RESEARCH-001 §1.1/§2.1.3. Multiple
   same-tier nodes still wrap left-to-right so composite convergence stays
   visually legible. One vertical scroll axis only. ------------------------ */
.bu-skill-tree {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-lg);
  position: relative;
  padding-left: 28px;
}

.bu-skill-tree::before {
  content: "";
  position: absolute;
  left: 9px;
  top: 6px;
  bottom: 6px;
  width: 2px;
}

.bu-skill-tier {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--structure-gap-sm);
  position: relative;
}

.bu-skill-tier::before {
  content: "";
  position: absolute;
  left: -24px;
  top: 14px;
  width: 14px;
  height: 2px;
}

.bu-skill-tier-node {
  /* Fixed compact slot — never grows past this width (the prior bug), only
     shrinks on very narrow viewports via the min() cap. */
  flex: 0 1 auto;
  width: min(100%, 150px);
  min-width: 0;
}

/* Skill-node medallion face: compact square card (icon-forward, label
   below), NOT a full-width dashed row — the Primer's max-5px interactive
   border-radius law rules out a true circular "medallion", so this is a
   compact squared tile instead, matching the research doc's "compact
   cards/medallion-style nodes" recommendation within that constraint. ----- */
.bu-callout[data-variant="skill-node"] {
  width: 100%;
}

.bu-callout[data-variant="skill-node"] .bu-callout-face {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--structure-gap-xs);
  min-height: 92px;
  padding: var(--structure-gap-sm);
}

.bu-callout[data-variant="skill-node"] .bu-callout-glyph {
  font-size: var(--structure-text-lg);
}

.bu-callout[data-variant="skill-node"] .bu-callout-label {
  white-space: normal;
  overflow: visible;
  text-align: center;
  font-size: var(--structure-text-xs);
  line-height: 1.2;
}

/* Ready-to-learn ("unlocked") nodes get a heavier border for emphasis — the
   "current node visually enlarged/emphasized" precedent, expressed here via
   border-width (shape) rather than color (render-layer concern). --------- */
.bu-callout[data-variant="skill-node"][data-state="unlocked"] .bu-callout-face {
  border-width: 2px;
}

.bu-callout[data-variant="skill-node"][data-state="achieved"] .bu-callout-face {
  font-weight: var(--structure-weight-bold);
}

/* Fog-of-war: a locked node's callout face is dimmed via opacity (shape-only
   signal — no color assigned here; a skin may layer a blur filter too). --- */
.bu-callout[data-variant="skill-node"][data-state="locked"] {
  opacity: 0.55;
}

.bu-callout[data-variant="skill-node"][data-state="locked"] .bu-callout-face {
  cursor: default;
}

/* --- Attendance stat tiles (2026-07-09 rebuild): REPLACES the old
   `<span class="bu-badge-tag">` pair stuffed inside a `.bu-brick` (a
   flex-column that stretches its children to 100% width by default —
   the exact root cause of the reported "numbers inside giant full-width
   INPUT bars" defect). Two proper `.bu-metric` stat tiles, side by side. -- */
.bu-locker-stat-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* --- QuestLog / QuestCard: two stacked groups (instructor-assigned above
   gated), each a vertical list of quest callouts; the 4-stage chip row
   inside a quest's popover uses data-active to fog future stages. --------- */
.bu-quest-log {
  display: flex;
  flex-direction: column;
}

.bu-quest-group {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
}

.bu-quest-group .bu-callout {
  width: 100%;
}

[data-stage][data-active="false"] {
  opacity: 0.45;
}

/* =========================================================================
   DIGITAL DOJO LANDING (PG-042) — bucket/tap-expand chassis
   (R-0709-17 interaction law + R-0709-18 structure ruling,
   docs/rulings/BUP-RULING-UI-POLISH-CAPTURE_2026-07-09.md lines 82-113).
   Structure only, zero color — a bucket's collapsed/expanded state is
   expressed via the data-expanded attribute (shape/behavior), never color.
   ========================================================================= */

/* --- Header identity strip: school name + welcome line + avatar slot,
   single row on wide viewports, stacks on phone (same shape law as the
   Ghost Locker HUD header above). ----------------------------------------- */
.bu-dojo-header {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
}

.bu-dojo-header-id {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  min-width: 0;
}

.bu-dojo-header-school {
  margin: 0;
  font-size: var(--structure-text-lg);
  font-weight: var(--structure-weight-bold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bu-dojo-header-welcome {
  margin: 0;
  font-size: var(--structure-text-sm);
}

.bu-dojo-avatar-slot {
  width: 48px;
  height: 48px;
  min-width: 48px;
  border-radius: 50%;
  flex-shrink: 0;
}

@media (max-width: 600px) {
  .bu-dojo-header {
    flex-wrap: nowrap;
  }

  /* WT-0915-056 — the school name is the page's identity (now its H1); at 375
     it was cut to an ellipsis (clientWidth 229 < scrollWidth 252). On a phone
     it wraps instead. */
  .bu-dojo-header-school {
    white-space: normal;
  }
}

/* WT-0915-056 — heading scale on the Dojo ran backwards: the Announcements H2
   rendered 36px above a 22px school name and beside 18px tile titles, and the
   "Locker Room" H3 inside the Your Space tile was 28px under its 18px tile
   title. The strip title takes the tile-title step; a heading inside a tile
   sits one step below the tile's own title. Shape only. */
.bu-dojo-marquee .bu-reveal > h2 {
  font-size: var(--structure-text-md);
}

.bu-bucket-body .bu-corner-title {
  font-size: var(--structure-text-base);
}

/* --- Bucket: the tap-to-expand content container used by every Digital
   Dojo section below the header (THIS WEEK, YOUR SPACE, SCHOOL NEWS).
   Idle state = header row only (title + one-line summary + optional count
   badge + chevron) — a collapsed bucket costs only this one row of height
   in the page's own end-to-end scroll (R-0709-17.2). Tap the header ->
   the body slot renders in place, capped at ~60vh with internal scroll
   beyond that (never full phone height); tap again -> the body is removed
   again. Expand/collapse is driven ENTIRELY by the header button's click in
   the page's @code (R-0709-17.3 "no gesture traps" — no swipe/drag handler
   exists anywhere in this chassis). --------------------------------------- */
.bu-bucket {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* WT-0915-003 — the Dojo tab bar's Schedule / News items now scroll to their
   tile in place; without a scroll margin the tile's header landed under the
   fixed 70px rail (see .bu-has-fixed-rail). */
.bu-bucket[id] {
  scroll-margin-top: calc(70px + var(--structure-gap-md));
}

.bu-bucket-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-sm);
  width: 100%;
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-sm) 0;
  border: none;
  background: none;
  font: inherit;
  /* D-0823-A: a <button> element does NOT inherit ancestor text color by
     browser default (UA stylesheets set an explicit buttontext/CanvasText
     color), unlike a plain <span>/<div>. Every dark skin's .bu-panel sets a
     light --theme-text-main via a render file, but this button never picked
     it up, so it fell through to the UA default (near-black) — unreadable
     on a near-black panel background (Fight Night bottom tiles: Skill Tree /
     Rank Ladder / Attendance). `color: inherit` is a keyword, not a color
     literal, so it keeps this Primer file's zero-hardcoded-color law intact
     (D-TPL-SHIP-02) while letting every descendant span (.bu-bucket-header-
     text/.bu-bucket-title/.bu-bucket-summary/.bu-bucket-meta/.bu-bucket-
     count/.bu-bucket-chevron — none of which set their own color) inherit
     the panel's real text color under EVERY skin, current and future. */
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.bu-bucket-header-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.bu-bucket-title {
  font-size: var(--structure-text-md);
  font-weight: var(--structure-weight-bold);
  margin: 0;
}

.bu-bucket-summary {
  font-size: var(--structure-text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bu-bucket-meta {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-sm);
  flex-shrink: 0;
}

.bu-bucket-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 0.4rem;
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
}

.bu-bucket-chevron {
  display: inline-block;
  font-weight: var(--structure-weight-bold);
  transition: transform 150ms ease;
}

.bu-bucket[data-expanded="true"] .bu-bucket-chevron {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .bu-bucket-chevron {
    transition: none;
  }
}

.bu-bucket-body {
  display: none;
}

.bu-bucket[data-expanded="true"] .bu-bucket-body {
  display: block;
  max-height: 60vh;
  overflow-y: auto;
  padding-top: var(--structure-gap-sm);
}

/* --- Bottom tab bar: fixed strip, Home/Schedule/News/Profile. Non-Home
   tabs render as honest disabled stubs (no route exists for them yet)
   rather than dead links. -------------------------------------------------- */
.bu-tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: var(--structure-z-rail);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  min-height: var(--structure-touch-min);
}

.bu-tab-bar-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-xs);
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-medium);
  text-decoration: none;
  border: none;
  background: none;
  font-family: inherit;
}

.bu-tab-bar-item[aria-disabled="true"] {
  cursor: not-allowed;
}

/* A slab hosting a fixed bottom tab bar needs bottom padding so the last
   piece of real content never sits underneath the fixed strip — same
   pattern as .bu-has-fixed-rail's top-padding companion above. */
.bu-slab.bu-has-fixed-tabbar {
  padding-bottom: 64px;
}

/* WT-0915-009 — the rule above pads the PAGE's section, but MainLayout's footer
   comes after that section, so at max scroll the footer sat exactly under the
   fixed bar (both [0,856,1425,44] at 1440; the layout slab's padding-bottom
   was 0). The reservation for a fixed bottom bar belongs on the layout slab
   that hosts it, the same way .bu-has-fixed-rail reserves the header. */
.bu-slab.bu-has-fixed-rail:has(.bu-tab-bar) {
  padding-bottom: var(--structure-touch-min);
}

/* =========================================================================
   GHOST LOCKER MOCKUP REBUILD (2026-07-10) — top-strip identity/momentum/
   quest/achievements elements + bucket-ized skill tree / rank ladder /
   attendance detail. Owner defect report: "It doesn't look anything like
   the screenshot or the mockup... It should be all bucketed out." Structure
   only, zero color, per this file's own law (D-TPL-SHIP-02) — appearance
   lives in render-*.css. Buckets reuse the .bu-bucket/.bu-bucket-header/
   .bu-bucket-body shapes defined above (Digital Dojo section) verbatim —
   same tap-to-expand chassis, same ~60vh cap, no new bucket primitive.
   ========================================================================= */

/* --- Identity strip avatar w/ status-ring slot (US-098): the ring itself
   is a border — greyscale in primer, a skin recolors it per belt. --------- */
.bu-locker-avatar-slot {
  width: 56px;
  height: 56px;
  min-width: 56px;
  border-radius: 50%;
  border-width: 3px;
  border-style: solid;
  flex-shrink: 0;
}

.bu-locker-hud-avatar-row {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-md);
}

/* --- Momentum bar (US-099): two-stop gradient track bound to current/next
   rank tokens (greyscale primer = two grey stops, assigned in render-*.css),
   belt-name endpoints, percent label, and the attendance-count stat that
   REPLACES the old standalone attendance panel on the top strip per the
   owner's defect report ("we can say yes you showed up four times"). ------ */
.bu-momentum {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
}

.bu-momentum-endpoints {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--structure-gap-sm);
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-bold);
}

.bu-momentum-endpoint-current,
.bu-momentum-endpoint-next {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bu-momentum-track {
  position: relative;
  height: 16px;
  border-radius: 8px;
  width: 100%;
  overflow: hidden;
}

.bu-momentum-fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  /* Width set inline per-instance from the computed percent (structural
     data-binding, not a color concern) — the gradient COLORS themselves are
     assigned in render-*.css against this element. */
}

.bu-momentum-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--structure-gap-sm);
  font-size: var(--structure-text-sm);
}

.bu-momentum-percent {
  font-weight: var(--structure-weight-bold);
}

.bu-momentum-stat {
  display: inline-flex;
  align-items: center;
  gap: var(--structure-gap-xs);
}

/* --- Achievements row (US-101): earned medallions + one locked "?" next
   slot. Horizontal row, wraps on narrow viewports — never forces sideways
   scroll (the .bu-slab overflow-x:hidden safety net still applies). ------- */
.bu-achievements-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--structure-gap-sm);
}

.bu-achievement-medallion {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--structure-gap-xs);
  width: 84px;
  min-height: 84px;
  padding: var(--structure-gap-sm);
  border-radius: var(--structure-radius-panel);
  border-width: 1px;
  border-style: solid;
  text-align: center;
}

.bu-achievement-medallion-glyph {
  font-size: var(--structure-text-lg);
  font-weight: var(--structure-weight-bold);
}

.bu-achievement-medallion-label {
  font-size: var(--structure-text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.bu-achievement-medallion[data-locked="true"] {
  opacity: 0.55;
}

/* --- Next Class card (US-102): honest empty state, QR affordance rendered
   disabled (no student-visible session data source exists — see
   GhostLocker.razor code comment). ----------------------------------------- */
.bu-nextclass-qr {
  min-height: var(--structure-touch-min);
}

/* --- Phone check-in code (R-1024-04 S3): a full-screen sheet with the QR big and centred. The QR itself is always black on white with a quiet zone:
   scanners need that contrast whatever the skin is, so it uses the plain keywords here rather than a theme token (same as a printed card). --- */
.bu-checkin-qr {
  position: fixed;
  inset: 0;
  z-index: var(--structure-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow-y: auto;
  padding: var(--structure-gap-md);
  background-color: var(--theme-bg-canvas, Canvas);
  color: var(--theme-text-main, CanvasText);
}

/* .bu-panel is a stacking context (position: relative + z-index), which would trap the sheet inside its own panel under the panels that follow it.
   Whichever panels hold an open sheet are lifted to the overlay layer. */
.bu-panel:has(.bu-checkin-qr),
.bu-slab:has(.bu-checkin-qr),
.bu-dojo-grid:has(.bu-checkin-qr) {
  z-index: var(--structure-z-overlay);
}

.bu-checkin-qr-body {
  width: 100%;
  max-width: 28rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--structure-gap-md);
  text-align: center;
}

.bu-checkin-qr-title,
.bu-checkin-qr-name {
  margin: 0;
}

.bu-checkin-qr-frame {
  /* >= 280px on a 390px phone, never wider than the screen minus its padding */
  width: min(100%, 22rem);
  min-width: min(100%, 17.5rem);
  aspect-ratio: 1 / 1;
  box-sizing: border-box;
  padding: var(--structure-gap-sm);
  background-color: white;
  color: black;
  border-radius: var(--structure-radius-panel);
}

.bu-checkin-qr-frame svg {
  display: block;
  width: 100%;
  height: 100%;
}

.bu-checkin-qr-hint {
  margin: 0;
  font-size: var(--structure-text-sm);
}

.bu-checkin-qr-close {
  min-height: var(--structure-touch-min);
  min-width: 8rem;
}

/* --- "Help {child}" banner (R-1025-01 item 4a): who the parent is helping, on every Locker Room page, with one big way back. Sticks under the fixed rail
   so it stays on screen while the page scrolls. Structure only; colour comes from the skin's tokens (the border is a non-colour signal on its own). --- */
.bu-help-banner {
  /* #519: the banner used to cover the first panel. Its slab (.bu-has-fixed-rail) is an overflow container that ALREADY reserves the 70px rail as padding, and a sticky
     offset is measured from inside that padding, so "top: 70px" put the banner at 140px (70px too low, over the first panel) before the page had even scrolled.
     The offset is 0 for the same reason; on a phone the banner is a normal block at the top (a 170px bar stuck to a 812px screen hides what the parent came to do). */
  position: static;
  z-index: calc(var(--structure-z-rail) - 1);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-sm);
  box-sizing: border-box;
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-block: 2px solid currentColor;
  background-color: var(--theme-bg-canvas, Canvas);
  color: var(--theme-text-main, CanvasText);
}

@media (min-width: 769px) {
  .bu-help-banner {
    position: sticky;
    top: 0;
  }
}

.bu-help-banner-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 14rem;
}

.bu-help-banner-name {
  font-size: var(--structure-text-lg);
}

.bu-help-banner-exit {
  min-height: var(--structure-touch-min);
  min-width: 6rem;
}

/* "Set up {child}'s device": each linked device is a row with its Remove button, the label growing and the button never squeezed under the touch minimum. */
.bu-device-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-sm);
}

.bu-device-row > .bu-btn-secondary {
  min-height: var(--structure-touch-min);
}

/* =========================================================================
   RESPONSIVE COMPOSITION LAW (R-0710-01, docs/rulings/
   BUP-RULING-UI-POLISH-CAPTURE_2026-07-09.md lines 140-147, owner
   2026-07-10): "each surface has DISTINCT compositions per form factor...
   the phone stack blown up tall on a desktop browser is a defect." This
   section defines the grid wrappers for the two Student-host surfaces in
   scope this pass (Ghost Locker + Digital Dojo). Structure only, zero
   color, per this file's own law.

   BREAKPOINT LAW (documented here, applies to both wrappers below):
     - < 640px  (phone):  wrapper is plain block flow — every child stacks
       full-width in source order, IDENTICAL to the pre-reflow one-column
       composition (no grid at all at this width, so nothing about the
       proven 375px composition changes).
     - 640-1023px (tablet): wrapper becomes a 2-column CSS grid via
       grid-template-areas; a full-width top strip stays full-width (spans
       both columns), paired content sits side by side.
     - >= 1024px (desktop): wrapper becomes a 3-column CSS grid; the top
       strip stays full-width (spans all three columns), and the row(s)
       below it reflow into a 3-up arrangement.
   Every grid uses minmax(0, 1fr) columns (same overflow-safety pattern as
   .bu-bay-grid above) so no column can be forced wider than its share by a
   long word/number, and the outer .bu-slab's overflow-x:hidden remains the
   safety net under all three widths. A bucket's data-expanded state (see
   .bu-bucket-body above) still only grows the bucket's OWN height, capped
   at 60vh with internal scroll — expansion never spans outside the grid
   item's own column/row, so "expanded bucket spans its column, not the
   page" holds at every breakpoint for free.
   ========================================================================= */

/* --- Ghost Locker (/locker/{token}, GhostLocker.razor) ------------------- */
/* FLOATING-TILE SPACING (owner ruling 2026-07-12): bento cells must never
   read as stacked/touching — the scene background must show BETWEEN cells so
   panels read as floating tiles. Grid widths get a wider gap; the phone
   (display:block) composition gets explicit inter-child spacing, which block
   flow otherwise provides none of. Structural spacing only, zero color. */
.bu-locker-grid {
  display: block;
}

@media (max-width: 639px) {
  .bu-locker-grid > * + *,
  .bu-dojo-grid > * + * {
    margin-top: var(--structure-gap-lg);
  }
}

@media (min-width: 640px) {
  .bu-locker-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--structure-gap-xl);
    width: 100%;
    max-width: var(--structure-container-max);
    margin: 0 auto;
    align-items: start;
    grid-template-areas:
      "identity     identity"
      "marquee      marquee"
      "momentum     momentum"
      "quest        achievements"
      "bucket-skill bucket-rank"
      "bucket-att   bucket-att"
      "nextclass    nextclass";
  }
}

@media (min-width: 1024px) {
  .bu-locker-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas:
      "identity     identity     identity"
      "marquee      marquee      marquee"
      "momentum     momentum     momentum"
      "quest        achievements nextclass"
      "bucket-skill bucket-rank  bucket-att";
  }
}

/* grid-area assignment is width-independent — harmless (unused) when the
   wrapper is display:block on phone, active once the wrapper becomes a
   grid at 640px+. */
.bu-locker-identity          { grid-area: identity; }
.bu-locker-marquee           { grid-area: marquee; }
.bu-locker-momentum          { grid-area: momentum; }
.bu-locker-quest             { grid-area: quest; }
.bu-locker-achievements      { grid-area: achievements; }
.bu-locker-bucket-skilltree  { grid-area: bucket-skill; }
.bu-locker-bucket-rankladder { grid-area: bucket-rank; }
.bu-locker-bucket-attendance { grid-area: bucket-att; }
.bu-locker-nextclass         { grid-area: nextclass; }

/* --- Digital Dojo (/app/home, DigitalDojo.razor) -------------------------- */
.bu-dojo-grid {
  display: block;
  /* WT-0915-021 / WT-0915-058 — one content edge on both student tabs. The
     Locker's panels sit in .bu-bay-grid, which insets them gap-lg/gap-xl inside
     the 1200px container; this grid had no inset, so the Dojo column was x113
     w1200 against the Locker's x145 w1136 at 1440, ran edge to edge at 1024
     (x0, rounded corners cut by the window), and its first panel touched the
     70px header with a 0 gap. Same insets as .bu-bay-grid, at every width. */
  padding: var(--structure-gap-lg) var(--structure-gap-xl);
}

@media (max-width: 768px) {
  .bu-dojo-grid {
    padding: var(--structure-gap-md);
  }
}

@media (min-width: 640px) {
  .bu-dojo-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--structure-gap-xl);
    width: 100%;
    max-width: var(--structure-container-max);
    margin: 0 auto;
    align-items: stretch; /* B37: sibling tiles in a row share one height */
    grid-template-areas:
      "header     header"
      "marquee    marquee"
      "thisweek   yourspace"
      "schoolnews schoolnews";
  }
}

@media (min-width: 1024px) {
  .bu-dojo-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas:
      "header     header     header"
      "marquee    marquee    marquee"
      "thisweek   yourspace  schoolnews";
  }
}

.bu-dojo-identity              { grid-area: header; }
.bu-dojo-marquee                { grid-area: marquee; }
.bu-dojo-bucket-thisweek        { grid-area: thisweek; }
.bu-dojo-bucket-yourspace       { grid-area: yourspace; }
.bu-dojo-bucket-schoolnews      { grid-area: schoolnews; }

/* -----------------------------------------------------------------------
   Greyscale fallback canvas — structural surface reference only. These
   two values referenced here by name only (canvas, surface) are the grounded baseline
   documented for the greyscale-skeleton skin. They are declared as tokens
   in render-greyscale-skeleton.css, NOT as literals here — this comment
   exists only to document intent; this file contains zero color literals.
   ----------------------------------------------------------------------- */

/* -----------------------------------------------------------------------
   .bu-table-wrapper — horizontal scroll container for a wide table, so a
   table that cannot fit the viewport scrolls itself instead of forcing the
   whole page to scroll sideways.
   ----------------------------------------------------------------------- */
.bu-table-wrapper {
  overflow-x: auto;
}

/* -----------------------------------------------------------------------
   Phone ergonomics (2026-08-22, from the layout walkthrough).

   A page may be DESIGNED for desktop, but it must be USABLE on a phone —
   these rules only fix reachability and fit, never appearance, so they are
   structure and belong in the Primer.
   ----------------------------------------------------------------------- */
@media (max-width: 768px) {
  /* .bu-link-stealth is a deliberately quiet 0.8rem footer link. Quiet is fine;
     un-tappable is not — it accounted for 107 of the 120 undersized touch
     targets found across the site, because it appears in the footer of every
     page. Given a real target height it stays visually identical and becomes
     hittable. */
  .bu-link-stealth {
    display: inline-flex;
    align-items: center;
    min-height: var(--structure-touch-min);
  }

  /* Tables cannot be made to fit a 390px screen without lying about the data,
     so let them scroll horizontally INSIDE their own box rather than pushing
     the whole page sideways. Applied to the element itself because most tables
     in this codebase are not wrapped in .bu-table-wrapper; `max-width: 100%`
     keeps the scroll contained to the table. */
  .bu-table-wrapper,
  table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Tighter cell padding so fewer columns need scrolling in the first place. */
  table th,
  table td {
    padding: 0.5rem;
    white-space: nowrap;
  }

  /* Native checkboxes and radios render ~13px, well under any reachable target.
     Enlarging the control itself keeps the hit area on the control (not just its
     label), which is what a thumb actually aims at. */
  input[type="checkbox"],
  input[type="radio"] {
    min-width: 24px;
    min-height: 24px;
  }

  /* A label wrapping a control is the real target — give it the full height so
     tapping the words works, not just the 24px box. */
  .bu-check-label,
  label:has(> input[type="checkbox"]),
  label:has(> input[type="radio"]) {
    display: inline-flex;
    align-items: center;
    gap: var(--structure-gap-sm);
    min-height: var(--structure-touch-min);
  }

  /* Text inputs, selects and file pickers: the Primer's 44px law applies on a
     phone above all, where these were rendering short. */
  .bu-input,
  input[type="text"],
  input[type="email"],
  input[type="tel"],
  input[type="number"],
  input[type="date"],
  input[type="password"],
  input[type="url"],
  input[type="file"],
  select,
  textarea {
    min-height: var(--structure-touch-min);
  }

  /* A <summary> styled as a control is a control. */
  summary {
    min-height: var(--structure-touch-min);
    display: flex;
    align-items: center;
  }

  /* Content must not sit flush against the screen edge. Measured on the Digital
     Dojo at 390px: headings and body copy were rendering at left: 1px with a 1px
     right gap, which reads as broken rather than dense. */
  .bu-main,
  .bu-slab > main {
    padding-left: var(--structure-gap-md);
    padding-right: var(--structure-gap-md);
  }

  /* WT-0915-021 — the rule above IS the phone gutter, so a grid inside main must
     not add its own side inset on top of it. The Locker's .bu-bay-grid kept its
     16px, giving its panels a 32px gutter (x32 w311 at 375) beside the Dojo's
     16px (x16 w343). One phone gutter on both tabs (REQ-UNIFORM-003 AC-003.1). */
  .bu-main .bu-bay-grid,
  .bu-main .bu-dojo-grid,
  .bu-slab > main .bu-bay-grid {
    padding-left: 0;
    padding-right: 0;
  }
}

/* WT-0915-008 — the developer skin chip (skin-preview.js, off unless
   SkinPreview is true). Its geometry used to be inline, and an inline
   `bottom:12px` outranks any stylesheet, so the phone "lift it clear of the
   tab bar" rule that lived here never applied: the chip sat on the Dojo's Home
   item at every width and on the Locker's footer copyright. skin-preview.js no
   longer inlines `bottom` or its 14px pill radius; the 44px floor, the 5px
   control radius and the offset live here. It clears the footer soffit at max
   scroll, and the Dojo's tab bar plus the footer above it where a bar is
   mounted. */
#bu-skin-preview-chip {
  min-height: var(--structure-touch-min);
  border-radius: var(--structure-radius-max);
  bottom: calc(var(--structure-touch-min) + var(--structure-gap-sm));
}

body:has(.bu-tab-bar) #bu-skin-preview-chip {
  bottom: calc(var(--structure-touch-min) * 2 + var(--structure-gap-sm));
}

/* -----------------------------------------------------------------------
   .bu-panel.bu-brick — padding collision repair (2026-08-23).
   Markup legitimately combines the two (`class="bu-panel bu-brick"`), but
   .bu-panel sets padding: var(--structure-gap-lg) and .bu-brick sets
   `var(--structure-gap-sm) 0`. Same specificity, .bu-brick declared later, so
   it won and zeroed the HORIZONTAL padding — content then sat flush against
   the panel edge. Under skins that draw a corner ornament (Fight Night's 35px
   cobalt wedge on .bu-panel::before) that clipped the first letter of every
   panel heading. A panel is a padded surface first; being a brick as well must
   not remove that.
   ----------------------------------------------------------------------- */
.bu-panel.bu-brick {
  padding: var(--structure-gap-lg);
}
