/* assets/design-system.css
 *
 * The ParaLearn design system. Loaded LAST on every page -- after site.css and
 * after index.html's inline <style> -- so it can set the visual direction
 * without either of those being rewritten.
 *
 * WHY A THIRD STYLESHEET RATHER THAN EDITING THE OTHER TWO
 *
 * index.html carries its own inline copy of most of site.css, deliberately, so
 * the homepage paints without waiting for a stylesheet. That means any change
 * made in one place has to be made in both, and the two have drifted before.
 * One file loaded after both is a single place to write, and it cannot fall
 * out of step with itself.
 *
 * It stays BEFORE the injected <style id="site-theme">, so the admin's theme
 * editor still overrides these tokens. Nothing here renames an existing
 * variable: --g, --gd, --gp, --hero, --brass, --bg, --t, --tm, --r,
 * --font-heading and --font-body are the names the theme editor writes, and
 * renaming one would silently disconnect that control.
 *
 * DIRECTION
 *
 * Clinical, calm, and mostly neutral. The green is an accent for actions and
 * emphasis, not a wash over whole sections -- a page that is green everywhere
 * has no way left to say "this is the important bit". Surfaces are warm white,
 * text is near-black, borders do the structural work and shadows are reserved
 * for things that genuinely lift off the page.
 */

:root {
  /* ─── spacing ───
     A 4px-based scale. Every gap, pad and margin below picks from it, so
     vertical rhythm is consistent instead of being re-guessed per section. */
  --s-1: 4px;   --s-2: 8px;   --s-3: 12px;  --s-4: 16px;
  --s-5: 20px;  --s-6: 24px;  --s-8: 32px;  --s-10: 40px;
  --s-12: 48px; --s-16: 64px; --s-20: 80px; --s-24: 96px;

  /* ─── type scale ───
     Fluid between a phone and a laptop so headings do not need a media query
     each. The floor matters more than the ceiling here: this is read on a
     phone on a bus, so body text never drops below 16px and never becomes the
     oversized 19-20px that forces constant scrolling. */
  --fs-display: clamp(2rem, 1.45rem + 2.6vw, 3.25rem);
  --fs-h1:      clamp(1.75rem, 1.35rem + 1.9vw, 2.6rem);
  --fs-h2:      clamp(1.4rem, 1.18rem + 1.05vw, 1.95rem);
  --fs-h3:      clamp(1.12rem, 1.03rem + 0.42vw, 1.32rem);
  --fs-body:    1rem;
  --fs-sm:      0.9375rem;
  --fs-xs:      0.8125rem;
  --fs-label:   0.75rem;

  --lh-tight: 1.15;
  --lh-snug:  1.3;
  --lh-body:  1.65;

  /* Measure. Long lines are the single most common readability failure on a
     wide screen, and 62-68 characters is the usual comfortable range. */
  --measure: 64ch;
  --measure-sm: 52ch;

  /* ─── radii ───
     One step per role. --r is the existing token the theme editor writes, so
     the others are derived from it and a change to it still moves everything
     together. */
  --r-sm: 8px;
  --r-lg: 18px;
  --r-pill: 999px;

  /* ─── elevation ───
     Deliberately restrained. A border carries most structure; a shadow means
     "this is above the page", which should be rare. */
  --e-1: 0 1px 2px rgba(20, 30, 22, .06);
  --e-2: 0 4px 16px rgba(20, 30, 22, .08);
  --e-3: 0 12px 32px rgba(20, 30, 22, .12);

  /* ─── semantic surfaces ───
     Named by role, not by colour, so a section can be restyled without
     hunting down every hex. Built on the existing brand tokens. */
  --surface:       var(--bg);
  --surface-sunk:  #FAF9F6;
  --surface-quiet: #F5F4F0;
  --surface-brand: rgba(60, 120, 81, .06);
  --line:          var(--gb);
  --line-strong:   #D6D2C8;
  --ink:           var(--t);
  --ink-muted:     var(--tm);
  --ink-quiet:     var(--tl);

  --focus: 0 0 0 3px rgba(60, 120, 81, .35);
}

/* ─── base ─────────────────────────────────────────────────────────────── */

body {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Headings set tight and dark. Weight does the emphasis, not colour, so a
   heading still reads as a heading when the brand colour is changed. */
/* Rhythm only -- deliberately no colour. An earlier version set
   color: var(--ink) here, which matched .h at equal specificity and, loading
   later, overrode .h1{color:#fff} in the hero: the headline turned near-black
   on a near-black background and all but disappeared. Headings already
   inherit a sensible colour from their own context, including the light-on-
   dark ones, and this file has no business guessing which is which. */
h1, h2, h3, .h, .st {
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
  text-wrap: balance;
}
h4, h5, h6 { line-height: var(--lh-snug); letter-spacing: -0.01em; }

p { text-wrap: pretty; }

/* ─── accessibility ───
   A visible focus ring on anything reachable by keyboard. :focus-visible
   rather than :focus so a mouse click does not leave a ring behind, which is
   the usual reason people delete focus styles altogether. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--g);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* ─── layout rhythm ────────────────────────────────────────────────────── */

.sec { padding: var(--s-16) 0; }
@media (min-width: 768px) { .sec { padding: var(--s-20) 0; } }
@media (min-width: 1200px) { .sec { padding: var(--s-24) 0; } }

.con { max-width: 1120px; padding-inline: var(--s-5); }
@media (min-width: 768px) { .con { padding-inline: var(--s-8); } }

/* Section heading block: eyebrow, heading, one line of support. Used instead
   of each section inventing its own header spacing. */
.pl-head { max-width: var(--measure); margin-bottom: var(--s-10); }
.pl-head.pl-center { margin-inline: auto; text-align: center; }
.pl-head .ey {
  display: inline-block;
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--gd);
  margin-bottom: var(--s-3);
}
.pl-head h2 { font-size: var(--fs-h2); margin: 0 0 var(--s-3); }
.pl-head p {
  font-size: var(--fs-sm);
  color: var(--ink-muted);
  margin: 0;
  max-width: var(--measure-sm);
}
.pl-head.pl-center p { margin-inline: auto; }

/* ─── buttons ───
   One shape, three weights. Min height 44px because these are tapped on a
   phone, which is most of the traffic. */
.btn {
  border-radius: var(--r-sm);
  font-weight: 650;
  letter-spacing: -0.01em;
  min-height: 44px;
  padding: var(--s-3) var(--s-6);
  transition: background-color .16s ease, border-color .16s ease,
              color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.btn.bg2 { box-shadow: var(--e-1); }
.btn.bg2:hover { box-shadow: var(--e-2); transform: translateY(-1px); }
.btn.bo { border-width: 1.5px; }
.btn:active { transform: translateY(0); }

/* A third, quieter action for "and also…" links that are not the main choice. */
.pl-btn-quiet {
  background: transparent;
  color: var(--gd);
  border: none;
  font-weight: 650;
  text-decoration: underline;
  text-underline-offset: 3px;
  padding: var(--s-3) var(--s-2);
}
.pl-btn-quiet:hover { color: var(--ink); }

/* ─── cards ───
   Border first, shadow only on hover. Restrained by default so a grid of them
   reads as a list rather than a pile of floating panels. */
.pl-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-6);
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.pl-card:hover { border-color: var(--line-strong); box-shadow: var(--e-2); }

/* A card that is a link to something: lifts very slightly, to say it is
   clickable without turning the page into a trampoline. */
a.pl-card:hover, .pl-card-interactive:hover { transform: translateY(-2px); }

/* ─── badges and pills ─────────────────────────────────────────────────── */
.pl-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 3px var(--s-2);
  border-radius: var(--r-pill);
  background: var(--surface-quiet);
  color: var(--ink-muted);
  border: 1px solid var(--line);
}
.pl-badge-free { background: var(--surface-brand); color: var(--gd); border-color: rgba(60,120,81,.25); }
.pl-badge-accent { background: var(--brass-tint); color: var(--brass-ink); border-color: rgba(191,143,58,.3); }

/* ─── price ───
   Prices are data, not decoration: same size and weight everywhere so they
   can be compared down a column at a glance. */
.pl-price { font-size: 1.0625rem; font-weight: 750; color: var(--ink); letter-spacing: -0.01em; }
.pl-price-free { color: var(--gd); }
.pl-price-was { font-size: var(--fs-sm); font-weight: 500; color: var(--ink-quiet); text-decoration: line-through; margin-left: var(--s-2); }

/* ─── grids ───
   auto-fit with a floor, so a row reflows instead of needing a breakpoint per
   column count. The floor is what keeps a card readable on a phone. */
.pl-grid { display: grid; gap: var(--s-5); }
.pl-grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.pl-grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.pl-grid-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); }
@media (min-width: 768px) { .pl-grid { gap: var(--s-6); } }

/* ─── trust strip ───
   Four short facts under the hero. Deliberately not four cards: they are one
   line of positioning, and making each a panel gives them more weight than
   they have earned. */
.pl-trust {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-8);
  padding: var(--s-5) 0;
  border-block: 1px solid var(--line);
  list-style: none;
  margin: 0;
}
.pl-trust li {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-muted);
}
.pl-trust li::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--g);
  flex: none;
}
@media (max-width: 640px) {
  .pl-trust { gap: var(--s-3) var(--s-5); }
  .pl-trust li { font-size: var(--fs-xs); }
}

/* ─── forms ───
   Comfortable targets and a real focus state. Font-size stays at 16px on
   phones because anything smaller makes iOS zoom the whole page on focus. */
input, select, textarea {
  font-family: inherit;
  font-size: max(16px, var(--fs-sm));
  border-radius: var(--r-sm);
  min-height: 44px;
}
input:focus, select:focus, textarea:focus { box-shadow: var(--focus); }

/* ─── motion ───
   Subtle, and off entirely for anyone who has asked for less. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ─── never let a page scroll sideways ───
   One overflowing element is enough to make every page on a phone feel
   broken, and it is usually invisible on a laptop. */
html, body { overflow-x: clip; }
img, svg, video { max-width: 100%; height: auto; }

/* ═══════════════════════════════════════════════════════════════════════
   COMPONENTS
   These restyle the classes the pages already use, rather than asking the
   markup to change. A redesign that needs every template rewritten is a
   redesign that breaks things; this one only needs the stylesheet to load.
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── hero ───
   The dark hero stays: it is the most recognisable thing about the site and
   it reads as clinical rather than commercial. What changes is the restraint
   -- more room, tighter headline, a sub-line at a readable measure, and a
   badge that no longer competes with the headline for attention. */
.hero {
  padding: var(--s-16) var(--s-5) var(--s-20);
  background: linear-gradient(180deg, var(--hero) 0%, color-mix(in srgb, var(--hero) 88%, #000) 100%);
}
@media (min-width: 768px) { .hero { padding: var(--s-24) var(--s-8); } }

.hi { gap: var(--s-12); }
@media (min-width: 900px) { .hi { gap: var(--s-16); } }

.hbadge {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .01em;
  padding: 6px var(--s-3);
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.10);
  border-color: rgba(255,255,255,.18);
  color: rgba(255,255,255,.88);
}

.h1 {
  font-size: var(--fs-display);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.03em;
  margin-bottom: var(--s-5);
  text-wrap: balance;
}
/* The second sentence is the promise; the emphasis makes it the thing you
   read if you read nothing else. Not italic -- italic at display size in a
   geometric sans reads as a mistake. */
.h1 em { font-style: normal; color: var(--grn-l); }

.hs {
  font-size: 1.0625rem;
  line-height: 1.6;
  color: rgba(255,255,255,.78);
  max-width: 46ch;
  margin-bottom: var(--s-8);
}

.hbt { gap: var(--s-3); margin-bottom: var(--s-6); }
.hbt .btn { padding-inline: var(--s-6); }

/* ─── product cards ───
   Border first, no tinted shadow at rest. A grid of these should read as a
   catalogue you can scan down, not as a set of floating objects. */
.pc {
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: none;
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.pc:hover {
  border-color: var(--line-strong);
  box-shadow: var(--e-2);
  transform: translateY(-2px);
}

/* ─── page hero on the generated pages ───
   shop, faqs, blog, contact. These were a heading on a tinted band; they now
   get the same rhythm and measure as everything else. */
.page-hero { padding: var(--s-12) 0 var(--s-10); }
@media (min-width: 768px) { .page-hero { padding: var(--s-16) 0 var(--s-12); } }
.page-hero .st { font-size: var(--fs-h1); margin-bottom: var(--s-4); }
.page-hero .ss {
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--ink-muted);
  max-width: var(--measure);
}
.page-hero .ey {
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--gd);
}

/* ─── section headings ───
   .st/.ss are used as section titles across the generated pages. */
.sec .st { font-size: var(--fs-h2); margin-bottom: var(--s-3); }
.sec .ss { font-size: var(--fs-sm); color: var(--ink-muted); line-height: 1.6; }

/* ─── navigation ───
   Compact, quiet, and out of the way. The nav is not the product. */
.nb {
  /* 14px, matching the original. 15px looked better in isolation and pushed
     the nav onto three rows at 1440px -- eight items plus a search, a cart and
     a CTA leave no slack, so the nav is not the place to spend a pixel. */
  font-size: 0.875rem;
  font-weight: 550;
  color: var(--ink-muted);
  padding: var(--s-2) var(--s-1);
  border-radius: var(--r-sm);
  transition: color .15s ease;
}
.nb:hover { color: var(--ink); }
.nc {
  font-weight: 650;
  font-size: var(--fs-sm);
  border-radius: var(--r-sm);
  min-height: 40px;
  display: inline-flex;
  align-items: center;
}

/* ─── footer ───
   Structured and quiet. The disclaimer stays present but visually secondary,
   which is what it should be: findable, not shouted. */
.fl {
  font-size: var(--fs-sm);
  line-height: 2;
  color: rgba(255,255,255,.72);
  transition: color .15s ease;
}
.fl:hover { color: #fff; }
.ftit {
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  opacity: .62;
  margin-bottom: var(--s-2);
}
.fbot { font-size: var(--fs-xs); line-height: 1.7; opacity: .62; }

/* ─── phone ───
   Everything above is mobile-first, so this is only what genuinely needs to
   change when there is more room, plus the few things that need less. */
@media (max-width: 640px) {
  .hero { padding-top: var(--s-12); padding-bottom: var(--s-16); }
  .hbt { flex-direction: column; align-items: stretch; }
  .hbt .btn { width: 100%; }
  .hs { margin-bottom: var(--s-6); }
  /* A hero image below a headline on a phone pushes the CTA off screen for
     no benefit -- the words are what sell here. */
  .hv { display: none; }
}

/* ─── clinical disclaimer ───
   On every page, findable, and deliberately quiet. It is a safety statement,
   not a selling point: it should never compete with the copy around it, and
   it should never be missing. */
.pl-disclaimer {
  max-width: var(--measure);
  margin: 0 auto var(--s-3);
  font-size: var(--fs-xs);
  line-height: 1.6;
  opacity: .72;
}

/* Band the positioning strip sits in: a light surface directly under the dark
   hero, which gives the page somewhere to breathe before the first section
   and stops two dark blocks running together. */
.pl-trust-band { background: var(--surface); }
.pl-trust-band .pl-trust { border-top: none; }

/* ═══════════════════════════════════════════════════════════════════════
   SHOP
   ═══════════════════════════════════════════════════════════════════════ */

/* Visually-hidden. index.html has its own copy inline; the generated pages
   had none, so the new filter labels had nothing to hide them with. */
.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ─── controls ───
   Search and sort on one row, filters on the next. On a phone they stack and
   the filter row scrolls sideways rather than wrapping into three lines of
   pills, which pushed the first product below the fold. */
.shop-controls {
  display: flex;
  gap: var(--s-3);
  align-items: center;
  margin-top: var(--s-6);
}
.shop-search { flex: 1; min-width: 0; }
.shop-search input,
.shop-sort select {
  width: 100%;
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--ink);
  background: #fff;
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
  padding: 11px var(--s-4);
  transition: border-color .15s, box-shadow .15s;
}
.shop-search input::placeholder { color: var(--ink-quiet); }
.shop-search input:focus,
.shop-sort select:focus {
  outline: none;
  border-color: var(--g);
  box-shadow: var(--focus);
}
.shop-sort select { min-width: 190px; cursor: pointer; }

.shop-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

/* Category pills. index.html styles .cpi inline and marks the chosen one
   .active; the shop marks it .on. Both are honoured so one look covers both
   pages and neither has to be rewritten. */
.cpi {
  padding: 8px var(--s-4);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  border: 1.5px solid var(--line);
  background: #fff;
  color: var(--ink-muted);
  transition: border-color .15s, color .15s, background .15s;
}
.cpi:hover { border-color: var(--g); color: var(--g); }
.cpi.on, .cpi.active {
  background: var(--g);
  border-color: var(--g);
  color: #fff;
}

/* The running count. It is the honest answer to "did my filter do anything",
   and it is always present -- not only when the result is zero. */
.shop-count {
  margin: var(--s-4) 0 0;
  font-size: var(--fs-xs);
  color: var(--ink-quiet);
}

/* ─── empty state ───
   Only ever reached by filtering cards that are already in the DOM, so it
   cannot appear because a fetch failed. It always offers the way back. */
.shop-empty {
  text-align: center;
  padding: var(--s-16) var(--s-5);
  border: 1.5px dashed var(--line);
  border-radius: var(--r-lg);
  margin-top: var(--s-6);
  background: var(--surface-sunk);
}
.shop-empty-t { font-weight: 700; font-size: var(--fs-h3); margin: 0 0 var(--s-2); }
.shop-empty-s { margin: 0; color: var(--ink-muted); font-size: var(--fs-sm); }

/* ─── cards ───
   One card says, in this order: what it looks like, what kind of thing it is,
   what it is called, what it does for you, what it costs, and one action. */
.shop-grid {
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
  gap: var(--s-5);
}
.shop-card {
  border: 1.5px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--e-1);
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.shop-card:hover {
  border-color: var(--g);
  box-shadow: var(--e-2);
  transform: translateY(-2px);
}
.shop-card[hidden] { display: none; }
.shop-card img { height: 180px; }

/* A placeholder for the products that have no image yet, so a card without
   one is a deliberate-looking block rather than a card of a different height
   to everything beside it. */
.shop-card-ph {
  height: 180px;
  background: linear-gradient(135deg, var(--surface-quiet), var(--surface-sunk));
  border-bottom: 1.5px solid var(--line);
}

.shop-card-body { padding: var(--s-5); gap: 0; }

/* Type line: "Guide", "Template", and the format when it is not a download. */
.shop-card-type {
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin-bottom: var(--s-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.shop-card-format {
  color: var(--gd);
  background: var(--surface-brand);
  border-radius: var(--r-pill);
  padding: 2px 8px;
  letter-spacing: .04em;
}

.shop-card-title {
  font-size: var(--fs-h3);
  font-weight: 800;
  line-height: var(--lh-snug);
  margin-bottom: var(--s-2);
}
.shop-card-sub {
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--ink-muted);
  margin-bottom: var(--s-4);
}
.shop-card-price {
  font-size: var(--fs-h3);
  font-weight: 800;
  color: var(--ink);
  margin: auto 0 var(--s-4);
}
/* Free is a price, and the strongest reason to click. Say it in the brand
   colour rather than leaving it looking like a missing value. */
.shop-card-price.is-free { color: var(--gd); }

/* Out of stock and coming soon. The card stays legible -- it is still a real
   product someone may want to be told about -- but it stops competing. */
.shop-card.is-soon { opacity: .85; }
.shop-card-badge {
  position: absolute;
  top: var(--s-3);
  left: var(--s-3);
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: rgba(20, 30, 22, .82);
  color: #fff;
  backdrop-filter: blur(2px);
}
.shop-card { position: relative; }

@media (max-width: 640px) {
  .shop-controls { flex-direction: column; align-items: stretch; gap: var(--s-2); }
  .shop-sort select { min-width: 0; }
  /* One row that scrolls, rather than three rows that push the shop down. */
  .shop-filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: var(--s-1);
  }
  .shop-filters::-webkit-scrollbar { display: none; }
  .cpi { flex: 0 0 auto; }
  .shop-grid { grid-template-columns: 1fr; }
  .shop-card img, .shop-card-ph { height: 150px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   PRODUCT DETAIL
   ═══════════════════════════════════════════════════════════════════════ */

/* The page used to be a cover image beside a buy panel, and then everything
   else in a 640px column down the left with the whole right half of the
   screen empty. Now the two columns run the full length of the page: content
   on the left, the buy panel on the right, pinned so price and the action are
   in view wherever you have read to. */
.product-grid {
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: var(--s-12);
  align-items: start;
}
.product-main { min-width: 0; }

.product-buy { position: relative; }
@media (min-width: 801px) {
  .product-buy-in {
    position: sticky;
    top: var(--s-6);
    border: 1.5px solid var(--line);
    border-radius: var(--r-lg);
    padding: var(--s-6);
    background: #fff;
    box-shadow: var(--e-2);
  }
}
/* On a phone the buy panel comes first -- you should not have to scroll past
   the cover and five sections to find out what it costs. */
@media (max-width: 800px) {
  .product-buy { order: -1; }
  .product-main { order: 1; }
  .product-grid { display: flex; flex-direction: column; gap: var(--s-8); }
}

.product-detail-img { margin-bottom: var(--s-10); }
/* A cover that has not loaded, or a product without one, should hold its
   place rather than collapsing the column around it. */
.product-detail-img img { display: block; aspect-ratio: 4 / 3; object-fit: cover; }

.product-detail-title { font-size: var(--fs-h1); margin-bottom: var(--s-2); }
.product-detail-price { font-size: var(--fs-h2); color: var(--ink); margin-bottom: var(--s-4); }
.product-detail-desc { font-size: var(--fs-sm); margin-bottom: var(--s-5); }

/* Two facts, stated once. Previously the same thing was said three times on
   one page -- a trust line, a pill and a Format section -- in three slightly
   different wordings, which reads as padding rather than reassurance. */
.pd-facts {
  list-style: none;
  margin: var(--s-4) 0 0;
  padding: 0;
  display: grid;
  gap: var(--s-2);
  font-size: var(--fs-xs);
  color: var(--ink-muted);
}
.pd-facts li { padding-left: var(--s-5); position: relative; }
.pd-facts li::before {
  content: "";
  position: absolute;
  left: 0; top: .48em;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--g);
}

/* ─── content blocks ─── */
.pd-block { margin-bottom: var(--s-12); max-width: var(--measure); }
.pd-h {
  font-size: var(--fs-h3);
  font-weight: 800;
  margin: 0 0 var(--s-4);
  padding-bottom: var(--s-3);
  border-bottom: 1.5px solid var(--line);
}
.pd-p { font-size: var(--fs-sm); color: var(--ink-muted); margin: 0 0 var(--s-3); }
.pd-p:last-child { margin-bottom: 0; }

.product-detail-benefits { gap: var(--s-3); margin-bottom: 0; }
.product-detail-benefit { font-size: var(--fs-sm); }

/* ─── per-product FAQs ───
   These were written, wired into the page's FAQPage structured data, and
   then never rendered: the page told Google it answered questions it did not
   actually show. They are on the page now, which is both the honest thing
   and what the structured data has been claiming all along. */
.pd-faq { padding: var(--s-4) 0; border-bottom: 1px solid var(--line); }
.pd-faq:last-child { border-bottom: none; padding-bottom: 0; }
.pd-faq-q { font-weight: 700; font-size: var(--fs-sm); margin-bottom: var(--s-1); }
.pd-faq-a { color: var(--ink-muted); font-size: var(--fs-xs); line-height: 1.7; }

/* ─── before / after ─── */
.pd-compare { display: grid; gap: var(--s-4); }
.pd-compare-bad, .pd-compare-good {
  border-radius: var(--r-lg);
  padding: var(--s-5);
  border: 1.5px solid;
}
.pd-compare-bad { background: #FFF5F5; border-color: #F0C4C4; }
.pd-compare-good { background: var(--surface-brand); border-color: var(--gm, var(--line)); }
.pd-compare-l {
  font-size: var(--fs-label);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .06em;
  margin-bottom: var(--s-2);
}
.pd-compare-bad .pd-compare-l { color: #A62828; }
.pd-compare-good .pd-compare-l { color: var(--gd); }
.pd-compare-t { font-size: var(--fs-sm); line-height: 1.7; }
.pd-compare-bad .pd-compare-t { color: var(--ink-muted); font-style: italic; }
.pd-compare-good .pd-compare-t { color: var(--ink); }

/* ─── the safety note ───
   Quiet, but never buried. It is the last thing in the column so it reads as
   a closing statement rather than an interruption. */
.pd-note {
  display: block;
  max-width: var(--measure);
  border: 1.5px solid var(--line);
  border-left: 4px solid var(--g);
  border-radius: var(--r-sm);
  padding: var(--s-5);
  background: var(--surface-sunk);
}
.pd-note-h { font-size: var(--fs-sm); font-weight: 800; margin: 0 0 var(--s-2); }
.pd-note-p { font-size: var(--fs-xs); color: var(--ink-muted); line-height: 1.7; margin: 0; }
.pd-note-meta { font-size: var(--fs-label); color: var(--ink-quiet); margin: var(--s-3) 0 0; }

/* ─── related ─── */
.related-title { font-size: var(--fs-h2); margin: var(--s-16) 0 var(--s-5); }
.rcard { border-radius: var(--r-lg); box-shadow: var(--e-1); transition: border-color .15s, box-shadow .15s, transform .15s; }
.rcard:hover { box-shadow: var(--e-2); transform: translateY(-2px); }
.rcard-title { font-size: var(--fs-sm); }
.rcard-price { font-size: var(--fs-sm); }

/* ─── preview card ─── */
.preview-card { border-radius: var(--r-lg); box-shadow: var(--e-1); padding: var(--s-5); }

/* ═══════════════════════════════════════════════════════════════════════
   HOMEPAGE: THE VALUE SECTION AND THE CLOSE
   ═══════════════════════════════════════════════════════════════════════ */

/* The hero's third action is a real <button> (it opens a product view in the
   SPA) wearing the quiet link styling, so the button chrome has to go. */
button.hbt-link {
  background: none;
  border: none;
  border-bottom: 1px solid transparent;
  font-family: inherit;
  cursor: pointer;
}

/* ─── framework list ───
   The replacement for six pills and four cards. Subject on the left, one
   sentence on the right: the eye takes the five subjects in a single pass and
   only reads the sentence for the one it stopped on. Cards would have made
   five equal blocks with no such reading order, which is what the two
   sections it replaces both did. */
.fw-list {
  list-style: none;
  margin: 0 auto;
  padding: 0;
  max-width: 880px;
  border-top: 1px solid var(--line);
}
.fw-item {
  display: grid;
  grid-template-columns: minmax(180px, 15rem) 1fr;
  gap: var(--s-4) var(--s-8);
  align-items: baseline;
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--line);
}
.fw-t {
  font-family: var(--font-heading), sans-serif;
  font-size: var(--fs-h3);
  font-weight: 800;
  line-height: var(--lh-snug);
}
.fw-d {
  font-size: var(--fs-sm);
  color: var(--ink-muted);
  line-height: var(--lh-body);
}
.fw-list-sm .fw-t { font-size: var(--fs-body); }
.fw-list-sm .fw-item { padding: var(--s-4) 0; }

.fw-note {
  max-width: 880px;
  margin: var(--s-6) auto 0;
  font-size: var(--fs-xs);
  color: var(--ink-quiet);
  line-height: 1.7;
}

@media (max-width: 700px) {
  .fw-item { grid-template-columns: 1fr; gap: var(--s-1); padding: var(--s-4) 0; }
}

/* ─── closing CTA ───
   The page used to end on a form. This offers the two things someone came
   for one last time, and the newsletter follows it. */
.pl-close { padding-bottom: var(--s-10); }
.pl-close-in {
  text-align: center;
  max-width: 720px;
  margin: 0 auto;
  padding: var(--s-16) var(--s-6);
  border: 1.5px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-brand);
}
.pl-close-h { font-size: var(--fs-h1); line-height: var(--lh-tight); margin: 0 0 var(--s-4); }
.pl-close-p {
  font-size: var(--fs-sm);
  color: var(--ink-muted);
  max-width: var(--measure-sm);
  margin: 0 auto var(--s-8);
}
.pl-close-bt { justify-content: center; margin-bottom: 0; }

@media (max-width: 640px) {
  .pl-close-in { padding: var(--s-10) var(--s-5); }
  .pl-close-bt .btn { width: 100%; }
}

/* ═══════════════════════════════════════════════════════════════════════
   TOUCH TARGETS
   ═══════════════════════════════════════════════════════════════════════ */

/* Measured across every route at 390px and 430px: the footer's link columns
   rendered at 30px tall and the header's search and cart at 31px. That is
   under the 44px a thumb wants and under the 24px minimum WCAG 2.2 asks for
   once spacing is taken into account -- and this is a site whose whole point
   is being used on a phone.
   
   Padding rather than a min-height on the anchor itself, so the hit area
   grows around the text instead of the text moving inside a taller box. The
   negative margin keeps the column's visual rhythm where it was: the links
   look the same, they are just easier to hit. */
@media (max-width: 860px) {
  .fl {
    padding: 7px 0;
    margin-bottom: 0;
  }
  .ftit + .fl { margin-top: -7px; }
}

/* The header icons are already square; they just need room. */
.cart-btn {
  padding: 11px 10px;
  min-width: 44px;
  justify-content: center;
}

/* ─── the whole product card is the tap target ───
   The title was a 21px-tall link in the middle of a 400px card. On a phone
   that is a thing to aim at rather than a thing to tap. The title's link now
   stretches over the card, so anywhere that is not the button opens the
   product; the button sits above it and still adds to the cart.

   Done with a pseudo-element rather than by wrapping the card in an <a>,
   because a card containing a button cannot be nested inside a link. */
.shop-card-title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
/* Both need to sit above the title's stretched link so they stay clickable
   and visible -- but only the actions row wants a new positioning context.
   Giving the badge `position: relative` here overrode the `absolute` it is
   laid out with further up this file, and it dropped out of the image's
   top-left corner into the flow as a full-width bar under the picture. */
.shop-card-actions { position: relative; z-index: 2; }
.shop-card-badge { z-index: 2; }
/* The hover state belongs to the card now, not just the four words in it. */
.shop-card:hover .shop-card-title a { color: var(--gd); }

/* ─── breadcrumbs and the footer email ───
   Both were real links rendering at 15px and 21px tall. */
.crumbs a { display: inline-block; padding: 6px 0; }
/* The footer's contact address carries its own inline styles, so this has
   to be specific enough to add padding without fighting them. */
footer a[data-email-link] { padding: 6px 0; }

/* ═══════════════════════════════════════════════════════════════════════
   STUDENT HUB
   ═══════════════════════════════════════════════════════════════════════ */

/* The hub is a set of tools, not an article, and it was laid out as one:
   a single 1100px column with each tool stacked under the last, so a tool
   description ran to 1100 characters a line on a laptop and the third tool
   sat two screens down from the first.

   A grid instead. Tools sit side by side once there is room, which is what
   makes a set of tools read as a set rather than a queue, and every tool is
   visible at once on a laptop instead of being scrolled to. The panels, the
   tabs and the tools themselves are untouched -- this is layout only, and
   every control still belongs to the same script it always did. */
@media (min-width: 900px) {
  .sh-panel {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-5);
    align-items: start;
  }
  /* The section heading introduces the whole panel, so it spans it. */
  .sh-panel > .sh-section-title { grid-column: 1 / -1; }
  .sh-tool { margin-bottom: 0; }
  /* A tool that has produced output needs the room to show it. */
  .sh-tool:has(.sh-out:not(:empty)) { grid-column: 1 / -1; }
}

.sh-tool {
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--e-1);
  transition: border-color .15s, box-shadow .15s;
}
.sh-tool:focus-within { border-color: var(--g); box-shadow: var(--e-2); }
.sh-tool > h3 { font-size: var(--fs-h3); }
/* Measure applies to the prose even inside a wide card. */
.sh-tool > p.sh-tool-desc { font-size: var(--fs-sm); max-width: var(--measure); }
.sh-section-title { font-size: var(--fs-label); }

/* Two fields fit side by side well before the card is wide enough for four. */
@media (min-width: 560px) {
  .sh-fields { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
}

/* The hub's own header is the page's title block, so it gets the same
   measure as everything else rather than one long line. */
.sh-head-in p, .sh-account-note { max-width: var(--measure); }

/* A planned price, on a product that cannot be bought yet. Sits under the
   number rather than beside it, so the price still reads as a price and the
   qualifier reads as a caveat. */
.price-note {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--ink-quiet);
  /* Pulled up under the price, which carries its own bottom margin. */
  margin: calc(var(--s-4) * -1 + var(--s-1)) 0 var(--s-4);
}

/* ═══════════════════════════════════════════════════════════════════════
   POLISH PASS
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── measure, applied to the text rather than its container ───
   --measure is 64ch, and ch is relative to the element's OWN font size. Put
   it on a container at 16px and it caps at 512px; a 12px caption inside that
   container then gets 85 characters a line, not 64. Every long line measured
   in this pass came from exactly that: a small-font paragraph inheriting a
   width meant for body text.

   Measured at 1440px before: the Student Hub's disclaimer ran to 184
   characters, the homepage's framework note to 135, the quiz intro to 113,
   the product page's safety note to 90. The cap belongs on each of them. */
.fw-note,
.fw-list,
.quiz-inner p,
.quiz-sub,
.sh-disclaimer,
.sh-head-in p,
.legal-body p,
.legal-body li,
.pd-note-p,
.pd-p,
.pd-faq-a,
.shop-empty-s {
  max-width: var(--measure);
}
/* Centred blocks keep their centring; the others stay left-aligned. */
.fw-note, .fw-list { margin-left: auto; margin-right: auto; }
.quiz-inner p, .quiz-sub { margin-left: auto; margin-right: auto; }

/* ─── heading scale ───
   The Student Hub's section label is an <h2> set at 13px, sitting above tool
   titles that are <h3> at 16px: the document outline and the visual scale
   disagreed, and an h2 rendered smaller than the body text under it reads as
   a caption rather than a heading. It keeps its eyebrow treatment -- that is
   the right look for it -- but it is no longer smaller than what it
   introduces. */
.sh-section-title { font-size: var(--fs-label); }
.sh-tool > h3 { font-size: var(--fs-h3); }

/* The legal and About pages were the one place headings did not scale with
   the viewport: h1 stayed at 28px from a phone to a 1440px screen while
   every other page's grew from 29 to 42. Same fluid scale as everywhere
   else, so the site reads as one site. */
.legal-body h3 { font-size: var(--fs-h3); margin-top: var(--s-8); }

/* ─── product imagery: one crop at every width ───
   A fixed pixel height with a fluid width means the visible slice of the
   picture changes with the viewport -- measured between 1.69:1 and 2.59:1
   across breakpoints on the same card, which is why the artwork looked
   cropped differently on a phone and a laptop. An aspect ratio holds the
   composition still and lets the height follow the column. */
.shop-card img,
.shop-card-ph {
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
.rcard img { aspect-ratio: 3 / 2; height: auto; object-fit: cover; }

/* ─── even card heights ───
   Product blurbs run one to three lines, so cards in the same row ended up
   as much as 85px apart and the row's prices and buttons stopped lining up.
   Reserving three lines evens the row without truncating anything that fits;
   anything longer is clamped, and the full text is on the product page --
   which is where the brief wants detail to live anyway. */
.shop-card-sub {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  min-height: calc(3 * 1.6em);
}
/* The type line wraps to two rows on some cards and one on others. Reserve
   the taller of the two so the title below it starts at the same height. */
.shop-card-type { min-height: calc(var(--fs-label) * 1.45); }

/* ═══════════════════════════════════════════════════════════════════════
   BLOCKS FOR PAGES BUILT IN THE LIVE EDITOR

   scripts/page_blocks.py renders a page from a list of blocks, and almost
   every one of them is markup this stylesheet already covers: .page-hero,
   .sec, .con, .pl-card, .shop-grid, .btn. Four arrangements are not, and
   they are here rather than in the renderer, so a page built last month
   picks up a change to how a split block looks without being rebuilt from
   different markup.
   ═══════════════════════════════════════════════════════════════════════ */

/* Body copy in a block. The rich-text field can produce headings, lists and
   links, none of which a bare .sec styles, and all of which read badly at
   full container width. */
.pl-prose { max-width: var(--measure); }
.pl-prose > * + * { margin-top: var(--s-4); }
.pl-prose h2, .pl-prose h3 { margin-top: var(--s-8); }
.pl-prose ul, .pl-prose ol { padding-left: 1.25em; }
.pl-prose li + li { margin-top: var(--s-2); }
.pl-prose a { color: var(--gd); text-decoration: underline; text-underline-offset: 2px; }

.pl-block-cta { margin-top: var(--s-6); }

/* Picture beside words. One column on a phone, with the picture first
   whichever side it is set to -- a caption-style paragraph above an image
   it refers to reads as an orphan. */
.pl-split { display: grid; gap: var(--s-8); align-items: center; }
.pl-split-img img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r);
  border: 1px solid var(--line);
}
@media (min-width: 768px) {
  .pl-split { grid-template-columns: 1fr 1fr; gap: var(--s-10); }
  /* Source order is picture-then-words for a left picture and the reverse
     for a right one, so the markup already reads correctly and nothing
     needs reordering here. */
}

/* Questions. <details> so it works with scripts blocked, which is the whole
   reason the generated pages exist. */
.pl-faq {
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-4) var(--s-5);
  background: var(--surface);
}
.pl-faq + .pl-faq { margin-top: var(--s-3); }
.pl-faq summary {
  cursor: pointer;
  font-weight: 700;
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: 44px;
  align-items: center;
}
.pl-faq summary::-webkit-details-marker { display: none; }
.pl-faq summary::after {
  content: "+";
  font-weight: 400;
  font-size: 1.4em;
  line-height: 1;
  color: var(--ink-muted);
  flex-shrink: 0;
}
.pl-faq[open] summary::after { content: "\2212"; }
.pl-faq-a { margin-top: var(--s-3); color: var(--ink-muted); }

/* A quote. Reuses .pl-card for the frame so it sits level with a feature
   card in the same grid. */
.pl-quote { margin: 0; display: flex; flex-direction: column; gap: var(--s-4); }
.pl-quote blockquote { margin: 0; font-size: 1.0625rem; line-height: 1.6; }
.pl-quote blockquote::before { content: "\201C"; }
.pl-quote blockquote::after { content: "\201D"; }
.pl-quote figcaption { font-weight: 700; font-size: var(--fs-sm); margin-top: auto; }
.pl-quote-role {
  display: block;
  font-weight: 500;
  color: var(--ink-muted);
  font-size: var(--fs-label);
}

/* The closing band. Centred, on the tinted background, so it reads as the
   end of the page rather than another section of it. */
.pl-band {
  background: var(--surface-brand);
  border-radius: var(--r);
  padding: var(--s-10) var(--s-6);
  text-align: center;
}
.pl-band .ss { margin-left: auto; margin-right: auto; }

/* ═══════════════════════════════════════════════════════════════════════
   THE REVIEW FORM  (/review)

   Reached from the link in the post-purchase email, by somebody who has
   already bought something and is doing us a favour. So: one thing per
   screen, nothing to dismiss, and targets a thumb can hit — most of these
   are opened on a phone, on a break.
   ═══════════════════════════════════════════════════════════════════════ */

.review-card {
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-6);
  background: var(--surface);
  max-width: var(--measure-sm);
}
.review-card + .review-card { margin-top: var(--s-6); }
.review-card-title { font-size: 1.125rem; font-weight: 800; margin: 0 0 var(--s-4); }

/* Real radios, visually replaced. They stay in the accessibility tree and
   keep keyboard and screen-reader behaviour, which a row of clickable spans
   would not — and a rating nobody can give with a keyboard is a rating a
   chunk of people simply will not leave. */
.review-stars { border: 0; padding: 0; margin: 0 0 var(--s-5); display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
.review-stars legend { font-size: var(--fs-sm); font-weight: 700; margin-bottom: var(--s-2); }
.review-stars input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.review-stars label {
  font-size: 30px;
  line-height: 1;
  color: var(--line-strong, #d8d5cc);
  cursor: pointer;
  padding: 6px 4px;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.review-stars label.on { color: #f5a623; }
.review-stars input:focus-visible + label { outline: 2px solid var(--gd); outline-offset: 2px; border-radius: 6px; }

.review-label { display: block; font-size: var(--fs-sm); font-weight: 700; margin-bottom: var(--s-2); }
.review-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  padding: 12px;
  min-height: 48px;
  border: 1.5px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  margin-bottom: var(--s-4);
}
.review-input:focus-visible { outline: 2px solid var(--gd); outline-offset: 1px; }
textarea.review-input { min-height: 110px; resize: vertical; }

.review-inline-msg { font-size: var(--fs-sm); margin-top: var(--s-3); min-height: 1.2em; color: var(--ink-muted); }
.review-inline-msg.bad { color: #8f2d2d; font-weight: 700; }

.review-done { font-size: var(--fs-sm); color: var(--ink-muted); margin: 0; }

.review-msg { max-width: var(--measure-sm); }
.review-msg p { margin: 0 0 var(--s-5); }
.review-msg.bad p { font-weight: 700; }

.review-note { font-size: var(--fs-label); color: var(--ink-muted); margin-top: var(--s-8); max-width: var(--measure-sm); }
