/* ==========================================================================
   Total Machinist by immerse2learn.com
   Foundation stylesheet

   Built to WCAG 2.1 Level AA. Notes marked [WCAG x.x.x] mark code that exists
   specifically to satisfy a success criterion — do not remove without
   understanding what it is holding up.

   Typography is deliberately limited to fonts present on every Windows, Mac,
   iOS and Android device. Nothing downloads, nothing falls back unexpectedly,
   and the wordmark matches the brand spec exactly (Arial Black / Trebuchet MS).
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Brand ------------------------------------------------------------------ */
  --navy:        #002060;  /* from the logo spec */
  --navy-hover:  #001845;
  --navy-tint:   #E8EDF6;
  /* Text drawn ON the navy. Must flip with the theme: dark-mode navy is a pale
     blue, and white on pale blue fails contrast. Anything sitting on a navy
     background uses this, never a hard-coded white. */
  --on-navy:     #FFFFFF;

  /* Neutrals — biased slightly toward the navy rather than pure grey ------- */
  --ink:         #14181F;
  --ink-soft:    #3D4655;
  --muted:       #5A6478;
  --line:        #D8DEE8;
  --line-strong: #B9C3D4;
  --ground:      #F4F6F9;
  --paper:       #FFFFFF;

  /* Accent — certification brass, used sparingly -------------------------- */
  --brass:       #8A5E0F;
  --brass-tint:  #FBF3E0;

  /* Track colours ---------------------------------------------------------
     One per track in the pathway. These are functional rather than decorative:
     a student or instructor jumping between track pages gets an immediate
     "which one am I in", and the colour carries across cards, page headers and
     breadcrumbs.

     [WCAG 1.4.1] Colour never carries the meaning alone — every use is paired
     with the visible "Track N" label, so nothing is lost if the hues are
     indistinguishable to the viewer. That matters here: red-green colour
     blindness affects roughly one man in twelve, and this trade is heavily male.

     Hues are spread widely and separated by lightness as well, so they stay
     distinguishable under the common forms of colour blindness. Every value
     clears 4.5:1 on the paper ground. */
  --track-1: #002060;   /* Core Skills      — the brand navy, the foundation */
  --track-2: #8A5E0F;   /* Manual Machining — bronze, the hand-tool track    */
  --track-3: #0B6068;   /* Machine Setup    — teal                           */
  --track-4: #6B2D5C;   /* G-Code           — plum                           */
  --track-5: #A32235;   /* CAM              — crimson                        */
  --track-x: #3D4655;   /* anything outside the numbered pathway             */

  /* Semantic --------------------------------------------------------------- */
  --ok:          #1C6446;
  --ok-tint:     #E7F2EC;
  --warn:        #9A4416;
  --warn-tint:   #FBEDE5;

  /* Type ------------------------------------------------------------------- */
  --font-display: "Arial Black", "Arial Bold", Gadget, Arial, sans-serif;
  --font-body:    Arial, Helvetica, "Helvetica Neue", sans-serif;
  --font-alt:     "Trebuchet MS", "Lucida Grande", Tahoma, sans-serif;

  /* Scale — 18px base. Larger than the usual 16 because a good share of our
     readers are in a workshop, on a phone, or over fifty. Costs nothing. ---- */
  --text-xs:   0.778rem;   /* 14px */
  --text-sm:   0.889rem;   /* 16px */
  --text-base: 1rem;       /* 18px */
  --text-lg:   1.167rem;   /* 21px */
  --text-xl:   1.444rem;   /* 26px */
  --text-2xl:  1.833rem;   /* 33px */
  --text-3xl:  2.333rem;   /* 42px */
  --text-4xl:  3rem;       /* 54px */

  /* Space ------------------------------------------------------------------ */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;

  /* Layout ----------------------------------------------------------------- */
  --measure:   68ch;   /* readable line length */
  --container: 1180px;
  --radius:    3px;

  --focus-width: 3px;
}

/* Dark theme. Not a naive inversion — the navy has to lift considerably to
   stay legible on a dark ground, and the brass desaturates.                  */
@media (prefers-color-scheme: dark) {
  :root {
    --navy:        #7EA6E0;
    --navy-hover:  #A8C4EC;
    --navy-tint:   #16213A;
    --on-navy:     #0B0F16;

    --ink:         #E7EBF3;
    --ink-soft:    #BFC7D6;
    --muted:       #97A1B5;
    --line:        #2A3346;
    --line-strong: #3D4860;
    --ground:      #0B0F16;
    --paper:       #131926;

    --brass:       #DCA94A;
    --brass-tint:  #241D0E;
    --track-1: #7EA6E0;
    --track-2: #DCA94A;
    --track-3: #4FBFC9;
    --track-4: #D28FC0;
    --track-5: #F0808F;
    --track-x: #97A1B5;

    --ok:          #6FC79B;
    --ok-tint:     #0F2119;
    --warn:        #E09468;
    --warn-tint:   #26140C;
  }
}

/* The viewer's explicit toggle must beat the OS preference in both directions. */
:root[data-theme="dark"] {
  --navy:        #7EA6E0;
  --navy-hover:  #A8C4EC;
  --navy-tint:   #16213A;
  --on-navy:     #0B0F16;
  --ink:         #E7EBF3;
  --ink-soft:    #BFC7D6;
  --muted:       #97A1B5;
  --line:        #2A3346;
  --line-strong: #3D4860;
  --ground:      #0B0F16;
  --paper:       #131926;
  --brass:       #DCA94A;
  --brass-tint:  #241D0E;
  --track-1: #7EA6E0;
  --track-2: #DCA94A;
  --track-3: #4FBFC9;
  --track-4: #D28FC0;
  --track-5: #F0808F;
  --track-x: #97A1B5;
  --ok:          #6FC79B;
  --ok-tint:     #0F2119;
  --warn:        #E09468;
  --warn-tint:   #26140C;
}

:root[data-theme="light"] {
  --navy:        #002060;
  --navy-hover:  #001845;
  --navy-tint:   #E8EDF6;
  --on-navy:     #FFFFFF;
  --ink:         #14181F;
  --ink-soft:    #3D4655;
  --muted:       #5A6478;
  --line:        #D8DEE8;
  --line-strong: #B9C3D4;
  --ground:      #F4F6F9;
  --paper:       #FFFFFF;
  --brass:       #8A5E0F;
  --brass-tint:  #FBF3E0;
  --track-1: #002060;
  --track-2: #8A5E0F;
  --track-3: #0B6068;
  --track-4: #6B2D5C;
  --track-5: #A32235;
  --track-x: #3D4655;
  --ok:          #1C6446;
  --ok-tint:     #E7F2EC;
  --warn:        #9A4416;
  --warn-tint:   #FBEDE5;
}


/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */

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

html {
  font-size: 112.5%;          /* 18px base */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.6;
}

/* [WCAG 2.3.3 / 2.2.2] Honour a stated preference for less movement. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

img,
picture,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

/* --------------------------------------------------------------------------
   3. Focus
   [WCAG 2.4.7] Every interactive element shows where the keyboard is. Themes
   routinely delete this for looks; it is the single most damaging thing you
   can do to a keyboard user.
   [WCAG 1.4.11] The ring needs 3:1 against its background, so it is drawn in
   the navy with a paper-coloured inner edge to survive on any ground.
   -------------------------------------------------------------------------- */

:focus-visible {
  outline: var(--focus-width) solid var(--navy);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Remove the ring for pointer users only, never for keyboard users. */
:focus:not(:focus-visible) { outline: none; }


/* --------------------------------------------------------------------------
   4. Typography
   -------------------------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--font-body);
  font-weight: bold;
  line-height: 1.2;
  color: var(--ink);
  text-wrap: balance;
}

h1 { font-size: var(--text-3xl); letter-spacing: -0.02em; }
h2 { font-size: var(--text-2xl); letter-spacing: -0.01em; }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); }

p { margin: 0; max-width: var(--measure); }

a {
  color: var(--navy);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
a:hover { color: var(--navy-hover); }

strong, b { font-weight: bold; }

/* Small uppercase label. Letter-spacing matters here — tight uppercase is
   markedly harder to read. */
.eyebrow {
  font-family: var(--font-alt);
  font-size: var(--text-xs);
  font-weight: bold;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.lede {
  font-size: var(--text-lg);
  color: var(--ink-soft);
  max-width: var(--measure);
}

/* Visually hidden but available to screen readers. Note it stays focusable —
   using display:none here would break the skip link. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* --------------------------------------------------------------------------
   5. Layout
   Sibling spacing comes from flex/grid gap rather than per-element margins,
   so nothing collapses or doubles unexpectedly.
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

/* Reserve clearance for the fixed contact tab.
   Measured across 360–1400px, content never actually sits under the tab — but
   between roughly 620 and 1150px the gap closes to zero. A focus ring extends
   5px past its control (3px ring + 2px offset), so at zero gap a focused field
   would have its indicator hidden behind the tab. This keeps real space there.
   Below 34rem the tab moves to the bottom of the screen, so no reserve needed. */
@media (min-width: 34.001rem) and (max-width: 72rem) {
  .container { padding-right: calc(var(--space-5) + 2.75rem); }
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.stack--tight { gap: var(--space-2); }
.stack--loose { gap: var(--space-6); }

/* Vertical rhythm is a doubling scale: 18px inside a group, 36px between
   groups, 72px between sections. Originally 4rem here, which put a 144px chasm
   between sections against 36px between paragraphs — a 3x jump that read as
   gaps rather than structure. 2rem gives 1:2:4 and holds across every page. */
.section { padding-block: var(--space-6); }
/* A genuinely tighter step, for sections that belong hard against each other. */
.section--tight { padding-block: var(--space-5); }
.section--paper { background: var(--paper); }
.section--ground { background: var(--ground); }

/* A full navy band. Used once or twice per page at most — its whole job is to
   break up a long run of white and give a call to action some weight. Overuse
   and it stops meaning anything. */
.section--navy {
  background: var(--navy);
  color: var(--on-navy);
}
.section--navy h1,
.section--navy h2,
.section--navy h3 { color: var(--on-navy); }
.section--navy .lede,
.section--navy p { color: var(--on-navy); opacity: 0.92; }
.section--navy .eyebrow { color: var(--on-navy); opacity: 0.75; }

/* Buttons and links have to invert inside the band or they vanish. */
.section--navy .btn--primary {
  background: var(--on-navy);
  border-color: var(--on-navy);
  color: var(--navy);
}
.section--navy .btn--secondary {
  border-color: var(--on-navy);
  color: var(--on-navy);
}
.section--navy .btn--secondary:hover {
  background: var(--on-navy);
  color: var(--navy);
}
.section--navy a { color: var(--on-navy); }

/* [WCAG 2.4.7] The focus ring is navy by default, which would disappear against
   a navy ground. Inside the band it flips to the on-navy colour. */
.section--navy :focus-visible { outline-color: var(--on-navy); }

/* The tick markers are navy too — they need the same treatment. */
.section--navy .tick-list li::before { background: var(--on-navy); }

/* [WCAG 1.4.10] Anything that could exceed the viewport scrolls inside its own
   container so the page body never scrolls sideways at 400% zoom. */
.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}


/* --------------------------------------------------------------------------
   6. Skip link
   [WCAG 2.4.1] Lets a keyboard user jump the navigation. Hidden until focused.
   -------------------------------------------------------------------------- */

.skip-link {
  position: absolute;
  top: 0;
  left: var(--space-4);
  z-index: 200;
  transform: translateY(-120%);
  padding: var(--space-3) var(--space-5);
  background: var(--navy);
  color: var(--on-navy);
  font-weight: bold;
  text-decoration: none;
  border-radius: 0 0 var(--radius) var(--radius);
  transition: transform 120ms ease-out;
}
.skip-link:focus { transform: translateY(0); }


/* --------------------------------------------------------------------------
   7. Brand lockup
   Total Machinist primary, immerse2learn.com secondary beneath — the 36/18pt
   relationship from the brand spec, expressed as a ratio so it scales.
   -------------------------------------------------------------------------- */

.lockup {
  display: inline-flex;
  flex-direction: column;
  gap: 0.1em;
  text-decoration: none;
  line-height: 1;
}

.lockup__primary {
  font-family: var(--font-display);
  font-size: 1em;
  letter-spacing: -0.025em;
  color: var(--ink);
  white-space: nowrap;
}

.lockup__secondary {
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: 0.5em;             /* 18pt against 36pt */
  letter-spacing: 0.005em;
  color: var(--navy);
  padding-left: 0.12em;
  white-space: nowrap;
}

/* Sizes. Set on the block so both lines scale together. */
.lockup--sm { font-size: 1.5rem; }
.lockup--md { font-size: 2.1rem; }
.lockup--lg { font-size: clamp(2.2rem, 7vw, 3.6rem); }

.lockup:hover .lockup__primary { color: var(--navy); }

/* Brand block: the lockup with "Train and Certify" set beside it, divided by a
   rule. Keeping the tagline out of the lockup itself matters — the lockup is
   already two lines, and a third would crowd it at small sizes. The divider
   also lets the tagline drop away on narrow screens without the mark changing. */
.brand {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  text-decoration: none;
}

.brand__tagline {
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--navy);
  padding-left: var(--space-4);
  border-left: 1px solid var(--line-strong);
  white-space: nowrap;
}

/* Below this width the tagline would either wrap or squeeze the mark, so it
   goes. The name and attribution carry the brand on their own. */
@media (max-width: 47rem) {
  .brand__tagline { display: none; }
}


/* --------------------------------------------------------------------------
   8. Buttons
   Minimum 44px tall — comfortably above the 24px of [WCAG 2.5.8] and matching
   the more generous mobile guidance, which matters for gloved hands.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  border: 2px solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: bold;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

/* Deliberately NOT transitioning colour on themed components.
   [WCAG 1.4.3] Chromium fails to re-resolve a transitioned property when the
   custom property behind it changes, so a button transitioning `color: var(--navy)`
   keeps the OLD colour after a light/dark switch. Measured: secondary buttons
   stayed at #002060 on the #131926 dark panel — a contrast ratio of 1.15:1
   against the 4.5:1 requirement. Suppressing transitions restored #7EA6E0.
   A 120ms hover flourish is not worth an unreadable button, so hover feedback
   is instant. If a transition is ever added back here, it must not cover
   colour, background-color or border-color. */

.btn--primary {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--on-navy);
}
.btn--primary:hover {
  background: var(--navy-hover);
  border-color: var(--navy-hover);
  color: var(--on-navy);
}

.btn--secondary {
  background: transparent;
  border-color: var(--navy);
  color: var(--navy);
}
.btn--secondary:hover {
  background: var(--navy-tint);
  color: var(--navy);
}

.btn--quiet {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink);
  font-weight: normal;
}
.btn--quiet:hover { border-color: var(--navy); color: var(--navy); }

.btn--lg { min-height: 52px; padding: var(--space-4) var(--space-6); font-size: var(--text-lg); }
.btn--block { display: flex; width: 100%; }



/* --------------------------------------------------------------------------
   9. Site header and navigation
   -------------------------------------------------------------------------- */

.site-header {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  position: relative;
  z-index: 60;
}

/* Two rows. The brand and the login link sit on the first; the menu gets the
   second to itself. Giving navigation its own full-width row means the labels
   can say what they mean rather than being abbreviated to fit beside the mark. */
.site-header__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-4) var(--space-3);
}

.site-header__bar {
  border-top: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

/* Login. Students and instructors arrive looking for this before anything else,
   so it stays in the top-right corner on every page and at every width. It is
   the one link that must never move. */
/* The pair of header links. They wrap rather than shrink on a narrow screen —
   squeezing two controls below the 44px target to keep them on one line would
   trade [WCAG 2.5.5] for tidiness, which is the wrong way round. */
.site-header__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
}

/* Find your class. Same box and same type size as the login beside it — Chris's
   instruction, 7 August 2026 — so the two read as equals. It carries the fill
   and the login carries the outline, which is the only thing separating them.

   No colour transition, deliberately. [WCAG 1.4.3] Chromium fails to re-resolve
   a transitioned property when the custom property behind it changes, which is
   what left secondary buttons at 1.15:1 after a theme switch. Same rule as .btn
   and .video-band__toggle. */
.header-cta {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  border: 2px solid var(--navy);
  border-radius: var(--radius);
  background: var(--navy);
  color: var(--on-navy);
  font-weight: bold;
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}
.header-cta:hover { background: transparent; color: var(--navy); }

.login-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  border: 2px solid var(--navy);
  border-radius: var(--radius);
  color: var(--navy);
  font-weight: bold;
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}
.login-link:hover {
  background: var(--navy);
  color: var(--on-navy);
}

/* The key icon is decoration; the word "Log in" is the accessible name. */
.login-link svg {
  width: 1em;
  height: 1em;
  flex: none;
  fill: currentColor;
}

@media (max-width: 26rem) {
  .login-link { padding: var(--space-2) var(--space-3); }
  .login-link .login-link__label { font-size: var(--text-xs); }
}

.nav-toggle { display: inline-flex; margin-block: var(--space-2); }
@media (min-width: 62rem) { .nav-toggle { display: none; } }

.site-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.site-nav__link {
  display: block;
  /* The extra bottom padding sets the current-page rule a few pixels clear of
     the text rather than tight under it. */
  padding: var(--space-3) var(--space-3) calc(var(--space-3) + 5px);
  color: var(--ink);
  font-weight: bold;
  text-decoration: none;
  border-radius: var(--radius);
  border-bottom: 2px solid transparent;
}
.site-nav__link:hover { color: var(--navy); background: var(--navy-tint); }

/* [WCAG 1.4.1] Current page is marked by a rule as well as colour, never by
   colour alone — every link in this row is already bold, so without the rule
   the only difference would be hue.

   [WCAG 1.4.11] The rule is a graphical object carrying that meaning, so it
   needs 3:1 against the background. --muted gives 5.95:1 in the light theme
   and 6.76:1 in the dark one. A lighter grey such as --line-strong measures
   1.9:1 and would fail — do not swap it for one of those. */
.site-nav__link[aria-current="page"] {
  color: var(--navy);
  border-bottom-color: var(--muted);
}

/* Small screens: the menu is a disclosure. Collapsed state uses [hidden], so
   keyboard users cannot tab into invisible links. */
.site-nav[hidden] { display: none; }

@media (max-width: 61.999rem) {
  .site-nav {
    position: absolute;
    inset-inline: 0;
    top: 100%;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: var(--space-4) var(--space-5) var(--space-6);
    box-shadow: 0 12px 24px rgba(0, 32, 96, 0.08);
    z-index: 50;
  }
}

@media (min-width: 62rem) {
  .site-nav { display: block !important; }   /* never hidden on desktop */
  .site-nav__list { flex-direction: row; align-items: center; gap: var(--space-1); }
  .site-nav__link {
    /* Keeps the extra bottom padding from the base rule, which is what sets
       the current-page rule clear of the text. Shorthand here would reset it. */
    padding: var(--space-3) var(--space-3) calc(var(--space-3) + 5px);
    /* A wrapped nav label reads as a layout bug. With the menu on its own row
       there is room for the full labels, but they still must not wrap. */
    white-space: nowrap;
  }
  /* No nudge needed for the current-page underline: every link already carries
     a 2px transparent bottom border, so all the boxes are the same height and
     the row stays perfectly aligned. A negative margin here shifted the current
     item half a pixel below its siblings. */
}


/* --------------------------------------------------------------------------
   10. Hero
   -------------------------------------------------------------------------- */

.hero {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  padding-block: var(--space-8) var(--space-8);
}

/* A thin band of the five track colours below the header — the pathway, stated
   as colour before it is stated in words.

   Deliberately a real element rather than a ::before on the hero. As a pseudo
   element it made axe-core abandon contrast checking for every piece of text in
   that region ("background could not be determined due to a pseudo element"),
   which left the busiest part of the page unverifiable by the standard tool.
   The contrast was fine; being able to PROVE it is worth more. Decorative, so
   it carries aria-hidden and adds nothing to the reading order. */
.track-band {
  height: 4px;
  background: linear-gradient(
    to right,
    var(--track-1) 0% 20%,
    var(--track-2) 20% 40%,
    var(--track-3) 40% 60%,
    var(--track-4) 60% 80%,
    var(--track-5) 80% 100%
  );
}

/* Centred, deliberately — Chris's call on 8 August 2026 after comparing both.
   The right column is the taller of the two (measured at 1282px: left 486px,
   right 545px), so `start` is the structurally correct choice and was tried.
   But the panel's border is what the eye reads as the top of that column, not
   the quiet 16px story link above it. Centring puts the headline 35px above the
   panel edge instead of 65px, which lines the two columns up optically.
   Do not "correct" this back to start. */
.hero__inner {
  display: grid;
  gap: var(--space-7);
  align-items: center;
}
@media (min-width: 62rem) {
  .hero__inner { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: var(--space-9); }
}

.hero__title {
  font-size: var(--text-4xl);
  letter-spacing: -0.025em;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* The quiet link under the buttons. Muted ink measures 5.95:1 on paper and
   6.42:1 on the dark theme's paper, so it clears 4.5:1 with room in both — it
   is understated, not faint. The underline is kept because [WCAG 1.4.1] means
   colour cannot be the only thing marking a link, and at this size and weight
   there is nothing else doing that job. */
.hero__aside a {
  font-size: var(--text-sm);
  color: var(--muted);
}


/* --------------------------------------------------------------------------
   11. Cards and panels
   -------------------------------------------------------------------------- */

.panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
}

.card-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
}

/* The whole card is clickable via the heading link, but the link itself stays
   the accessible name — no redundant "read more" links for a screen reader to
   wade through. */
.card__title { font-size: var(--text-lg); }
.card__title a { text-decoration: none; color: var(--ink); }
.card__title a:hover { color: var(--navy); text-decoration: underline; }
.card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.card { position: relative; }

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

/* The price on classes.html. It was an inline style attribute — the only one on
   the site — which meant the one number a buyer looks hardest at was the one
   piece of type not governed by the design system. A class instead, so it moves
   with the tokens. [FTC junk-fees rule / WCAG 3.3.4] The full price has to be
   visible before any commitment, so this is deliberately the largest thing in
   its block. Found by audit-resilience.mjs, 8 August 2026. */
.price { font-size: var(--text-2xl); font-weight: bold; }

/* Track cards carry their position in the pathway. The number is meaningful
   here — the sequence is real and students follow it. */
.card--track { padding-top: var(--space-6); }
.card__step {
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--track, var(--navy));
}

/* A colour rule across the top of each track card. Set --track on the element
   and everything else follows — one property to change per track, and the
   colour cannot drift between the rule, the label and the page header. */
.card--track {
  border-top: 4px solid var(--track, var(--navy));
  padding-top: calc(var(--space-6) - 4px);
}
.card--track .card__title a:hover { color: var(--track, var(--navy)); }

/* Set the track by position so the markup stays clean. */
.track-1 { --track: var(--track-1); }
.track-2 { --track: var(--track-2); }
.track-3 { --track: var(--track-3); }
.track-4 { --track: var(--track-4); }
.track-5 { --track: var(--track-5); }
.track-x { --track: var(--track-x); }

/* Track page header — a colour band tying the page to its card. */
.track-header {
  border-left: 5px solid var(--track, var(--navy));
  padding-left: var(--space-5);
}
.track-header .card__step { color: var(--track, var(--navy)); }

/* A small numbered chip, used where a track is referenced inline. */
.track-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--track, var(--navy));
}
.track-chip::before {
  content: "";
  width: 0.7rem;
  height: 0.7rem;
  background: var(--track, var(--navy));
  border-radius: 2px;
  transform: rotate(45deg);
}


/* --------------------------------------------------------------------------
   12. Story dialog
   Native <dialog>: focus handling, Escape and the backdrop come for free, and
   the browser gets it right more reliably than hand-written JS.
   -------------------------------------------------------------------------- */

.story-dialog {
  width: min(46rem, calc(100vw - 2rem));
  max-height: min(80vh, 46rem);
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  /* The dialog clips; the body inside it scrolls. Without this the content
     spilled past max-height and the wheel did nothing, because nothing was
     actually a scroll container. */
  overflow: hidden;
}

/* Scoped to [open] deliberately. A bare `display: flex` on the dialog would
   override the UA's `display: none` for the closed state and leave the story
   permanently visible at the foot of every page. */
.story-dialog[open] {
  display: flex;
  flex-direction: column;
}

.story-dialog::backdrop {
  background: rgba(11, 15, 22, 0.6);
}

.story-dialog__body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-y: auto;
  /* Load-bearing. A flex child will not shrink below its content height without
     this, so `overflow-y: auto` never engaged and the panel could not be
     scrolled by wheel, keyboard or trackpad. */
  min-height: 0;
}

.story-dialog__close {
  align-self: flex-end;
}


/* --------------------------------------------------------------------------
   13. Slide-out contact panel
   A disclosure, not a modal — it does not trap focus, because a student
   halfway through checkout should be able to tab back out of it.
   -------------------------------------------------------------------------- */

.contact-tab {
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 80;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  /* [WCAG 1.4.11] The tab is fixed and the page scrolls under it, so it spends
     part of every page sitting on a .section--navy band — whose background is
     var(--navy), the tab's own fill. Boundary contrast there was 1:1 and the
     control simply vanished. The border is what draws its edge.

     It is var(--on-navy) and NOT a hard-coded white, for the reason in §6 of
     the handoff: dark-mode navy is a pale blue, so a white outline would
     disappear on the very band it exists to solve. The token is white in light
     theme, near-black in dark, and visible against the band in both.

     It is also close to invisible against paper and ground, which is correct —
     the outline should only assert itself where the tab would otherwise be
     lost, and nothing needs to know which band is underneath.

     No border-right: the radius is on the left corners only, because the tab
     reads as emerging from the edge of the screen. Closing that edge would
     draw a box the shape denies. The narrow-viewport rule below moves the tab
     to the bottom and flips which edge is the open one. */
  border: 2px solid var(--on-navy);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--navy);
  color: var(--on-navy);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: bold;
  cursor: pointer;
  writing-mode: vertical-rl;
  /* Two rings, because the tab crosses two kinds of background and one colour
     cannot serve both.

       - the white border above shows against the navy band
       - this 1px navy ring shows against paper and ground

     Only 2 of the 21 pages carry a .section--navy band at all, so on the other
     19 the white border alone was invisible and the tab looked unchanged. The
     ring is what makes the outline read everywhere.

     It is a box-shadow rather than an outline so it cannot collide with the
     focus ring, which is a real outline and has to stay distinguishable.
     [WCAG 2.4.7] */
  box-shadow:
    0 0 0 1px var(--navy),
    -2px 0 10px rgba(0, 32, 96, 0.18);
}
.contact-tab:hover { background: var(--navy-hover); }

.contact-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  width: min(26rem, 100vw);
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border-left: 1px solid var(--line-strong);
  box-shadow: -8px 0 32px rgba(0, 32, 96, 0.16);
  transition: transform 200ms ease-out;
}

/* Closed state uses [hidden] rather than an off-screen transform, so the form
   inside is genuinely unreachable by keyboard when shut. */
.contact-panel[hidden] { display: none; }

.contact-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5);
  border-bottom: 1px solid var(--line);
}

.contact-panel__body {
  padding: var(--space-5);
  overflow-y: auto;
  flex: 1;
}

/* [WCAG 1.4.10] Below this width a fixed 26rem flyout would force horizontal
   scrolling, so it becomes full-screen instead. This also covers 400% zoom on
   a desktop, which reports a narrow viewport. */
@media (max-width: 34rem) {
  .contact-panel { width: 100vw; }
  /* The tab now sits on the bottom edge, so the bottom is the open edge and the
     right-hand one closes again. Keeps the outline consistent with the radius. */
  .contact-tab { writing-mode: horizontal-tb; top: auto; bottom: 0; transform: none; border-radius: var(--radius) var(--radius) 0 0; right: var(--space-4); border-right: 2px solid var(--on-navy); border-bottom: 0; }
}


/* --------------------------------------------------------------------------
   14. Forms
   -------------------------------------------------------------------------- */

/* [WCAG 1.3.1] The browser's own `[hidden] { display: none }` is a user-agent
   rule with the lowest weight there is, so ANY component rule that sets display
   silently beats it. `.field` below is display:flex — which meant the
   conditional Country box on quote.html and demonstration.html stayed fully on
   screen, 641x119px, while its `hidden` attribute said it was not there.
   Nothing looked wrong; the attribute was toggling correctly the whole time.

   The field was `disabled` alongside `hidden`, so no empty country was ever
   submitted. The fault was visual only — but a field marked hidden must be
   hidden, and every future flex or grid component would hit the same trap.
   `!important` is deliberate: `hidden` means "not currently present", and no
   component has a good reason to override it. Found 8 August 2026 by measuring
   computed display rather than trusting the attribute. */
[hidden] { display: none !important; }

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-weight: bold;
  font-size: var(--text-sm);
}

/* [WCAG 3.3.2] Hints are tied to the input with aria-describedby, not left
   floating as unassociated text. */
/* "(optional)" rather than an asterisk on everything else. On the quote form
   nine of ten fields are required, so marking the exception is one word in one
   place against nine marks plus a legend at the top explaining what they mean —
   and a legend at the top is invisible to anyone who arrives mid-form from the
   emailed link.

   It is part of the label, not a separate note, so it is read out with the field
   name and cannot drift away from it. [WCAG 3.3.2] Deliberately NOT aria-hidden:
   the browser announces "required" from the required attribute, and this is the
   matching information for the fields that are not. */
.field__optional {
  font-weight: normal;
  color: var(--muted);
}

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

/* A quantity box holds a number, not a sentence, so it is sized to its content
   instead of stretching the width of the card. It keeps the 44px minimum height
   from .field__control below [WCAG 2.5.5], and the width is generous enough for
   three digits plus the browser's own spinner.

   No max attribute on the input, deliberately — Chris, 9 August 2026: the volume
   message is an offer, not a gate, and a cap would stop a sale. */
.field__control--qty { width: 7rem; }

.field__control {
  width: 100%;
  min-height: 44px;
  padding: var(--space-3);
  border: 1px solid var(--line-strong);   /* [WCAG 1.4.11] 3:1 against paper */
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
}
.field__control:hover { border-color: var(--muted); }

textarea.field__control { min-height: 8rem; resize: vertical; }

/* [WCAG 1.4.1 / 3.3.1] Errors are marked by an icon, a border, a text message
   and aria-invalid — never by colour on its own. */
.field__control[aria-invalid="true"] {
  border-color: var(--warn);
  border-width: 2px;
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: bold;
  color: var(--warn);
}
.field__error[hidden] { display: none; }

/* The honeypot. Moved off-screen rather than display:none, because some bots
   skip anything display:none and the trap would never be sprung. It carries
   aria-hidden and tabindex="-1" in the markup, so nothing reaches it by screen
   reader or by keyboard -- all three together, never one alone. */
.spam-guard {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

fieldset {
  margin: 0;
  padding: var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
legend { font-weight: bold; padding-inline: var(--space-2); }


/* --------------------------------------------------------------------------
   15. School search combobox
   ARIA 1.2 combobox pattern. This is the most commonly botched widget on the
   web; the markup in shop.html carries the required attributes and site.js
   the keyboard behaviour. [WCAG 4.1.2, 2.1.1]
   -------------------------------------------------------------------------- */

/* The listbox must anchor to the INPUT, not to the field group.
   An absolutely-positioned child of a flex container takes its static position
   from the container's content box, not from where it sits in source order —
   so anchoring to .combo (which also holds the label and hint) drops the list
   over the top of them. The input and listbox therefore share their own
   positioning context. */
.combo__anchor { position: relative; }

.combo__listbox {
  list-style: none;
  margin: 0;
  padding: var(--space-1);
  position: absolute;
  top: calc(100% + var(--space-1));
  inset-inline: 0;
  z-index: 40;
  max-height: 18rem;
  overflow-y: auto;
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: 0 10px 24px rgba(0, 32, 96, 0.12);
}
.combo__listbox[hidden] { display: none; }

.combo__option {
  padding: var(--space-3);
  border-radius: var(--radius);
  cursor: pointer;
  line-height: 1.35;
}
.combo__option:hover { background: var(--navy-tint); }

/* The visual highlight follows aria-activedescendant, so what a sighted user
   sees and what a screen reader announces stay in step. */
.combo__option[aria-selected="true"] {
  background: var(--navy);
  color: var(--on-navy);
}

.combo__option-meta {
  display: block;
  font-size: var(--text-sm);
  opacity: 0.85;
}

.combo__empty {
  padding: var(--space-4);
  color: var(--muted);
}


/* --------------------------------------------------------------------------
   16. Step indicator
   -------------------------------------------------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.steps__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  color: var(--muted);
}

/* Current step carries a text label for screen readers as well as the styling. */
.steps__item[aria-current="step"] {
  background: var(--navy-tint);
  color: var(--navy);
  font-weight: bold;
}

.steps__item--done { color: var(--ok); }


/* --------------------------------------------------------------------------
   16b. Price

   The figure a buyer is deciding on, so it is set larger than the copy round
   it. Added 8 August 2026 for the Industry Store, which shows six of them —
   `classes.html` still carries the same values as an inline style on its single
   price, from before there was a second place that needed one.
   -------------------------------------------------------------------------- */

.price {
  font-size: var(--text-2xl);
  font-weight: bold;
}


/* --------------------------------------------------------------------------
   17. Callouts
   -------------------------------------------------------------------------- */

.callout {
  border-left: 3px solid var(--navy);
  background: var(--navy-tint);
  padding: var(--space-4) var(--space-5);
  border-radius: 0 var(--radius) var(--radius) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.callout--brass { border-left-color: var(--brass); background: var(--brass-tint); }
.callout--ok    { border-left-color: var(--ok);    background: var(--ok-tint); }


/* --------------------------------------------------------------------------
   18. Lists
   -------------------------------------------------------------------------- */

.tick-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: var(--measure);
}

/* NOT a grid, and that is the fix rather than the style choice.
   [WCAG 1.4.10] It was `display: grid` with `1.5rem minmax(0, 1fr)`. A grid
   makes every child its own item INCLUDING bare text, so an item written as
   `<strong>Check every character.</strong> Zero against capital O...` produced
   three items, not two: the marker, the <strong>, and then the loose text —
   which wrapped onto the next row into the 27px marker column and set one
   character per line, 849px tall, at every viewport width. Items that are plain
   text were unaffected, which is why it survived so long.

   Indent plus an absolutely positioned marker keeps all the inline content in a
   single flow, so it wraps normally however many elements are mixed into it.
   Found 8 August 2026. Do not put the grid back. */
.tick-list li {
  position: relative;
  padding-left: calc(1.5rem + var(--space-3));
}

/* Decorative marker — hidden from screen readers, which already announce the
   list and its item count. */
.tick-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.45em;
  width: 0.65rem;
  height: 0.65rem;
  background: var(--navy);
  border-radius: 1px;
  transform: rotate(45deg);
}


/* --------------------------------------------------------------------------
   19. Project gallery
   Photographs of parts students have actually made. This is the most credible
   content on the site — an instructor can tell in one glance whether the work
   is real, and no stock photograph survives that test.

   Every image needs alt text describing what matters about the part: the
   operation, the feature, the tolerance being held. "A metal part" tells a
   screen reader user nothing and fails [WCAG 1.1.1] in spirit if not letter.
   -------------------------------------------------------------------------- */

.gallery {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}

.gallery__item {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

/* A fixed aspect ratio keeps the grid tidy regardless of what the camera did.
   object-fit stops varied source images from distorting. */
.gallery__image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--navy-tint);
  border-bottom: 1px solid var(--line);
}

.gallery__caption {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.gallery__title { font-weight: bold; font-size: var(--text-sm); }

.gallery__meta {
  font-size: var(--text-xs);
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   Screenshots
   Product screenshots carry more weight than any description, so they get a
   frame and a caption rather than floating loose in the text.
   -------------------------------------------------------------------------- */

.shot {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.shot img {
  width: 100%;
  height: auto;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  /* A dark UI screenshot on a light page needs a little separation. */
  box-shadow: 0 2px 12px rgba(0, 32, 96, 0.10);
}

.shot figcaption {
  font-size: var(--text-sm);
  color: var(--muted);
  max-width: var(--measure);
}

/* Side-by-side with body copy on wide screens, stacked below. */
.shot-split {
  display: grid;
  gap: var(--space-6);
  align-items: center;
}
@media (min-width: 60rem) {
  .shot-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: var(--space-7); }
  .shot-split--flip > .shot { order: -1; }
}

/* Placeholder shown while real photography is outstanding. Delete the rule and
   the class together once images land — it should not survive to production. */
.gallery__image--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-alt);
  font-size: var(--text-xs);
  font-weight: bold;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--navy);
  border: 1px dashed var(--line-strong);
}


/* --------------------------------------------------------------------------
   20. Legal documents
   Privacy policy, EULA and the renewal agreement, as HTML rather than linked
   PDFs. PDFs are one of the most common accessibility failures on otherwise
   conformant sites — they resist screen readers, do not reflow at high zoom,
   and cannot be read comfortably on a phone. These are also the pages an
   institutional buyer's procurement office actually reads.
   -------------------------------------------------------------------------- */

.legal { max-width: 42rem; }

.legal h2 {
  font-size: var(--text-xl);
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

/* The first section should not carry a rule — it sits directly under the
   document meta block, which already provides separation. */
.legal h2:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }

.legal h3 {
  font-size: var(--text-lg);
  margin-top: var(--space-5);
}

.legal p,
.legal li { max-width: none; }

.legal ul,
.legal ol {
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Document metadata — effective date, governing entity, contact. */
.legal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  padding: var(--space-4) var(--space-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: var(--text-sm);
}

.legal-meta div { display: flex; flex-direction: column; gap: 2px; }
.legal-meta dt { color: var(--muted); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; }
.legal-meta dd { margin: 0; font-weight: bold; }

/* In-page contents for the longer agreements. */
.legal-toc { padding: var(--space-4) var(--space-5); }
.legal-toc ol {
  columns: 2;
  column-gap: var(--space-6);
  font-size: var(--text-sm);
  padding-left: var(--space-5);
  display: block;
}
.legal-toc li { margin-bottom: var(--space-2); break-inside: avoid; }

@media (max-width: 40rem) {
  .legal-toc ol { columns: 1; }
}

/* Safety clauses carry real consequence — these documents concern machine
   tools. Giving them visual weight is honest, not decorative. */
.legal .callout--safety {
  border-left-color: var(--warn);
  background: var(--warn-tint);
}


/* --------------------------------------------------------------------------
   21. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--paper);
  border-top: 3px solid var(--navy);
  padding-block: var(--space-7) var(--space-6);
  margin-top: var(--space-8);
}

.site-footer__grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}

.site-footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.site-footer__bottom {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
  font-size: var(--text-sm);
  color: var(--muted);
}


/* --------------------------------------------------------------------------
   20. Responsive hardening
   The layout above is fluid by construction — auto-fit grids, clamp() type and
   min()/max-width rather than fixed pixel widths — so it reflows rather than
   breaking. These rules cover the edges that fluid layout alone does not.

   Target range: 320px (an older phone) through 2560px, plus 400% desktop zoom,
   which browsers report as a viewport around 320–400px wide. [WCAG 1.4.10]
   -------------------------------------------------------------------------- */

/* Long words — part numbers, email addresses, URLs — must not force the page
   sideways on a narrow screen. */
body {
  overflow-wrap: break-word;
  word-break: normal;
}

/* Nothing may push the document wider than the viewport. If something does,
   this makes it obvious in testing rather than silently scrolling the body. */
html, body { overflow-x: hidden; }

/* The wordmark is set in Arial Black, which is wide. Below 26rem it needs to
   step down or it crowds the menu button on a 320px phone. */
@media (max-width: 26rem) {
  .lockup--sm { font-size: 1.2rem; }
  .site-header__inner { gap: var(--space-3); }
}

/* Tablets in portrait and Chromebooks in split-screen land here. The card grid
   already handles itself; the hero needs its gap pulled in. */
@media (max-width: 62rem) {
  .hero { padding-block: var(--space-7); }
  .hero__title { font-size: var(--text-3xl); }
}

/* Very wide screens: stop the measure from stretching into something unreadable
   even though the container is capped. */
@media (min-width: 100rem) {
  :root { --container: 1320px; }
}

/* Touch devices: hover styles that latch on tap are worse than no hover styles.
   Only apply them where a real pointer exists. */
@media (hover: none) {
  .card__title a:hover { text-decoration: none; }
  .combo__option:hover { background: transparent; }
}

/* Coarse pointers get a little more room. [WCAG 2.5.8] is satisfied at 24px;
   44px is the practical target, and gloved hands in a workshop are a real case. */
@media (pointer: coarse) {
  /* Bottom stays deeper than top so the current-page rule keeps its clearance. */
  .site-nav__link { padding-block: var(--space-3) calc(var(--space-3) + 5px); }
  .site-footer__list a { display: inline-block; padding-block: var(--space-1); }
}

/* Landscape phones are short, not just narrow. Anything pinned to the viewport
   height needs to stay scrollable. */
.contact-panel__body,
.story-dialog__body { overscroll-behavior: contain; }


/* --------------------------------------------------------------------------
   21. Print
   Course pages get printed and taken into workshops. Worth ten lines.
   -------------------------------------------------------------------------- */

@media print {
  .site-header,
  .site-footer,
  .contact-tab,
  .contact-panel,
  .skip-link { display: none !important; }

  body { background: #FFFFFF; color: #000000; font-size: 11pt; }
  a { text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}


/* --------------------------------------------------------------------------
   22. 3D model cards
   Three across on a wide screen, so a part sits in a third of the row rather
   than stretched over the whole page. The stage keeps a fixed 4:3 box whether
   it is holding the placeholder or a loaded model, so nothing reflows when
   somebody presses the button.

   Only existing tokens are used. --paper against --ink is a pairing already
   proven in both themes.
   -------------------------------------------------------------------------- */

.model-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 45rem) { .model-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .model-grid { grid-template-columns: repeat(3, 1fr); } }

.model-card {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

/* White in BOTH themes, deliberately. The renders arrive on a white ground, so
   the stage matches it exactly and there is no visible rectangle inside the
   box. A themed background would put a white plate on a dark card.

   Because the stage does not follow the theme, the text drawn on it cannot use
   the theme tokens either — see .model-card__mark and friends below, which
   carry fixed dark values. */
.model-card__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #FFFFFF;
  border-bottom: 1px solid var(--line);
}

/* The model fills the stage once it exists. */
.model-card__stage model-viewer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: transparent;
}

/* Overlay text, laid out the way the original part viewers had it. */
.model-card__overlay {
  position: absolute;
  top: var(--space-3);
  left: var(--space-4);
  z-index: 2;
  display: flex;
  flex-direction: column;
  pointer-events: none;
}

.model-card__mark {
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: 1.05rem;
  line-height: 1.15;
  /* Fixed, not --ink: the stage is white in both themes. 17.8:1 on white. */
  color: #14181F;
}

.model-card__part {
  font-size: var(--text-xs);
  color: #5A6478;   /* 5.95:1 on white */
}

.model-card__addr {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-3);
  z-index: 2;
  font-family: var(--font-alt);
  font-weight: bold;
  font-size: var(--text-xs);
  color: #002060;   /* 15.9:1 on white */
  pointer-events: none;
}

/* The control sits under the box, as its own row across the full card width. */
.model-card__control {
  display: block;
  width: 100%;
  padding: var(--space-3);
  border: 0;
  border-top: 1px solid var(--line);
  background: var(--paper);
  color: var(--navy);
  font: inherit;
  font-weight: bold;
  text-align: center;
  cursor: pointer;
}
.model-card__control:hover { background: var(--navy-tint); }
.model-card__control[disabled] { color: var(--muted); cursor: default; }

@media print {
  /* A WebGL canvas prints as an empty box. */
  .model-card__stage model-viewer { display: none; }
  .model-card__control { display: none; }
}

/* The picture is clickable as well as the control below it. Hover lifts it so
   it reads as something you can press; the button underneath is what keyboard
   and screen reader users operate. */
/* The frame lights up, not the background. A render sits on its own ground and
   tinting that ground behind it looks like a mistake — and it would fight the
   part's own white in any image that is not cut out.

   No transition on border-color: Chromium fails to re-resolve a transitioned
   property when the custom property behind it changes on a theme switch. Same
   reason .btn has none. */
.model-card__stage.is-interactive { cursor: pointer; }

/* Pointer over the card: the frame picks up. */
.model-card:has(.is-interactive):hover,
.model-card:focus-within {
  border-color: var(--navy);
  box-shadow: 0 0 0 3px var(--navy-tint);
}

/* Model open: the same treatment, turned up, so it is obvious which of a row
   of cards is the live one. Set by site.js on the stage. */
.model-card:has(.is-open) {
  border-color: var(--navy);
  box-shadow: 0 0 0 6px var(--navy-tint);
}

/* [WCAG 2.4.7] The model takes focus so it can be rotated from the keyboard,
   so it needs a visible focus ring like any other control. */
.model-card__stage model-viewer:focus-visible {
  outline: 3px solid var(--navy);
  outline-offset: -3px;
}

/* The still sits in the stage until the model replaces it. contain rather than
   cover so the whole part is visible — these are renders on white, and cropping
   one to fill a 4:3 box would cut the flange off. */
.model-card__stage picture { position: absolute; inset: 0; }
.model-card__still {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-5) var(--space-4) var(--space-6);
}

/* --------------------------------------------------------------------------
   23. Specification lists
   A description list is the correct element for name/value pairs, and it keeps
   the value from running on from its label as if it were a sentence. Wraps to
   two lines in a narrow card rather than squeezing the value.
   -------------------------------------------------------------------------- */

.spec-list {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: var(--measure);
}

.spec-list > div {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
}

.spec-list dt { color: var(--muted); }
.spec-list dd { margin: 0; font-weight: bold; }

/* A figure with no frame — for product renders on a transparent background,
   where a border and shadow would draw a box around empty space. Capped so a
   near-square image does not run the full container width and become a
   thousand pixels tall. */
.shot--plain img {
  border: 0;
  box-shadow: none;
  max-width: 60rem;
  margin-inline: auto;
}

/* --------------------------------------------------------------------------
   24. Master3DGage panel
   Fills the two grid cells left empty beside the hand scanner on the second
   row. The span only applies once the grid is genuinely three columns wide —
   below that the grid is one or two columns and a span-2 element would create
   an implicit extra column and overflow the container.
   -------------------------------------------------------------------------- */

@media (min-width: 64rem) {
  .card-grid > .grid-span-2 { grid-column: span 2; }
}

/* [WCAG 1.4.10] auto-fit, and NO viewport media query - that is the fix.

   This panel is a CARD inside .card-grid and only spans two columns above
   64rem, so its own width comes from a grid track, not from the viewport. The
   old rule was `@media (min-width: 34rem) { grid-template-columns: minmax(0,
   19rem) minmax(0, 1fr) }`, which fired whenever the WINDOW was wide enough
   even when the card was not. Measured before the fix: at a 900px viewport the
   card was 389px, the arm took its full 19rem (342px) and the specifications
   column resolved to 0px, setting "SPECIFICATIONS" one letter per line and
   403px tall. At 560px the detail column was 32px.

   auto-fit with a 16rem minimum asks the same question against the space that
   actually exists: two columns while both fit, otherwise one, and the
   specifications drop BELOW the arm. No breakpoint to get wrong.

   A flex-wrap version was tried first and was worse - it pushed the arm to the
   right and off the card, because .card sets flex-direction: column and the
   panel inherited it. Found 8 August 2026. Do not reinstate a viewport media
   query here: this element cannot see the viewport, only its card. */
.m3dg {
  display: grid;
  /* min(16rem, 100%), not a bare 16rem. A bare minimum larger than the card
     cannot shrink, so at 320px the column resolved to 288px inside a 266px card
     and pushed the arm 50px past its own edge — horizontal scroll on the whole
     page. [WCAG 1.4.10] */
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--space-5);
  align-items: start;
}
.m3dg__figure { max-width: 19rem; min-width: 0; }
.m3dg__detail { min-width: 0; }

/* The title runs across both columns, above the arm and the specifications, and
   is set larger than a card heading — this panel is the feature of the row, not
   one of the four products beside it. */
.m3dg__title {
  grid-column: 1 / -1;
  font-size: var(--text-xl);
  margin-bottom: calc(var(--space-2) * -1);
}

/* Section labels inside the panel carry more weight than a normal eyebrow, and
   sit in navy rather than grey so they read as headings rather than captions.
   Navy on paper is 15.9:1 in the light theme; the token flips in the dark one. */
.m3dg .eyebrow {
  font-size: var(--text-base);
  letter-spacing: 0.08em;
  color: var(--navy);
}

/* The detail itself is set smaller, so the labels lead and the figures follow
   rather than competing. */
.m3dg .spec-list > div { font-size: var(--text-xs); }
.m3dg .tick-list { font-size: var(--text-sm); gap: var(--space-2); }

/* The arm is a cut-out on a transparent ground, so no frame — a border here
   would draw a box around empty space. */
.m3dg__figure img {
  width: 100%;
  height: auto;
  max-height: 34rem;
  object-fit: contain;
  object-position: top center;
}


/* --------------------------------------------------------------------------
   25. Hero video band
   A looping shot of the work itself, sitting below the hero as its own band.

   No text sits over it, deliberately. [WCAG 1.4.3] Text on moving footage has
   to clear 4.5:1 against every frame, and this footage runs from a bright
   chuck to a dark machine bed inside a single shot. No scrim opacity is
   defensible across all of it, so the words stay above the video instead.

   The poster and the video occupy the same box and the box keeps its shape
   from the aspect-ratio below, so nothing reflows when playback starts and the
   page never jumps while the video loads.
   -------------------------------------------------------------------------- */

/* The band sits deliberately close under the hero, so its top edge shows at the
   bottom of a laptop screen. Motion at the fold reads as a scroll cue and pulls
   the eye down, without displacing the headline or the two buttons — which is
   why the video is here rather than above the hero.

   A considered exception to the 72px section rhythm of §12.4b: the hero and the
   band are one unit, not two independent sections. 64px + 32px becomes
   32px + 24px.

   Scoped with :has() because .hero is also used on certification.html, which has
   no band and must keep its original spacing. Where :has() is unsupported the
   hero simply keeps its roomier padding — the band's own reduction still
   applies, so the result is a smaller improvement rather than a broken layout. */
.hero:has(+ .video-band) { padding-bottom: var(--space-6); }
.video-band { padding-top: var(--space-5); }

.video-band__frame {
  position: relative;
  /* The default matches the hero's encoded 1280x416 exactly. Set here rather
     than left to the intrinsic size so the space is reserved before either
     asset arrives. 1280 is not a compromise: the band renders about 1126px
     wide and the container caps at 72rem, so a larger encode would ship pixels
     the page cannot display. Measured on the built page, not assumed.

     It is a custom property because the same frame, poster, video and pause
     control now also drive the click-to-play videos inside a gallery, which
     are 4:3. One mechanism, one control, one set of rules — see
     .gallery__video below and initVideoFrames() in site.js. */
  aspect-ratio: var(--video-ratio, 1280 / 416);
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--ground);
}

/* A video used as a gallery card. The card around it already draws the border
   and the rounded corner, so the frame drops both and takes the same bottom
   rule and tint .gallery__image carries — otherwise a video card and a picture
   card sit differently in the same grid.

   4:3 matches the encode, which is cropped to 4:3 in ffmpeg rather than
   cover-cropped here, for the same reason the gallery stills are: the browser
   is never handed pixels it will only throw away. */
.gallery__video {
  --video-ratio: 4 / 3;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: var(--navy-tint);
}

.video-band__poster,
.video-band__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The video sits above the poster once it can actually paint. Until then the
   poster is what shows — see initHeroVideo, which only reveals the video after
   its first frame is decoded. */
.video-band__video { opacity: 0; transition: opacity 240ms ease; }
.video-band__frame.is-playing .video-band__video { opacity: 1; }

/* The pause control. Bottom-right, over the footage, so it is beside the thing
   it controls rather than adrift below it.

   [WCAG 1.4.11] The button is a solid navy chip rather than a transparent
   overlay: a translucent control over moving video has no stable contrast
   against its own background, and non-text contrast still has to be met.
   [WCAG 2.5.5] 44px minimum target. */
.video-band__toggle {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  z-index: 2;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 2px solid var(--on-navy);
  border-radius: 999px;
  background: var(--navy);
  color: var(--on-navy);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: bold;
  cursor: pointer;
}

/* Deliberately no colour transition. [WCAG 1.4.3] Chromium fails to re-resolve
   a transitioned property when the custom property behind it changes, which is
   what left secondary buttons at 1.15:1 after a theme switch. Same rule as
   .btn — do not add one here either. */
.video-band__toggle:hover { background: var(--navy-ink, var(--navy)); }
.video-band__toggle:focus-visible { outline-color: var(--on-navy); }

/* On the hero band the control only exists when there is motion to stop, so it
   is absent for a reduced-motion visitor, a no-JavaScript visitor, and anyone
   whose browser refused to autoplay. Nothing moves in those cases and 2.2.2
   does not apply.

   A click-to-play gallery video is the other way round: it never moves until
   the visitor presses Play, so the control is shown from the start and reads
   "Play". That is also why reduced motion does not remove it — the motion is
   requested, not imposed, and taking the control away would remove the video
   rather than the animation. A no-JavaScript visitor still gets the poster and
   its alt text, which is the whole point of the poster carrying a real
   description on those cards. */

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