/* ==========================================================================
   3andB DESIGN SYSTEM  ·  v3  ·  "Teacher's Desk" in brand colors
   --------------------------------------------------------------------------
   Palette drawn straight from the logo: bright green, electric blue,
   magenta-purple, deep navy. Brighter + happier than v2; navy replaces brown.
   Site-wide: loaded from base.html; every :root token below is available to
   any page/component, public or admin.
   ========================================================================== */

/* ========================================================================
   1 · TOKENS (from the logo)
   ======================================================================== */
:root{
  /* Bright material */
  --paper:      #F6F8FD;   /* cool bright white : background */
  --paper-card: #FFFFFF;   /* clean sheet */
  --paper-2:    #EAF0FB;   /* light cool band */
  --ink:        #0C1478;   /* deep navy (logo triangle) : dark surfaces */
  --ink-2:      #141E92;   /* raised navy */
  --text:       #16205E;   /* navy ink : body text on light */
  --text-soft:  #5B658C;   /* muted slate-blue */
  --line:       #DBE3F3;   /* cool hairline */

  /* Brand markers */
  --green:      #12C93C;   /* tuned logo green (fills, buttons) */
  --green-hi:   #00E637;   /* pop green (hand-drawn circle) */
  --green-ink:  #063C13;   /* text on green */
  --green-d:    #0A9E2F;   /* pressed/shadow green */
  --blue:       #2440F0;   /* electric blue */
  --purple:     #A814EE;   /* magenta-purple */
  --cyan:       #23A6DE;   /* gradient cyan */
  --red:        #F5533D;   /* alerts, errors */
  --amber:      #C4700A;   /* deliberate, temporary states — maintenance mode.
                              Darker than the orange in the subject palette on
                              purpose: this one is read as text on a tint, so it
                              has to hold contrast rather than just catch an eye. */

  /* Subject coding (DB: subjects.slug) — bright + happy. storefront.ACCENTS
     says which subject wears which; every subject in the taxonomy has one, and
     anything added later falls back to --ink rather than borrowing a colour. */
  --s-science:    #12B83C;
  --s-ela:        #2440F0;
  --s-social:     #17A6C9;
  --s-math:       #E8890C;
  --s-stem:       #A814EE;   /* technology */
  --s-psych:      #E33FA0;
  --s-lifeskills: #0E9E9E;
  --s-cte:        #F5533D;   /* career readiness, and CTE pathways */
  --s-econ:       #8C5A2B;   /* economics & personal finance */
  --s-life:       #E33FA0;

  /* Type */
  --display:"Bricolage Grotesque",Georgia,serif;
  --body:"Hanken Grotesk",system-ui,sans-serif;
  --hand:"Shantell Sans","Comic Sans MS",cursive;

  --wrap:1180px; --gut:clamp(20px,5vw,56px);
  --r:10px; --r-lg:16px;
  --shadow:0 2px 0 rgba(12,20,120,.04),0 14px 30px -20px rgba(12,20,120,.35);
  --shadow-h:0 2px 0 rgba(12,20,120,.05),0 24px 46px -20px rgba(12,20,120,.45);
}

/* ========================================================================
   2 · RESET + BASE
   ======================================================================== */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* The browser's own [hidden] rule is a bare element selector, so any class that
   sets display — .card, .chipset, .btn — silently outranks it and the element
   stays on screen. The finder toggles visibility with the attribute on exactly
   those elements, so it has to win here. */
[hidden]{display:none!important}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{font-family:var(--body);color:var(--text);background:var(--paper);
  line-height:1.55;font-size:clamp(16px,1.05vw,17px);-webkit-font-smoothing:antialiased}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
button{font:inherit;cursor:pointer;border:none;background:none;color:inherit}
:focus-visible{outline:3px solid var(--green);outline-offset:3px;border-radius:3px}
.wrap{max-width:var(--wrap);margin-inline:auto;padding-inline:var(--gut)}

/* ========================================================================
   3 · TYPE
   ======================================================================== */
h1,h2,h3{font-family:var(--display);font-weight:800;line-height:1.02;letter-spacing:-.02em}
.display{font-family:var(--display);font-weight:800;font-size:clamp(2.8rem,7.5vw,6rem);line-height:.94;letter-spacing:-.035em}
.h2{font-family:var(--display);font-weight:800;font-size:clamp(1.8rem,3.6vw,2.9rem);letter-spacing:-.025em}
.eyebrow{font-family:var(--body);font-weight:700;font-size:.74rem;letter-spacing:.16em;text-transform:uppercase;color:var(--text-soft)}
.hand{font-family:var(--hand);font-weight:500}
.tape{display:inline-block;font-family:var(--body);font-weight:700;font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--green-ink);background:var(--green);padding:.3em .6em;border-radius:3px;box-shadow:1px 1px 0 rgba(12,20,120,.16)}

/* ========================================================================
   4 · BUTTONS
   ======================================================================== */
.btn{display:inline-flex;align-items:center;gap:.55em;font-weight:700;font-size:.95rem;padding:.82em 1.4em;border-radius:99px;
  transition:transform .16s,box-shadow .16s,background .16s,color .16s}
.btn:active{transform:translateY(1px)}
.btn--primary{background:var(--green);color:var(--green-ink);box-shadow:0 3px 0 var(--green-d)}
.btn--primary:hover{transform:translateY(-1px);box-shadow:0 5px 0 var(--green-d)}
.btn--primary:active{transform:translateY(2px);box-shadow:0 1px 0 var(--green-d)}
.btn--ink{background:var(--ink);color:#fff}
.btn--ink:hover{background:var(--ink-2)}
.btn--outline{border:2px solid var(--ink);color:var(--ink)}
.btn--outline:hover{background:var(--ink);color:#fff}
.btn--sm{padding:.55em 1em;font-size:.86rem}
/* The product page's primary action. Bigger than a nav button because it is the
   only thing on that page anyone is asked to press, and a call to action the
   same size as "About" is not a call to action. */
.btn--lg{padding:1em 1.6em;font-size:1rem}

/* --- the buy buttons ----------------------------------------------------
   These were btn--primary and btn--outline: bright green with dark green text,
   and a transparent button with a navy hairline. Both were hard to read, for
   different reasons.

   The green one measured 5.68:1, which passes AA — so this was NOT a contrast
   failure, and it is worth being precise about that because the fix would have
   been the wrong one. #063C13 on #12C93C is dark green type on bright green:
   the luminance gap is fine and the hue gap is nothing, and type that separates
   only by lightness within one hue is type the eye has to work at. On a product
   page it was also sitting inside a hero washed with the subject accent, which
   for every science title is green as well — so a green button on a green tint,
   in a page whose whole colour system is that accent.

   The outline one was the opposite problem: 14.25:1 text, and no fill at all,
   so on that same tinted wash it read as an empty rectangle rather than as a
   thing to press. Legible, invisible.

   So: navy fill, white text, 15.14:1. Navy is the one colour on this site that
   is constant across all nine subject accents, so this reads identically loudly
   whether the page around it is green, magenta or orange, and the hue gap
   against every one of those tints is enormous. Green does not leave, it moves:
   the mark inside is --green-hi at 8.92:1 on navy, the same white-text-with-a-
   green-mark treatment the chat launcher already uses.

   ONE style, for both. The first pass gave TPT the solid button and Amazon a
   quiet outlined one, which was wrong: a styling difference in a row of two is
   read as a recommendation, and this site is not recommending a store. They are
   different products — the PDF and the printed book — sold by whoever sells
   that format, and a teacher who wants paper is not taking second best. So the
   only thing separating them is the store's own mark (see _icons.html), which
   distinguishes without ranking. Anything added here later has to keep that
   property: no hover that fires on one and not the other, no order-dependent
   emphasis, nothing that makes one of them the answer.

   Squarer than the site's pills, deliberately. Everything else on a product
   page — chips, facts, the look-inside pill, the nav — is a 99px pill, so a
   12px radius is what makes these read as the things that are actually buttons
   rather than as two more pieces of furniture. */
.btn--buy{background:var(--ink);color:#fff;border-radius:12px;letter-spacing:-.005em;gap:.6em;
  box-shadow:0 1px 2px rgba(12,20,120,.24),0 10px 24px -12px rgba(12,20,120,.75)}
.btn--buy:hover{background:var(--ink-2);transform:translateY(-2px);
  box-shadow:0 2px 5px rgba(12,20,120,.26),0 16px 30px -14px rgba(12,20,120,.8)}
.btn--buy:active{transform:translateY(0)}
.btn--buy .ico{color:var(--green-hi)}

/* The arrow is a leaves-this-site signal, not a second call to action, so it
   sits back until the button is under the cursor. */
.btn--buy .arw{opacity:.55;transition:transform .16s,opacity .16s}
.btn--buy:hover .arw{opacity:1}

/* --- on a dark band -----------------------------------------------------
   The closing call to action sits on a navy gradient, where the hero's navy
   button would disappear. So it inverts: white with navy type, the strongest
   thing that can be drawn on navy, keeping the 12px corner and the green mark
   of its twin at the top of the page. Both buttons take it, for the same reason
   both take btn--buy up there. */
.btn--onink{background:#fff;color:var(--ink);border-radius:12px;letter-spacing:-.005em;gap:.6em;
  box-shadow:0 2px 6px rgba(0,0,0,.22),0 14px 28px -14px rgba(0,0,0,.55)}
.btn--onink:hover{transform:translateY(-2px);
  box-shadow:0 3px 8px rgba(0,0,0,.24),0 20px 34px -16px rgba(0,0,0,.6)}
.btn--onink:active{transform:translateY(0)}
.btn--onink .ico{color:var(--green-d)}
.btn--onink .arw{opacity:.6;transition:transform .16s,opacity .16s}
.btn--onink:hover .arw{opacity:1}
.btn .arw{transition:transform .16s}
.btn:hover .arw{transform:translateX(3px)}
/* Inline marks never shrink to fit their label — a squashed icon looks broken
   in a way a wrapped one does not. */
.btn .ico,.ico{flex:none}

/* ========================================================================
   5 · TOP BAR
   ======================================================================== */
.topbar{position:sticky;top:0;z-index:50;background:color-mix(in srgb,var(--paper) 86%,transparent);backdrop-filter:blur(8px);border-bottom:1.5px solid var(--line)}
.topbar__in{display:flex;align-items:center;justify-content:space-between;height:70px;gap:1.5rem}
.brand{display:flex;align-items:center;gap:.6rem}
.brand img{height:44px;width:auto}
.brand__tag{font-family:var(--hand);font-size:.98rem;color:var(--purple);transform:rotate(-3deg);white-space:nowrap}
.nav{display:flex;align-items:center;gap:1.7rem}
.nav a{font-weight:600;font-size:.92rem;color:var(--text);opacity:.78;transition:opacity .15s}
.nav a:hover{opacity:1}
.nav .btn{padding:.6em 1.1em;font-size:.88rem}
.navtoggle{display:none;font-size:1.5rem;color:var(--ink)}
/* logout is a POST form, styled to sit in the nav like any other link */
.nav__form{display:flex}
.nav__logout{font-weight:600;font-size:.92rem;color:var(--text);opacity:.78;transition:opacity .15s}
.nav__logout:hover{opacity:1}

/* ========================================================================
   6 · HERO
   ======================================================================== */
.hero{padding-block:clamp(3rem,7vw,6rem) clamp(2.5rem,5vw,4.5rem)}
.hero__grid{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(2rem,5vw,4rem);align-items:center}
.hero__eye{display:inline-flex;align-items:center;gap:.6rem;margin-bottom:1.4rem}
.hero__eye .dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,var(--green),var(--blue))}
.hero h1{color:var(--ink)}

/* hand-drawn circle around a word */
.circled{position:relative;display:inline-block}
.circled svg{position:absolute;inset:-22% -12% -22% -12%;width:124%;height:144%;overflow:visible;pointer-events:none}
.circled path{fill:none;stroke:var(--green-hi);stroke-width:7;stroke-linecap:round;
  stroke-dasharray:900;stroke-dashoffset:900;animation:draw 1.2s .4s cubic-bezier(.6,0,.2,1) forwards}
@keyframes draw{to{stroke-dashoffset:0}}

/* margin note in handwriting with a little arrow */
.notenote{margin-top:1.5rem;display:flex;align-items:flex-start;gap:.5rem;max-width:38ch}
.notenote .arrow{flex:none;color:var(--purple);font-size:1.4rem;line-height:1;transform:translateY(-2px) rotate(8deg)}
.notenote span{font-family:var(--hand);font-size:1.12rem;color:var(--text);transform:rotate(-1.2deg);line-height:1.4}

.hero__sub{margin-top:1.4rem;color:var(--text-soft);font-size:clamp(1.02rem,1.5vw,1.2rem);max-width:44ch;line-height:1.55}
.hero__cta{margin-top:1.9rem;display:flex;gap:.8rem;flex-wrap:wrap;align-items:center}

/* the "index card" of honest stats — pinned like a note */
.card-stats{background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--shadow);transform:rotate(1.4deg);position:relative;overflow:hidden}
.card-stats::before{content:"";position:absolute;top:-10px;left:50%;transform:translateX(-50%) rotate(-2deg);
  width:120px;height:26px;background:color-mix(in srgb,var(--green) 55%,transparent);
  border-left:1px dashed rgba(255,255,255,.6);border-right:1px dashed rgba(255,255,255,.6)}
.card-stats__hd{padding:1.5rem 1.3rem .4rem;font-family:var(--hand);font-size:1.05rem;color:var(--text-soft)}
.stat{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;padding:.9rem 1.3rem;border-top:1.5px dashed var(--line)}
.stat:first-of-type{border-top:none}
.stat dt{color:var(--text-soft);font-size:.92rem}
.stat dd{font-family:var(--display);font-weight:800;font-size:clamp(1.5rem,2.4vw,2rem);color:var(--ink);display:flex;flex-direction:column;align-items:flex-end}
.stat dd small{font-family:var(--hand);font-weight:500;font-size:.72rem;color:var(--purple)}

/* ========================================================================
   7 · SECTIONS
   ======================================================================== */
.section{padding-block:clamp(3rem,7vw,5.5rem)}
.band-2{background:var(--paper-2)}
.sec-head{display:flex;justify-content:space-between;align-items:flex-end;gap:1.5rem;margin-bottom:2.2rem;flex-wrap:wrap}
/* h1 as well as h2: /samples is a landing page in its own right — the nav
   button, the homepage call to action and the sitemap all aim at it — so its
   statement is the document's heading rather than a section's, and it wears
   the same .h2 type as every other band head on the site. */
.sec-head h2,.sec-head h1{max-width:22ch}
.sec-head p{color:var(--text-soft);max-width:38ch}

/* ========================================================================
   8 · BROWSE — subjects as sticky notes + grade "class list"
   ======================================================================== */
.browse{display:grid;grid-template-columns:2fr 1fr;gap:clamp(1.5rem,3vw,2.5rem);align-items:start}
.subjects{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.note{--c:var(--ink);position:relative;display:block;background:var(--paper-card);border-radius:4px;
  padding:1.15rem 1.1rem 1.25rem;box-shadow:var(--shadow);transition:transform .16s,box-shadow .16s;border-top:6px solid var(--c)}
.note:nth-child(3n+1){transform:rotate(-1.4deg)}
.note:nth-child(3n+2){transform:rotate(.8deg)}
.note:nth-child(3n){transform:rotate(-.5deg)}
.note:hover{transform:rotate(0) translateY(-4px) scale(1.02);box-shadow:var(--shadow-h)}
.note__code{font-family:var(--body);font-weight:700;font-size:.66rem;letter-spacing:.12em;text-transform:uppercase;color:var(--c)}
.note__name{font-family:var(--display);font-weight:800;font-size:1.2rem;margin-top:.45rem;letter-spacing:-.02em;color:var(--ink)}
.note__meta{font-size:.82rem;color:var(--text-soft);margin-top:.15rem}

.classlist{background:var(--ink);color:#fff;border-radius:var(--r-lg);padding:1.4rem 1.5rem 1.6rem;box-shadow:var(--shadow)}
.classlist h3{color:#fff;font-size:1.1rem;margin-bottom:.2rem}
.classlist .hand{color:var(--green-hi);font-size:1rem;display:block;margin-bottom:1rem}
.grade{display:flex;justify-content:space-between;align-items:center;padding:.85rem 0;border-top:1px dashed rgba(255,255,255,.22);transition:padding-left .15s,color .15s}
.grade:hover{padding-left:.4rem;color:var(--green-hi)}
.grade__l{font-family:var(--display);font-weight:700;font-size:1.05rem}
.grade__r{font-family:var(--hand);font-size:.92rem;opacity:.8}

/* ========================================================================
   9 · PRODUCT CARDS
   ======================================================================== */
/* Heading on the left, then the stamp and the catalog link. The first child
   takes the slack, so the two on the right stay together and wrap as a pair
   rather than being spread to opposite ends of the row. */
.rail-head{display:flex;align-items:center;margin-bottom:1.8rem;gap:.9rem 1.3rem;flex-wrap:wrap}
.rail-head>:first-child{margin-right:auto}
.products{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(1rem,2vw,1.5rem)}
.card{--c:var(--s-science);display:flex;background:var(--paper-card);border-radius:var(--r);overflow:hidden;
  box-shadow:var(--shadow);transition:transform .18s,box-shadow .18s}
.card:hover{transform:translateY(-5px);box-shadow:var(--shadow-h)}
/* The whole card is one link. It carries the flex column the card used to, so
   that the click target is the card and not just the words inside it. */
.card__link{display:flex;flex-direction:column;width:100%}
.cover{position:relative;aspect-ratio:3/4;padding:1rem;display:flex;flex-direction:column;justify-content:space-between;
  background:radial-gradient(130% 90% at 100% 0%,color-mix(in srgb,var(--c) 30%,#fff),transparent 58%),var(--paper-2);border-bottom:1px solid var(--line)}
.cover::before{content:"";position:absolute;left:0;top:0;bottom:0;width:8px;background:var(--c);z-index:1}
/* A real cover is the whole tile. contain, not cover: these are scans of book
   covers at whatever aspect the printer used, and cropping one to fill the
   frame cuts the title off the top of the book. */
.cover__img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;padding:.55rem .55rem .55rem .9rem}
.cover__code{align-self:flex-end}
.cover__title{font-family:var(--display);font-weight:800;font-size:1.3rem;line-height:1.03;letter-spacing:-.02em;padding-left:.15rem;color:var(--ink)}
.cover__series{font-family:var(--hand);font-size:.82rem;color:var(--text-soft);margin-top:.35rem}
/* Corner flag for the two hand-set booleans. Only one shows — a title that is
   both new and a best seller is a best seller, which is the stronger claim. */
.flag{position:absolute;top:.7rem;left:.7rem;z-index:2;font-weight:800;font-size:.62rem;letter-spacing:.08em;
  text-transform:uppercase;padding:.35em .6em;border-radius:99px;box-shadow:0 2px 0 rgba(12,20,120,.14)}
.flag--best{background:var(--green);color:var(--green-ink)}
.flag--new{background:var(--purple);color:#fff}
.card__body{padding:.9rem 1rem 1.05rem;display:flex;flex-direction:column;gap:.6rem;flex:1}
/* Two lines, then an ellipsis. Imported titles run long — "25x: Life Sciences -
   Middle School · 25 mini-lessons…" — and four cards in a row have to agree on
   a height or the grid looks broken. */
.card__name{font-family:var(--display);font-weight:800;font-size:1.02rem;line-height:1.16;letter-spacing:-.02em;color:var(--ink);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.card__teaser{font-size:.87rem;line-height:1.4;color:var(--text-soft);
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.chips{display:flex;gap:.4rem;flex-wrap:wrap}
.chip{font-weight:600;font-size:.68rem;padding:.3em .65em;border-radius:99px;background:var(--paper-2);color:var(--text-soft)}
.chip--subject{background:color-mix(in srgb,var(--c) 16%,#fff);color:color-mix(in srgb,var(--c) 72%,#000)}
.card__title{font-weight:600;font-size:.97rem;line-height:1.3}
/* Wraps rather than collides: in a two-up grid on a 320px screen "View →" and
   the page count have less room between them than they need. */
.card__foot{margin-top:auto;display:flex;justify-content:space-between;align-items:center;
  flex-wrap:wrap;gap:.25rem .8rem}
.card__view{font-weight:700;font-size:.85rem;display:inline-flex;gap:.4em;transition:gap .15s}
.card:hover .card__view{gap:.65em;color:color-mix(in srgb,var(--c) 62%,#000)}
.card__count{font-family:var(--hand);font-size:.88rem;color:var(--text-soft)}

/* /samples: the same card, with the three free files listed on it. Four links
   means the card can no longer BE one link, so it stacks — the cover and body
   go to the workbook's page, and the files sit under a dashed rule in the strip
   below.

   The strip is a fixed three rows on every card, because the shelf only holds
   workbooks that have all three; nothing here has to cope with a card that
   offers two. That is what lets the grid keep four cards the same height
   without a min-height anywhere. */
.card--sample{flex-direction:column}
.card--sample .card__link{flex:1}
.card--sample .card__body{padding-bottom:.85rem}
.flag--sample{background:var(--purple);color:#fff}
.card__kit{list-style:none;border-top:1px dashed var(--line);padding:.45rem .5rem .55rem;
  display:flex;flex-direction:column;gap:.1rem}
/* Rows, not a three-across row of buttons: at the width of a card in a four-up
   grid, three labels side by side are three abbreviations, and "Answer key"
   abbreviated is the one thing on this card a teacher is scanning for. */
.card__kit a{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border-radius:8px;
  font-weight:600;font-size:.83rem;line-height:1.2;color:var(--text);
  transition:background .14s,color .14s}
.card__kit a:hover{background:color-mix(in srgb,var(--c) 12%,#fff);color:var(--ink)}
.card__kitico{flex:none;display:grid;place-items:center;color:var(--c)}
.card__kitname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card__kit .arw{margin-left:auto;color:var(--text-soft);transition:transform .14s}
.card__kit a:hover .arw{transform:translateX(3px);color:var(--ink)}

/* Fills the fourth slot when there are fewer than four best sellers. A row
   that stops after two cards reads as a broken grid; a note that says why
   reads as a shop that is still filling its shelves — which is the truth. */
.card--ghost{flex-direction:column;justify-content:center;align-items:flex-start;gap:.5rem;
  padding:1.4rem 1.3rem;background:none;border:2px dashed var(--line);box-shadow:none}
.card--ghost:hover{transform:none;border-color:var(--green);box-shadow:none}
.card--ghost .hand{color:var(--purple);font-size:1.05rem}
.card--ghost strong{font-family:var(--display);font-weight:800;font-size:1.15rem;line-height:1.15;color:var(--ink)}

/* Rubber stamp. It used to be absolutely positioned at the right edge of
   .rail-head, which parked it directly on top of the "See the whole catalog"
   button at every width where both fitted — desktop included. It sits in the
   flow now and keeps only the tilt. */
.stamp{flex:none;transform:rotate(-8deg);font-family:var(--display);font-weight:800;font-size:.8rem;
  letter-spacing:.06em;text-transform:uppercase;color:var(--purple);border:3px double var(--purple);border-radius:8px;
  padding:.4em .7em;opacity:.85;line-height:1;background:color-mix(in srgb,var(--purple) 6%,transparent)}

/* ========================================================================
   10 · PROOF
   ======================================================================== */
.proof{background:var(--ink);color:#fff}
.proof .wrap{padding-block:clamp(3rem,6vw,4.5rem)}
.proof__top{display:flex;justify-content:space-between;align-items:flex-end;gap:1.5rem;flex-wrap:wrap;margin-bottom:2rem}
.proof__top h2{color:#fff;max-width:18ch}
.proof__top .hand{color:var(--green-hi);font-size:1.2rem}
.badges{display:grid;grid-template-columns:repeat(4,1fr);gap:1.1rem;margin-bottom:2.2rem}
.badge{border:1.5px dashed rgba(255,255,255,.24);border-radius:var(--r);padding:1.3rem 1.2rem}
.badge b{display:block;font-family:var(--display);font-weight:800;font-size:1.55rem;color:var(--green-hi);letter-spacing:-.02em}
.badge span{font-size:.85rem;color:rgba(255,255,255,.72);margin-top:.25rem;display:block}
.quotes{display:grid;grid-template-columns:repeat(3,1fr);gap:1.2rem}
.quote{background:var(--paper-card);color:var(--text);border-radius:var(--r);padding:1.3rem;box-shadow:var(--shadow)}
.quote:nth-child(2){transform:rotate(-1deg)}
.quote:nth-child(3){transform:rotate(.8deg)}
.quote .stars{color:var(--green);letter-spacing:.1em;font-size:.95rem;margin-bottom:.6rem}
.quote p{font-size:.98rem;line-height:1.5}
.quote footer{margin-top:.9rem;font-family:var(--hand);font-size:.95rem;color:var(--text-soft)}
.ph{font-size:.62rem;color:var(--text-soft);opacity:.6;display:block;margin-top:.5rem}

/* ========================================================================
   11 · CTE SPOTLIGHT
   ======================================================================== */
.spot{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1.5rem,4vw,3.5rem);align-items:center}
.spot__panel{background:linear-gradient(135deg,var(--s-cte),#d63062);color:#fff;border-radius:var(--r-lg);padding:clamp(1.8rem,4vw,2.8rem);box-shadow:var(--shadow);position:relative}
.spot__panel .eyebrow{color:rgba(255,255,255,.82)}
.spot__panel h2{color:#fff;margin:.55rem 0 1rem}
.spot__panel p{color:rgba(255,255,255,.93);max-width:42ch}
.pathlist{margin-top:1.5rem}
.pathlist a{display:flex;justify-content:space-between;align-items:center;padding:.8rem 0;border-top:1px dashed rgba(255,255,255,.4);font-weight:600;transition:padding-left .15s}
.pathlist a:hover{padding-left:.4rem}
.pathlist a span{font-family:var(--hand);opacity:.9;font-weight:500}
.spot__copy h3{font-size:1.4rem;margin-bottom:.5rem;color:var(--ink)}
.spot__copy>p{color:var(--text-soft);margin-bottom:1rem}
.spot__copy ul{list-style:none;display:flex;flex-direction:column;gap:.7rem}
.spot__copy li{display:flex;gap:.7rem;align-items:flex-start;font-size:.96rem}
.spot__copy li::before{content:"✓";flex:none;color:#fff;background:var(--green);width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:.75rem;font-weight:700;margin-top:.1rem}

/* ========================================================================
   12 · CLOSER + FOOTER
   ======================================================================== */
.closer{background:var(--paper-2);text-align:center}
.closer .wrap{padding-block:clamp(3rem,7vw,5rem)}
.closer .hand{color:var(--purple);font-size:1.3rem;display:block;margin-bottom:.5rem;transform:rotate(-1.5deg)}
.closer h2{max-width:17ch;margin-inline:auto;color:var(--ink)}
.closer p{color:var(--text-soft);max-width:44ch;margin:.9rem auto 1.8rem}
.closer__cta{display:flex;gap:.8rem;justify-content:center;flex-wrap:wrap}

.foot{background:var(--ink);color:rgba(255,255,255,.74)}
.foot .wrap{padding-block:3rem 2rem}
.foot__grid{display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:2rem;padding-bottom:2.2rem;border-bottom:1px dashed rgba(255,255,255,.2)}
.foot h4{font-weight:700;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:#fff;margin-bottom:.9rem}
.foot ul{list-style:none;display:flex;flex-direction:column;gap:.55rem}
.foot a{font-size:.9rem;transition:color .15s}
.foot a:hover{color:var(--green-hi)}
.foot__brand img{height:52px;width:auto;margin-bottom:.9rem}
.foot__brand p{font-size:.88rem;max-width:32ch;line-height:1.5}
/* The category links. A wrapping row rather than a fifth column of the grid
   above: there are a few dozen of them and they grow with the catalog, so a
   column would either be a very long one or a list that quietly stops. Smaller
   and dimmer than the columns because this is the index, not the navigation —
   somebody looking for "Contact" should still find it first. */
.foot__cats{padding-top:1.6rem;border-bottom:1px dashed rgba(255,255,255,.2);
  padding-bottom:1.6rem}
.foot__cats ul{flex-direction:row;flex-wrap:wrap;gap:.4rem 1.1rem}
.foot__cats a{font-size:.82rem;opacity:.8}
.foot__cats a:hover{opacity:1}

.foot__bottom{display:flex;justify-content:space-between;padding-top:1.5rem;flex-wrap:wrap;gap:.8rem;font-size:.8rem}

/* ========================================================================
   13 · RESPONSIVE
   ======================================================================== */
@media (max-width:1000px){
  .hero__grid{grid-template-columns:1fr}
  .card-stats{max-width:400px;transform:rotate(0)}
  .browse{grid-template-columns:1fr}
  .products{grid-template-columns:repeat(3,1fr)}
  .badges{grid-template-columns:repeat(2,1fr)}
  .quotes{grid-template-columns:1fr}
  .spot{grid-template-columns:1fr}
  .foot__grid{grid-template-columns:1fr 1fr}
}

/* The nav folds into the toggle at 990, not 640. Between those two widths the
   links wrap onto a second row, "unlocking potential" runs underneath
   "Catalog", and a signed-in admin — who gets two links more — pushes the bar
   past the viewport and makes the whole page scroll sideways.

   WAS 900, FOR SEVEN LINKS. "Contact" made it eight, and a link is not free:
   measured from Hanken Grotesk at the .92rem/600 this rule renders it at, the
   word is 53.4px and the flex gap before it is 1.7rem = 27.2px, so the row
   needs 80.6px more than it did. 900 + 81 = 981, rounded up to 990 to keep the
   margin the old number had. Anything below that is the toggle, which is a
   working menu; getting this wrong in the other direction is a bar that
   overflows the viewport, so it is rounded up rather than down.

   IF A NINTH LINK IS EVER ADDED, this number moves again — measure the word
   at .92rem/600, add 27.2 for its gap, and add the total here. */
@media (max-width:990px){
  .nav{position:absolute;top:70px;left:0;right:0;background:var(--paper);flex-direction:column;align-items:stretch;gap:0;
    border-bottom:1.5px solid var(--line);padding:.4rem var(--gut) 1.2rem;display:none;
    box-shadow:0 18px 34px -24px rgba(12,20,120,.55)}
  .nav.open{display:flex}
  .nav a{padding:.85rem 0;border-top:1px dashed var(--line)}
  /* The dashed rule is a divider between links; on the pill it just draws a
     line across the top of the button. */
  .nav .btn{margin-top:.7rem;justify-content:center;border-top:none}
  .nav__form{border-top:1px dashed var(--line)}
  .nav__logout{padding:.85rem 0;width:100%;text-align:left}
  .navtoggle{display:block}
}
/* The card grid rode along in the block above while the nav folded at the same
   width. It does not belong to the nav and its width was never in question, so
   it stays at 900 rather than following the header up to 990 — which would have
   left the three-column shelf alive over a ten-pixel range between 1000 and
   990 and dropped everything else straight to two. */
@media (max-width:900px){
  .products{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:640px){
  .brand__tag{display:none}
  .subjects{grid-template-columns:1fr 1fr}
  .badges{grid-template-columns:1fr}
  .foot__grid{grid-template-columns:1fr}
  .note{transform:none!important}
}
/* Two cards per row on a phone. One card per row makes the cover a 300px-tall
   slab and puts a single book on a whole screen, so the shelf reads as a list
   of posters. The trade is a card about 160px wide, which is too narrow for the
   type set above — so everything on it steps down here: tighter gap and
   padding to buy back width, smaller title and chips, and a teaser that
   hyphenates rather than breaking mid-word. */
@media (max-width:560px){
  .products{grid-template-columns:repeat(2,1fr);gap:.75rem}
  .cover{padding:.7rem}
  .cover__img{padding:.4rem .4rem .4rem .7rem}
  .cover__title{font-size:1.05rem}
  .cover__series{font-size:.75rem}
  .card__body{padding:.7rem .75rem .8rem;gap:.45rem}
  .chips{gap:.3rem}
  .chip{font-size:.62rem;padding:.28em .5em}
  .card__name{font-size:.92rem}
  /* Long single words — "Comprehension", a bare URL in an imported teaser —
     are wider than the column, so let them hyphenate before they overflow. */
  .card__teaser{font-size:.8rem;-webkit-line-clamp:2;hyphens:auto;overflow-wrap:break-word}
  .card__view{font-size:.8rem}
  .card__count{font-size:.8rem}
  /* The file strip keeps all three rows at half width — they are what the card
     is for — and buys the room back from the padding around them. */
  .card__kit{padding:.35rem .35rem .45rem}
  .card__kit a{font-size:.78rem;gap:.4rem;padding:.35rem .4rem}
  /* Spans the row: at half width the ghost's three lines of copy stack into a
     ragged block taller than the cards it sits beside. */
  .card--ghost{grid-column:1/-1;padding:1.1rem}
  .card--ghost strong{font-size:1.05rem}
}
@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important;scroll-behavior:auto!important}
  .circled path{stroke-dashoffset:0}
}

/* ========================================================================
   14 · FORMS · AUTH SCREENS
   ======================================================================== */
.authwrap{display:grid;place-items:center;padding-block:clamp(2.5rem,7vw,5rem);min-height:58vh}
.authcard{width:100%;max-width:410px;background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);box-shadow:var(--shadow);padding:clamp(1.5rem,4vw,2.2rem);position:relative;overflow:hidden}
/* washi-tape strip, same signature as the hero stat card */
.authcard::before{content:"";position:absolute;top:-10px;left:50%;transform:translateX(-50%) rotate(-2deg);
  width:120px;height:26px;background:color-mix(in srgb,var(--green) 55%,transparent);
  border-left:1px dashed rgba(255,255,255,.6);border-right:1px dashed rgba(255,255,255,.6)}
.authcard h1{font-size:1.7rem;letter-spacing:-.025em;color:var(--ink);margin-top:.5rem}
.authcard .hand{display:block;margin-top:.35rem;font-size:1rem;color:var(--purple)}
.authcard__logo{height:56px;width:auto;margin:.6rem 0 1rem}

.field{display:flex;flex-direction:column;gap:.4rem;margin-top:1.2rem}
.field label{font-weight:600;font-size:.85rem;color:var(--text-soft)}
.field input,.field select,.field textarea{font:inherit;color:var(--text);background:var(--paper);
  border:1.5px solid var(--line);border-radius:var(--r);padding:.7em .85em;transition:border-color .15s,box-shadow .15s}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--green);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 18%,transparent);outline:none}
.field__hint{font-size:.78rem;color:var(--text-soft)}
.authcard .btn{width:100%;justify-content:center;margin-top:1.5rem}

.alert{margin-top:1.2rem;padding:.75rem .9rem;border-radius:var(--r);font-size:.88rem;line-height:1.4;
  border-left:4px solid var(--red);background:color-mix(in srgb,var(--red) 8%,var(--paper-card))}
.alert--note{border-left-color:var(--purple);background:color-mix(in srgb,var(--purple) 10%,var(--paper-card))}
/* .alert--ok has been in _layout.html's flash markup since the admin was
   built and has never had a rule, so every "Saved." has been rendering in the
   red of a failure. Green, which is what it was always asking for. */
.alert--ok{border-left-color:var(--green);background:color-mix(in srgb,var(--green) 9%,var(--paper-card))}
/* Not an error and not good news either: the state the site is deliberately
   in while somebody works on it. */
.alert--warn{border-left-color:var(--amber);background:color-mix(in srgb,var(--amber) 12%,var(--paper-card))}
/* Several to a table cell on the old-site review screen, where an alert is one
   row's objection rather than the whole page's bad news. */
.alert--tight{margin-top:0;padding:.5rem .6rem;font-size:.82rem}

/* ========================================================================
   15 · ADMIN
   --------------------------------------------------------------------------
   Same tokens as the public site, but flatter and denser — a working desk,
   not a shop window. No rotation, no hand-drawn marks except the greeting.
   ======================================================================== */
.adminbar{background:var(--ink);color:#fff}
.adminbar .wrap{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;flex-wrap:wrap;padding-block:1.6rem 1.5rem}
.adminbar h1{color:#fff;font-size:clamp(1.6rem,3vw,2.2rem);letter-spacing:-.025em}
.adminbar .eyebrow{color:rgba(255,255,255,.6)}
.adminbar .hand{color:var(--green-hi);font-size:1.02rem;display:block;margin-top:.35rem}
.adminbar__actions{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}
.adminbar .btn--outline{border-color:rgba(255,255,255,.55);color:#fff}
.adminbar .btn--outline:hover{background:#fff;color:var(--ink)}

.admin{padding-block:clamp(2rem,4vw,3rem) clamp(3rem,6vw,4.5rem)}

/* quick-read metric tiles */
.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(.8rem,1.6vw,1.1rem);margin-bottom:clamp(1.6rem,3vw,2.4rem)}
.tile{--c:var(--blue);background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r);
  padding:1.1rem 1.2rem;border-top:5px solid var(--c)}
.tile dt{font-size:.78rem;letter-spacing:.1em;text-transform:uppercase;font-weight:700;color:var(--text-soft)}
.tile dd{font-family:var(--display);font-weight:800;font-size:1.9rem;color:var(--ink);letter-spacing:-.02em;margin-top:.3rem;line-height:1}
.tile dd small{display:block;font-family:var(--body);font-weight:500;font-size:.78rem;color:var(--text-soft);letter-spacing:0;margin-top:.35rem}

/* panel grid — each panel is one area of the admin */
/* min(280px,100%), not a bare 280px: a bare track minimum is a floor the track
   cannot go under, so on a viewport narrower than the minimum plus the gutters
   the grid pushes past the screen edge and the whole page scrolls sideways.
   Every auto-fit/auto-fill grid below is capped the same way. */
.panels{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:clamp(1rem,2vw,1.4rem)}
.panel{background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  padding:1.3rem 1.4rem 1.4rem;box-shadow:var(--shadow);display:flex;flex-direction:column;gap:.55rem}
.panel__hd{display:flex;align-items:center;justify-content:space-between;gap:.8rem}
.panel h2,.panel h3{font-size:1.12rem;color:var(--ink);letter-spacing:-.02em}
.panel p{font-size:.92rem;color:var(--text-soft);line-height:1.5}
.panel__foot{margin-top:auto;padding-top:.9rem;display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}

/* status pill: what's live vs. what's still to build */
.pill{font-weight:700;font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;padding:.35em .7em;border-radius:99px;white-space:nowrap}
.pill--soon{background:color-mix(in srgb,var(--purple) 12%,#fff);color:var(--purple)}
.pill--live{background:color-mix(in srgb,var(--green) 16%,#fff);color:var(--green-ink)}
/* Also the header marker, where it is an <a> — hence the explicit colour on
   hover, which would otherwise take the nav link's. */
.pill--maint{background:color-mix(in srgb,var(--amber) 18%,#fff);color:var(--amber);
  border:1.5px solid color-mix(in srgb,var(--amber) 40%,transparent)}
a.pill--maint:hover{background:var(--amber);color:#fff;border-color:var(--amber);text-decoration:none}

/* key/value rows — session details, and a shape for future record lists */
.kv{list-style:none;display:flex;flex-direction:column}
.kv li{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;padding:.6rem 0;border-top:1px dashed var(--line);font-size:.92rem}
.kv li:first-child{border-top:none}
.kv span:first-child{color:var(--text-soft)}
.kv span:last-child{font-weight:600;color:var(--ink);text-align:right;overflow-wrap:anywhere}

@media (max-width:1000px){ .tiles{grid-template-columns:repeat(2,1fr)} }
@media (max-width:640px){
  .tiles{grid-template-columns:1fr}
  .adminbar .wrap{flex-direction:column;align-items:flex-start}
}

/* ========================================================================
   16 · ADMIN · CMS COMPONENTS
   ======================================================================== */

/* section nav under the admin bar */
.subnav{background:var(--ink-2);position:sticky;top:70px;z-index:40}
.subnav .wrap{display:flex;gap:1.4rem;overflow-x:auto}
.subnav a{color:rgba(255,255,255,.7);font-weight:600;font-size:.88rem;padding:.85rem 0;white-space:nowrap;
  border-bottom:3px solid transparent;transition:color .15s,border-color .15s}
.subnav a:hover{color:#fff}
.subnav a.is-on{color:#fff;border-bottom-color:var(--green-hi)}

.flash{margin-bottom:1.4rem}
.muted{color:var(--text-soft)}
.small{font-size:.84rem}
.warn{color:var(--red);font-weight:600}

/* editor tab strip */
.tabs{display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:1.6rem;border-bottom:1.5px solid var(--line)}
.tab{padding:.7em 1.1em;font-weight:700;font-size:.9rem;color:var(--text-soft);
  border:1.5px solid transparent;border-bottom:none;border-radius:var(--r) var(--r) 0 0;margin-bottom:-1.5px}
.tab:hover{color:var(--ink)}
.tab.is-on{background:var(--paper-card);border-color:var(--line);color:var(--ink)}
.tab.is-off{opacity:.4;cursor:not-allowed}

/* list toolbar + filters */
.listbar{display:flex;justify-content:space-between;align-items:flex-end;gap:1rem;flex-wrap:wrap;margin-bottom:1.4rem}
.filters{display:flex;gap:.5rem;flex-wrap:wrap;align-items:center}
.filters input,.filters select{font:inherit;font-size:.9rem;color:var(--text);background:var(--paper-card);
  border:1.5px solid var(--line);border-radius:var(--r);padding:.5em .7em}
.filters input:focus,.filters select:focus{border-color:var(--green);outline:none}

/* data table */
.table{width:100%;border-collapse:collapse;background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r);overflow:hidden}
.table th{text-align:left;font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;color:var(--text-soft);
  padding:.8rem .9rem;background:var(--paper-2);border-bottom:1.5px solid var(--line);white-space:nowrap}
.table td{padding:.8rem .9rem;border-top:1px solid var(--line);vertical-align:top;font-size:.92rem}
.table tr:hover td{background:color-mix(in srgb,var(--paper-2) 45%,transparent)}
.table a strong{color:var(--ink)}

.thumb{display:block;width:228px;height:auto;border-radius:4px;border:1px solid var(--line)}
.thumb--empty{aspect-ratio:2/3;background:repeating-linear-gradient(45deg,var(--paper-2),var(--paper-2) 5px,#fff 5px,#fff 10px)}

/* workbook cards — the "Edit a workbook" list.
   Two to a row on desktop at half the cover width, because that list is
   searched by looking at covers and a full-width row showed four books at a
   time. minmax(0,1fr) rather than 1fr so a long slug can't hold a column open
   wider than half the page; min-width:0 on the body for the same reason one
   level down. One column below 900px, where the table it replaced used to
   start scrolling sideways. */
.wbgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;align-items:start}
.wbcard{display:grid;grid-template-columns:114px minmax(0,1fr);gap:1rem;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r);
  padding:1rem;transition:border-color .15s ease}
.wbcard:hover{border-color:color-mix(in srgb,var(--green) 45%,var(--line))}
.wbcard__cover{display:block}
.wbcard .thumb{width:100%}
.wbcard__body{min-width:0;display:flex;flex-direction:column;gap:.4rem}
.wbcard__body a strong{color:var(--ink)}
.wbcard__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem .6rem}
.wbcard__meta .bar{margin-top:0;flex:1 1 60px;max-width:80px}
.wbcard__acts{margin-top:auto;padding-top:.3rem}
@media (max-width:900px){ .wbgrid{grid-template-columns:1fr} }
@media (max-width:420px){ .wbcard{grid-template-columns:84px minmax(0,1fr);gap:.8rem} }

.rowacts{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}
.linkbtn{font-weight:600;font-size:.84rem;color:var(--blue);text-decoration:underline;text-underline-offset:2px}
.linkbtn:hover{color:var(--purple)}

/* status pills reuse the .pill base from section 15 */
.pill--published{background:color-mix(in srgb,var(--green) 16%,#fff);color:var(--green-ink)}
.pill--draft{background:var(--paper-2);color:var(--text-soft)}
.pill--retired{background:color-mix(in srgb,var(--red) 12%,#fff);color:var(--red)}

/* editor forms */
.formgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr));gap:clamp(1rem,2vw,1.4rem);align-items:start}
.formgrid .panel,.repeater{gap:0}
/* minmax(0,1fr) rather than 1fr, plus min-width:0 on the children: a grid item
   defaults to min-width:auto, so a text input's intrinsic width (~20 chars)
   would hold these columns open wider than the panel and spill the fields out
   of it. Both halves are needed — the track has to be allowed to shrink AND
   the item has to be allowed to shrink inside it. */
.pair{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.9rem}
.pair>*{min-width:0}
.field input,.field select,.field textarea{max-width:100%}
.field textarea{resize:vertical;font-family:inherit}

/* Links & resources: a full-width panel so its own two columns have room. In a
   ~340px formgrid track two columns of URL fields would be unreadable, so the
   panel spans every track and splits inside itself instead. */
.panel--wide{grid-column:1/-1}
.linkgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 1.4rem}
.linkgrid>*{min-width:0}
/* The uploaded-file line sits between the label and the input, so it reads as
   part of the field rather than as a hint about it. */
.filenow{display:flex;flex-direction:column;gap:.15rem;font-size:.85rem}
.filenow a{color:var(--green);font-weight:600}
.filenow em{color:var(--text-soft);font-style:normal;font-size:.78rem;line-height:1.4}
.field input[type=file]{padding:.5rem .6rem;font-size:.88rem}
.linkgrid .check{margin-top:.1rem;font-size:.85rem}
@media (max-width:860px){ .linkgrid{grid-template-columns:1fr} }
.checks{display:flex;flex-direction:column;gap:.55rem;margin-top:.9rem}
.check{display:flex;align-items:center;gap:.55rem;font-size:.93rem;cursor:pointer}
.check input{width:17px;height:17px;accent-color:var(--green);flex:none}
.check em{color:var(--text-soft);font-style:normal;font-size:.82rem}
.preview{max-width:130px;border-radius:6px;border:1px solid var(--line);margin-bottom:.5rem}
/* An explicit rule, because the import screen redefines .preview as a grid
   container further down this file — and any author display: beats the UA
   sheet's [hidden]{display:none}. Without this, a product with no cover yet
   renders the empty <img> the edit page keeps in place for the upload to
   fill. */
.preview[hidden]{display:none}
.formactions{display:flex;gap:.8rem;flex-wrap:wrap;margin-top:1.6rem;align-items:center}

/* subject picker with a primary radio */
.picker{width:100%;border-collapse:collapse;margin-top:.6rem}
.picker th{text-align:left;font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-soft);padding:.4rem .3rem}
.picker td{padding:.4rem .3rem;border-top:1px dashed var(--line);font-size:.93rem}
.picker input[type=checkbox]{width:17px;height:17px;accent-color:var(--green)}
.picker input[type=radio]{width:17px;height:17px;accent-color:var(--purple)}
.picker label{cursor:pointer}

/* pathways lock until CTE is selected */
[data-pathways][data-locked] .checks{opacity:.45;pointer-events:none}

/* repeaters: editions, listings, attached standards */
.repeater{margin-bottom:.4rem}
.subdelete{margin:0 0 1.4rem .2rem}
.editionblock{margin-bottom:2.2rem}
.editionblock__hd{font-size:1.1rem;color:var(--ink);margin-bottom:.8rem;
  display:flex;align-items:baseline;gap:.6rem;border-bottom:1.5px dashed var(--line);padding-bottom:.5rem}

/* inline-editable rows (standards + taxonomy libraries) */
.resultlist{display:flex;flex-direction:column;gap:.5rem;margin-top:1rem}
.resultrow{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;background:var(--paper-card);
  border:1.5px solid var(--line);border-radius:var(--r);padding:.7rem .9rem}
.resultrow input,.resultrow select{font:inherit;font-size:.9rem;color:var(--text);background:var(--paper);
  border:1.5px solid var(--line);border-radius:8px;padding:.45em .6em}
.resultrow input:focus,.resultrow select:focus{border-color:var(--green);outline:none}
.resultrow .grow{flex:1;min-width:140px}
.resultrow__id{flex:1;min-width:200px}

.empty{background:var(--paper-card);border:1.5px dashed var(--line);border-radius:var(--r-lg);
  padding:2.4rem 1.5rem;text-align:center;display:flex;flex-direction:column;gap:.5rem;align-items:center}

.dangerzone{display:flex;justify-content:space-between;align-items:center;gap:1.5rem;flex-wrap:wrap;
  margin-top:2.5rem;padding:1.1rem 1.3rem;border:1.5px solid color-mix(in srgb,var(--red) 35%,var(--line));
  border-radius:var(--r);background:color-mix(in srgb,var(--red) 4%,var(--paper-card))}
.dangerbtn{border-color:var(--red);color:var(--red)}
.dangerbtn:hover{background:var(--red);color:#fff}

/* Stars, in the admin. The home page's .quote .stars is more specific and so
   keeps its own colour — this is the bare class, for the review dashboard and
   the star picker. */
.stars{color:var(--green);letter-spacing:.08em;white-space:nowrap}

/* Visually hidden but still focusable and still submittable. Used for the
   default submit button that makes Return save a form rather than trip one of
   the buttons that happens to come first in the markup. */
.offscreen{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em;
  background:var(--paper-2);padding:.15em .4em;border-radius:4px}

/* A data table scrolls sideways instead of being crushed. display:block is what
   makes overflow-x apply; the cell minimums are what stop the columns from
   collapsing to one word each once it does — without them a title column ends
   up a 40px ribbon of hyphenated slug. */
@media (max-width:900px){
  .table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
  .table th,.table td{min-width:8rem}
  .table th:first-child,.table td:first-child{min-width:15rem}
}
@media (max-width:640px){
  .pair{grid-template-columns:1fr}
  .subnav{top:70px}
}

/* ========================================================================
   17 · ADMIN · IMPORT WIZARD
   ======================================================================== */

/* numbered step rail */
.steps{list-style:none;display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.6rem}
.step{display:flex;align-items:center;gap:.6rem;padding:.6rem 1rem;border-radius:99px;
  background:var(--paper-card);border:1.5px solid var(--line);font-size:.88rem;color:var(--text-soft)}
.step__n{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;flex:none;
  background:var(--paper-2);color:var(--text-soft);font-weight:800;font-size:.78rem}
.step.is-on{border-color:var(--green);color:var(--ink);font-weight:600}
.step.is-on .step__n{background:var(--green);color:var(--green-ink)}
.step.is-done{border-color:color-mix(in srgb,var(--green) 45%,var(--line))}
.step.is-done .step__n{background:var(--green-hi);color:var(--green-ink)}

/* preview: image beside the pulled data */
.preview{display:grid;grid-template-columns:260px 1fr;gap:clamp(1.2rem,3vw,2rem);
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  padding:clamp(1.2rem,3vw,1.8rem);box-shadow:var(--shadow)}
.preview__img img{width:100%;border-radius:var(--r);border:1px solid var(--line);margin-bottom:.6rem}
.preview__body h2{color:var(--ink);font-size:1.4rem;letter-spacing:-.02em}


/* compliance / feature bullets */
.rules{list-style:none;display:flex;flex-direction:column;gap:.6rem;margin-top:.4rem}
/* Deliberately not display:flex. These items carry inline <code> and <strong>
   inside the sentence, and each one of those becomes a flex item of its own —
   so the line breaks into side-by-side columns as soon as the panel is narrow.
   The marker is positioned instead, which leaves the sentence a single run. */
.rules li{position:relative;padding-left:1.1rem;font-size:.92rem;line-height:1.5;color:var(--text)}
.rules li::before{content:"·";position:absolute;left:.1rem;top:-.12em;
  color:var(--green);font-weight:800;font-size:1.4rem;line-height:1}

@media (max-width:760px){ .preview{grid-template-columns:1fr} }

/* ========================================================================
   18 · ADMIN · SIDE-BY-SIDE DESCRIPTION EDITOR
   ======================================================================== */
.sxs{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem;margin-top:1rem;align-items:start}
.sxs .field{margin-top:0}
.sxs textarea{font-family:inherit;font-size:.93rem;line-height:1.5;resize:vertical;min-height:14rem}
.sxs label .muted{font-weight:500;text-transform:none;letter-spacing:0}
@media (max-width:860px){ .sxs{grid-template-columns:1fr} }

/* ========================================================================
   19 · ADMIN · IMPORT WIZARD RAIL + PUBLIC PRODUCT PAGE
   ======================================================================== */

/* progress rail — five steps, current one highlighted, done ones clickable */
.wiz{display:grid;grid-template-columns:repeat(5,1fr);gap:.5rem;margin-bottom:1rem}
.wiz__step{display:flex;align-items:center;gap:.6rem;padding:.75rem .9rem;border-radius:var(--r);
  background:var(--paper-card);border:1.5px solid var(--line);transition:border-color .15s,transform .15s}
a.wiz__step:hover{border-color:var(--green);transform:translateY(-2px)}
.wiz__n{width:26px;height:26px;border-radius:50%;flex:none;display:grid;place-items:center;
  font-weight:800;font-size:.8rem;background:var(--paper-2);color:var(--text-soft)}
.wiz__label{display:flex;flex-direction:column;line-height:1.2;min-width:0}
.wiz__label strong{font-size:.9rem;color:var(--ink)}
.wiz__label em{font-style:normal;font-size:.72rem;color:var(--text-soft);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wiz__step.is-on{border-color:var(--green);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 15%,transparent)}
.wiz__step.is-on .wiz__n{background:var(--green);color:var(--green-ink)}
.wiz__step.is-done .wiz__n{background:var(--green-hi);color:var(--green-ink)}
.wiz__step.is-todo{opacity:.55}
.wiz__meta{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;margin-bottom:1.6rem}

/* thin progress bar in the products list */
.bar{height:4px;border-radius:99px;background:var(--paper-2);margin-top:.3rem;overflow:hidden;max-width:110px}
.bar span{display:block;height:100%;background:var(--green);border-radius:99px}

/* browser-chrome frame around the step-4 preview */
.previewframe{border:1.5px solid var(--line);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow);background:var(--paper-card)}
.previewframe__bar{display:flex;align-items:center;gap:.4rem;padding:.6rem .9rem;background:var(--paper-2);border-bottom:1.5px solid var(--line)}
.previewframe__bar .dot{width:10px;height:10px;border-radius:50%;background:var(--line)}
.previewframe__url{margin-left:.6rem;font-size:.78rem;color:var(--text-soft);
  background:var(--paper-card);border-radius:99px;padding:.2em .8em;border:1px solid var(--line)}
.previewframe__page{padding:clamp(1.2rem,3vw,2.2rem)}

.donecard{display:flex;gap:1.2rem;align-items:flex-start}
.donecard__mark{width:46px;height:46px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:var(--green);color:var(--green-ink);font-size:1.4rem;font-weight:800}

/* The public product page used to live here, sharing a section header with the
   wizard rail because the wizard's step-4 preview renders it. It has its own
   section at the end of this file now — it outgrew a footnote, and being last
   is also what lets it win against the unscoped .chip and .muted rules the
   admin sections declare further down. See §27. */

@media (max-width:900px){
  .wiz{grid-template-columns:repeat(2,1fr)}
  .wiz__label em{display:none}
}
@media (max-width:560px){ .wiz{grid-template-columns:1fr} }

/* ========================================================================
   20 · SAMPLE PAGES + TABLE OF CONTENTS
   --------------------------------------------------------------------------
   Two blocks that appear twice each: in the product editor's tabs, and on the
   public page (which the wizard's step-4 preview renders too). The admin
   variants add controls; the shapes themselves are shared.
   ======================================================================== */

/* a section label with an action on the far right */
.sechead{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap}

/* --- admin: the sample-page gallery being arranged ---------------------- */
.pagegrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(190px,100%),1fr));gap:1rem}
.pagecard{position:relative;background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r);padding:.6rem;display:flex;flex-direction:column;gap:.5rem}
.pagecard img{width:100%;aspect-ratio:3/4;object-fit:contain;background:var(--paper-2);
  border-radius:6px;border:1px solid var(--line)}
/* the position number, so the order is readable at a glance rather than
   inferred from where a card happens to have wrapped to */
.pagecard__n{position:absolute;top:.4rem;left:.4rem;z-index:1;min-width:22px;height:22px;
  padding:0 .35em;border-radius:99px;display:grid;place-items:center;
  font-weight:800;font-size:.72rem;background:var(--ink);color:#fff}
.pagecard figcaption{display:flex;flex-direction:column;gap:.45rem}
.pagecard input{font:inherit;font-size:.82rem;width:100%;color:var(--text);background:var(--paper);
  border:1.5px solid var(--line);border-radius:8px;padding:.4em .55em}
.pagecard input:focus{border-color:var(--green);outline:none}
.pagecard__acts{display:flex;gap:.35rem;flex-wrap:wrap}
.pagecard__acts .btn{padding:.35em .7em;font-size:.78rem}
.pagecard__acts .btn[disabled]{opacity:.35;cursor:not-allowed}
.pagecard__acts .btn[disabled]:hover{background:none;color:var(--ink);transform:none}

/* --- admin: the parsed-review check table -------------------------------
   One card per review the model pulled out of a paste, before any of them are
   written. See _reviews_import.html.

   Every field is a real input rather than text with an Edit button, because the
   whole purpose of this screen is that somebody reads each row against the
   source and fixes what came out wrong. A row that has to be opened first is a
   row that gets skimmed.

   The unticked state is dimmed rather than hidden: the decision to drop a
   review stays visible and reversible until Import is pressed. :has() does that
   with no script; browsers without it simply show every row at full strength,
   which is a cosmetic loss and not a functional one. */
.revrows{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}
.revrow{display:flex;flex-direction:column;gap:.75rem;
  background:var(--paper);border:1.5px solid var(--line);border-radius:var(--r);
  padding:1rem 1.05rem;transition:opacity .15s,border-color .15s}
.revrow:has(.revrow__keep input:not(:checked)){opacity:.45}
.revrow:has(.revrow__keep input:checked){border-color:color-mix(in srgb,var(--green) 34%,var(--line))}
.revrow__keep{font-weight:700;font-size:.86rem}
.revrow__meta{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr);gap:.7rem}
.revrow .field{margin:0}
.revrow textarea{min-height:0}
/* What arrived, under a summary. Monospaced so a punctuation fix is visible as
   a difference rather than as a feeling that something moved. */
.revrow__diff summary{cursor:pointer;font-size:.82rem;font-weight:700;color:var(--text-soft)}
.revrow__diff summary:hover{color:var(--ink)}
.revrow__diff p{margin-top:.5rem;padding:.7rem .8rem;border-radius:8px;
  background:var(--paper-2);border:1px solid var(--line);
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.82rem;
  line-height:1.55;color:var(--text);white-space:pre-wrap}
@media (max-width:640px){ .revrow__meta{grid-template-columns:1fr} }

/* --- table of contents (admin preview and public page) ------------------ */
/* Row gap carries the separation between units in the admin preview, which has
   no card around each one to do it. The public page overrides it in §27.8,
   where .pdp .toc__unit is a bordered card and 1rem is already plenty. */
.toc{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:1.6rem 2rem;margin-top:.6rem}
.toc__unit{break-inside:avoid}
.toc__unit strong{display:block;font-family:var(--display);font-weight:800;font-size:1rem;
  color:var(--ink);letter-spacing:-.01em;padding-bottom:.35rem;margin-bottom:.4rem;
  border-bottom:1.5px dashed var(--line)}
.toc__unit ul{list-style:none;display:flex;flex-direction:column;gap:.25rem}
.toc__unit li{font-size:.9rem;color:var(--text-soft);line-height:1.4}
.toc--public .toc__unit li{color:var(--text)}

.samples{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(180px,100%),1fr));gap:1rem;margin-top:1rem}
.sample{display:flex;flex-direction:column;gap:.4rem}
.sample img{width:100%;aspect-ratio:3/4;object-fit:contain;background:var(--paper-2);
  border-radius:var(--r);border:1.5px solid var(--line);box-shadow:var(--shadow);
  transition:transform .16s,box-shadow .16s}
.sample a:hover img{transform:translateY(-3px);box-shadow:var(--shadow-h)}
.sample figcaption{font-size:.82rem;color:var(--text-soft);line-height:1.4}

@media (max-width:560px){
  .pagegrid,.samples{grid-template-columns:repeat(2,1fr)}
  .toc{grid-template-columns:1fr}
}

/* ========================================================================
   21 · /ed · EDUCATION NEWS
   ------------------------------------------------------------------------
   SKELETON. This is the plain structural pass that makes /ed a real page
   while the pipeline is built; the dense, headline-forward treatment
   replaces this section wholesale at step 6. Anything here that survives
   that pass survives on merit, not because it was written first.

   Accent colours are not hard-coded: each column sets --c from
   ed_categories.accent_color and everything inside reads it.
   ======================================================================== */

.edhead{border-bottom:2.5px solid var(--ink);padding-block:1.4rem 1rem}
.edhead__in{display:flex;align-items:baseline;justify-content:space-between;gap:1.2rem;flex-wrap:wrap}
.edhead h1{font-size:clamp(1.9rem,4.2vw,2.9rem);color:var(--ink)}
.edhead__meta{display:flex;align-items:baseline;gap:.8rem;flex-wrap:wrap;font-size:.9rem}
.edhead__date{font-weight:700;color:var(--text-soft);font-variant-numeric:tabular-nums}
.edhead__today{font-weight:700;padding:.28em .7em;border-radius:99px;
  background:color-mix(in srgb,var(--c,var(--blue)) 14%,#fff);color:var(--c,var(--blue))}

.edcols{display:grid;grid-template-columns:repeat(5,1fr);gap:0;padding-block:1.6rem 3rem}
.edcol{--c:var(--blue);padding-inline:clamp(.6rem,1.2vw,1rem);border-left:1px solid var(--line)}
.edcol:first-child{border-left:0;padding-left:0}
.edcol:last-child{padding-right:0}
.edcol__hd{border-top:4px solid var(--c);padding-top:.55rem;margin-bottom:1rem}
.edcol__hd h2{font-size:1.02rem;color:var(--ink);letter-spacing:-.01em}
.edcol__hd p{font-size:.78rem;color:var(--text-soft);line-height:1.35;margin-top:.25rem}

.edpost{padding-block:.9rem;border-top:1px solid var(--line)}
.edcol__list>.edpost:first-child{border-top:0}
/* The card thumbnail. 3:2 to match what the image stage asks the model for, so
   a generated illustration is cropped by nothing here. */
.edpost__art{display:block;margin-bottom:.5rem}
.edpost__art img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;
  border-radius:2px;background:color-mix(in srgb,var(--c,var(--blue)) 8%,#fff)}
.edpost h3{font-family:var(--display);font-weight:800;font-size:1.02rem;line-height:1.15;letter-spacing:-.015em}
.edpost h3 a:hover{color:var(--c)}
.edpost__dek{font-size:.85rem;color:var(--text-soft);line-height:1.4;margin-top:.3rem}
.edpost__meta{display:flex;gap:.5rem;align-items:baseline;margin-top:.4rem;font-size:.76rem;color:var(--text-soft)}
.edpost__date{font-variant-numeric:tabular-nums;font-weight:600}

.edempty{font-size:.84rem;color:var(--text-soft);line-height:1.45;padding-block:.9rem}

/* --- The post itself -------------------------------------------------------
   Used by the public page, the admin preview and the run result screen, which
   is why it lives here rather than in a <style> block on one of them. What you
   approve has to be what ships, and that only holds if all three render through
   the same rules. */
.edarticle__in{max-width:44rem;padding-block:2.5rem 4rem}
.edarticle__hd{margin-bottom:1.4rem}
.edarticle__hd h1{font-size:clamp(1.9rem,4.2vw,2.9rem);line-height:1.1;margin:.3rem 0 0}
.edarticle__dek{font-size:1.15rem;line-height:1.45;margin:.7rem 0 0;color:var(--text)}
.edarticle__meta{font-size:.86rem;color:var(--text-soft);margin:.9rem 0 0}

.edarticle__art{margin:1.6rem 0 2rem}
.edarticle__art img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;
  border-radius:2px;background:color-mix(in srgb,var(--c,var(--blue)) 8%,#fff)}

.edarticle__body{font-size:1.05rem;line-height:1.7}
.edarticle__body>p:first-child{font-size:1.12rem;line-height:1.6}
.edarticle__body p{margin:0 0 1.15rem}
.edarticle__body h2,.edarticle__body h3{margin:2rem 0 .6rem;line-height:1.2}
.edarticle__body ul,.edarticle__body ol{margin:0 0 1.15rem;padding-left:1.4rem}
.edarticle__body li{margin-bottom:.4rem}
/* Source links inside the prose are underlined and stay underlined. This is a
   post whose whole claim is that it is checkable — the links are the argument,
   not an embellishment on it. */
.edarticle__body a{color:inherit;text-decoration:underline;
  text-decoration-color:var(--c,currentColor);text-underline-offset:.16em;text-decoration-thickness:.09em}
.edarticle__body a:hover{background:var(--c,transparent);color:#fff}

/* The pull quote is set into the body after the opening paragraph. Indented
   rather than centred so the column keeps one left edge all the way down. */
.edarticle__pull{border-left:3px solid var(--c,var(--ink));margin:1.8rem 0;padding-left:1.1rem;
  font-family:var(--display);font-size:1.35rem;line-height:1.3;color:var(--ink)}

/* Preview only: every inline link tinted, so a reviewer can see at a glance
   whether the citations are spread through the post or stacked in one
   paragraph. That is a real failure mode and it is invisible in plain prose. */
.edarticle--preview .edarticle__body a{
  background:color-mix(in srgb,var(--c,var(--blue)) 12%,transparent);
  box-shadow:0 0 0 .18em color-mix(in srgb,var(--c,var(--blue)) 12%,transparent)}

.edsources{margin-top:3rem;padding-top:1.6rem;border-top:2px solid var(--c,var(--line))}
.edsources h2{font-size:1.05rem;letter-spacing:.06em;text-transform:uppercase;margin:0}
.edsources__note{font-size:.86rem;color:var(--text-soft);margin:.5rem 0 1.1rem}
.edsources__list{margin:0;padding-left:1.3rem}
.edsource{margin-bottom:.85rem;line-height:1.45}
.edsource__headline{display:block;font-weight:600}
.edsource__pub{font-size:.86rem;color:var(--text-soft)}
.edsource__check{display:block;font-size:.78rem}

@media (max-width:900px){
  .edcols{grid-template-columns:1fr;gap:0}
  .edcol{border-left:0;padding-inline:0;border-top:1px solid var(--line);padding-block:1.1rem}
  .edcol:first-child{border-top:0}
}

/* ========================================================================
   22 · THE FINDER  ·  browse in two clicks
   ------------------------------------------------------------------------
   One component, two homes: the home page's #browse band and /catalog. The
   only difference is the cap — see .is-capped at the bottom.

   Every control here is an <a> that works without JavaScript. The states
   below (.is-on, [hidden]) are set by the server on a full page load and by
   finder.js on a click; neither knows which one did it last.
   ======================================================================== */

.finder{background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--shadow);padding:clamp(1.1rem,2.6vw,1.7rem)}

/* --- search --------------------------------------------------------------- */
.finder__search{display:flex;gap:.5rem;margin-bottom:1.3rem}
.finder__search input{flex:1;min-width:0;font:inherit;font-size:.96rem;color:var(--text);background:var(--paper);
  border:1.5px solid var(--line);border-radius:99px;padding:.7em 1.1em;transition:border-color .15s,box-shadow .15s}
.finder__search input:focus{border-color:var(--green);outline:none;
  box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 18%,transparent)}
.finder__search .btn{flex:none;border-radius:99px}

/* --- the two steps -------------------------------------------------------- */
.finder__step{padding-top:1.1rem;border-top:1.5px dashed var(--line)}
.finder__step:first-of-type{padding-top:0;border-top:none}
.finder__label{display:flex;align-items:center;gap:.55rem;margin-bottom:.85rem;
  font-family:var(--hand);font-size:1.06rem;color:var(--text)}
.finder__n{width:22px;height:22px;flex:none;border-radius:50%;display:grid;place-items:center;
  background:var(--ink);color:#fff;font-family:var(--body);font-weight:800;font-size:.72rem}

.chipset{display:flex;flex-wrap:wrap;gap:.7rem;align-items:stretch}
.chipset--sm{gap:.45rem;align-items:center;padding-bottom:.3rem}

/* step 1 — the three rooms, plus everything */
.pick{position:relative;display:flex;flex-direction:column;gap:.1rem;padding:.7rem 1.15rem;
  border-radius:14px;background:var(--paper);border:2px solid var(--line);
  transition:transform .15s,border-color .15s,background .15s,box-shadow .15s}
.pick b{font-family:var(--display);font-weight:800;font-size:1rem;letter-spacing:-.015em;color:var(--ink)}
.pick em{font-style:normal;font-size:.76rem;color:var(--text-soft)}
.pick:hover{border-color:var(--green);transform:translateY(-2px)}
.pick.is-on{border-color:var(--green);background:color-mix(in srgb,var(--green) 11%,#fff);
  box-shadow:0 3px 0 var(--green-d);transform:translateY(-2px)}
.pick__n{position:absolute;top:-9px;right:-7px;min-width:22px;height:22px;padding:0 .35em;border-radius:99px;
  display:grid;place-items:center;background:var(--ink);color:#fff;font-weight:800;font-size:.7rem;
  font-variant-numeric:tabular-nums}
.pick.is-on .pick__n{background:var(--green);color:var(--green-ink)}
/* A room with nothing in it yet is still offered — Middle, High and CTE are
   the shape of the catalog, not a report on it — but it says so. */
.pick.is-empty{opacity:.5}
.pick.is-empty .pick__n{background:var(--text-soft)}

/* step 2 — subjects, or pathways when CTE is the room */
.tagpick{--c:var(--ink);display:inline-flex;align-items:center;gap:.4rem;padding:.44em .9em;border-radius:99px;
  background:var(--paper);border:1.5px solid var(--line);font-weight:600;font-size:.88rem;color:var(--text);
  transition:border-color .15s,background .15s,color .15s}
.tagpick:hover{border-color:var(--c);color:color-mix(in srgb,var(--c) 75%,#000)}
.tagpick.is-on{border-color:var(--c);background:color-mix(in srgb,var(--c) 14%,#fff);
  color:color-mix(in srgb,var(--c) 78%,#000)}
.tagpick__n{font-size:.72rem;font-weight:800;opacity:.6;font-variant-numeric:tabular-nums}
.finder__none{font-family:var(--hand);font-size:.95rem;color:var(--text-soft)}

/* --- result bar + grid ---------------------------------------------------- */
.finder__bar{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap;
  margin:1.3rem 0 1rem;padding-top:1.1rem;border-top:1.5px dashed var(--line)}
.finder__count{font-size:.92rem;color:var(--text-soft)}
.finder__count b{font-family:var(--display);font-size:1.15rem;color:var(--ink);margin-right:.15rem}
.finder__clear{font-family:var(--hand);font-size:.98rem;color:var(--purple);text-decoration:underline;text-underline-offset:3px}

.finder__grid{align-items:stretch}
/* Staggered entrance, restarted by finder.js on each filter. --i is the card's
   position in the *matching* set, capped so the last card in a long grid isn't
   still waiting half a second later. */
@keyframes cardin{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.finder__grid .card.is-in{animation:cardin .34s cubic-bezier(.2,.7,.3,1) backwards;
  animation-delay:calc(var(--i,0) * 32ms)}

.finder__empty{margin-top:.6rem;font-family:var(--hand);font-size:1.1rem;color:var(--text-soft)}
.finder__empty a{color:var(--purple);text-decoration:underline;text-underline-offset:3px}
.finder__more{margin-top:1.4rem;text-align:center}

/* The sibling links at the foot of a category landing page. Real links, not
   the finder's chips: this page is reached from an ad and has no JS filtering
   on it, and these are also what makes the whole set of category pages
   crawlable. Ruled off above rather than boxed, so it reads as the end of the
   shelf instead of a second shelf. */
.catnav{margin-top:2.6rem;padding-top:1.6rem;border-top:1px solid var(--line)}
.catnav__hd{font-size:.82rem;font-weight:800;letter-spacing:.06em;
  text-transform:uppercase;color:var(--text-soft);margin-bottom:.8rem}
.catnav__all{margin-top:1rem}

/* The margin note under /samples' shelf, for the teacher whose workbook has a
   preview but not yet the whole set. Quiet on purpose — it is the answer to a
   question the page has already mostly answered, not a second call to action —
   so it borrows the empty state's voice rather than the buttons'. */
.samples__ask{margin-top:2rem;max-width:62ch;font-size:.95rem;line-height:1.6;color:var(--text-soft)}
.samples__ask a{color:var(--purple);text-decoration:underline;text-underline-offset:3px}

/* The no-JS cap. finder.js removes .is-capped on load and caps the matching
   set instead — the number here has to match app.HOME_CARDS, which is what
   sets data-cap on the grid. */
.finder__grid.is-capped>.card:nth-child(n+13){display:none}

@media (max-width:640px){
  .finder__search{flex-wrap:wrap}
  .finder__search input{flex:1 1 100%}
  .pick{flex:1 1 100%}
}

/* ========================================================================
   23 · HOME · EDUCATION NEWS STRIP
   ======================================================================== */
.edstrip{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(1rem,2vw,1.5rem)}
.edtease{--c:var(--blue);display:flex;flex-direction:column;gap:.5rem;background:var(--paper-card);
  border:1.5px solid var(--line);border-top:4px solid var(--c);border-radius:var(--r);
  padding:1rem 1.1rem 1.2rem;box-shadow:var(--shadow);transition:transform .16s,box-shadow .16s}
.edtease:hover{transform:translateY(-4px);box-shadow:var(--shadow-h)}
.edtease img{width:100%;aspect-ratio:3/2;object-fit:cover;border-radius:4px;
  background:color-mix(in srgb,var(--c) 8%,#fff)}
.edtease__cat{font-weight:700;font-size:.68rem;letter-spacing:.12em;text-transform:uppercase;color:var(--c)}
.edtease h3{font-size:1.05rem;line-height:1.16;color:var(--ink)}
.edtease p{font-size:.88rem;line-height:1.45;color:var(--text-soft)}

@media (max-width:900px){ .edstrip{grid-template-columns:1fr} }

/* ========================================================================
   24 · ABOUT  ·  one narrative, one column
   ------------------------------------------------------------------------
   This page is an essay, so it is set as one: a single measure of prose that
   runs from the first line to the last without a break in it, and a rail down
   the left carrying everything that would otherwise interrupt — the section
   labels, the founders' names, the two numbers.

   Nothing here is a card and nothing alternates background. A card says "these
   are four separate things"; this is one thing said in order. The rail is what
   makes that possible: an aside can sit *beside* the sentence it belongs to
   instead of stopping it.
   ======================================================================== */
.about__hero{background:var(--paper-2);border-bottom:1.5px solid var(--line);
  padding-block:clamp(2.6rem,6vw,4.4rem)}
/* The band is full-bleed; the words inside it are not — they ride the story's
   column so the headline and the first paragraph share a left edge. */
.about__hero .story{padding-block:0}
.about__hero .story__rail{padding-top:.7rem}
.about__h1{margin:.6rem 0 1.15rem;color:var(--ink);max-width:20ch;font-size:clamp(2rem,4.6vw,3.3rem)}
.about__lede{font-size:clamp(1.06rem,1.65vw,1.28rem);color:var(--text);max-width:56ch;line-height:1.58}
.about__cta{display:flex;gap:.8rem;flex-wrap:wrap;margin-top:2.4rem}

/* The glance row · the four facts, for the visitor who reads the headline and
   then leaves. It takes the rail as well as the column because it summarises
   the whole page rather than any one paragraph, and it sits on a rule instead
   of in four boxes — the essay has no cards in it and this is not the place to
   introduce them. */
.glance{grid-column:1/-1;list-style:none;margin-top:clamp(1rem,2.2vw,1.6rem);
  border-top:1.5px solid var(--line);
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr))}
.glance li{padding:1.1rem .9rem 0 0}
.glance li+li{padding-left:1.4rem;border-left:1.5px solid var(--line)}
.glance b{display:block;font-family:var(--display);font-weight:800;line-height:1;
  letter-spacing:-.035em;color:var(--green-ink);font-size:clamp(1.5rem,2.9vw,2.1rem)}
/* A word standing where a number stands: same weight, stepped down so the
   figures stay the loudest thing in the row. */
.glance .glance__word{color:var(--ink);font-size:clamp(1.15rem,2.1vw,1.55rem);letter-spacing:-.028em}
.glance span{display:block;margin-top:.42rem;font-family:var(--hand);font-weight:500;
  font-size:.9rem;line-height:1.3;color:var(--text-soft)}

/* --- the professional-development panel ---------------------------------- */
/* The one bordered thing on the page. It is addressed to a department head
   rather than to a reader, so it stops being prose and looks like it. */
.pd{margin-top:.6rem;background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);box-shadow:var(--shadow);padding:clamp(1.4rem,3vw,1.9rem)}
/* Two classes deep so it outranks .story p's bottom margin without !important. */
.pd .pd__lede{color:var(--text);margin-bottom:1.35rem}
.pd__who{list-style:none;display:flex;flex-wrap:wrap;gap:.9rem 2.6rem;
  padding-top:1.2rem;border-top:1.5px solid var(--line)}
.pd__who li{display:flex;align-items:baseline;gap:.7rem}
.pd__name{font-family:var(--display);font-weight:800;font-size:1.25rem;
  letter-spacing:-.028em;color:var(--c,var(--ink))}
.pd__who a{font-weight:600}

/* --- the column and its rail --------------------------------------------- */
/* The measure is the whole layout: the rail and the gap are added to it rather
   than taken out of it, so the prose is the same width whatever is alongside. */
.story{--rail:11rem;--gap:2.8rem;--measure:63ch;
  max-width:calc(var(--rail) + var(--gap) + var(--measure));
  margin-inline:auto;padding-inline:var(--gut);padding-block:clamp(2.6rem,6vw,4rem)}
.story__part{display:grid;grid-template-columns:var(--rail) minmax(0,1fr);column-gap:var(--gap)}
.story__rail{grid-column:1;grid-row:1;text-align:right;padding-top:.45rem}
.story__text{grid-column:2;grid-row:1}

/* One paragraph's worth of air between parts, the same as between paragraphs.
   A larger gap here is what would turn the parts back into sections. */
.story__part+.story__part{margin-top:1.2rem}
.story p{line-height:1.72;margin-bottom:1.2rem}
.story__text>*:last-child{margin-bottom:0}
.story a{color:var(--green-ink);text-decoration:underline;text-underline-offset:3px;font-weight:600}
.story a:hover{color:var(--purple)}
/* The run-in lead: a claim in display type that the evidence continues from,
   in the same paragraph. A heading would break the column; this doesn't. */
.story__runin{font-family:var(--display);font-weight:800;letter-spacing:-.015em;color:var(--ink)}

/* Rail · the section label. Deliberately quiet — a signpost for someone
   scanning, not a headline for someone reading. */
.story__label{font-family:var(--hand);font-size:1.06rem;line-height:1.3;color:var(--text-soft)}

/* Rail · a founder's name, standing exactly where a label would. Three people
   introduced without three boxes to put them in. */
.story__name{font-family:var(--display);font-weight:800;font-size:1.55rem;letter-spacing:-.028em;
  line-height:1;color:var(--ink)}
.story__name small{display:block;margin-top:.34rem;font-family:var(--hand);font-weight:500;
  font-size:.88rem;letter-spacing:0;color:var(--c,var(--purple))}

/* Rail · one of the two numbers, beside the sentence that earns it rather than
   in a panel of statistics. */
.story__fig{font-family:var(--display);font-weight:800;font-size:clamp(1.6rem,2.9vw,2.2rem);
  letter-spacing:-.035em;line-height:1;color:var(--green-ink)}
.story__fig small{display:block;margin-top:.34rem;font-family:var(--hand);font-weight:500;
  font-size:.9rem;letter-spacing:0;color:var(--text-soft)}

/* The line the catalog is built on. The one place the measure widens — it
   takes the rail as well as the column — and then the prose picks up under it.
   No band, no box: the narrative raises its voice and carries on. */
.story__creed{grid-column:1/-1;margin:2rem 0 1.7rem}
.story__creed q{quotes:none;display:block;font-family:var(--display);font-weight:800;
  font-size:clamp(1.45rem,3.1vw,2.1rem);letter-spacing:-.03em;line-height:1.14;color:var(--ink)}
.story__creed em{font-style:normal;color:var(--green-ink);
  box-shadow:inset 0 -.4em 0 color-mix(in srgb,var(--green) 24%,transparent)}
.story__creed p{margin:1.05rem 0 0;color:var(--text-soft);max-width:56ch}

/* Under the rail's width the aside has nowhere to hang, so it becomes a line
   above the paragraph it belongs to — still an aside, just stacked. */
@media (max-width:900px){
  .story{--rail:0rem;--gap:0rem}
  .story__part{grid-template-columns:minmax(0,1fr)}
  .story__rail{grid-column:1;grid-row:auto;text-align:left;padding-top:0;margin-bottom:.6rem}
  .story__text{grid-column:1;grid-row:auto}
  .story__part+.story__part{margin-top:2rem}
  .story__fig{display:flex;align-items:baseline;gap:.65rem}
  .story__fig small{margin-top:0}
  .story__name small{display:inline;margin-left:.5rem}
}

/* Four across is one word per line by this width, so the row folds to two and
   the rule that separated the columns has to run between the new rows too. */
@media (max-width:780px){
  .glance{grid-template-columns:repeat(2,minmax(0,1fr))}
  .glance li+li{padding-left:0;border-left:0}
  .glance li:nth-child(2n){padding-left:1.4rem;border-left:1.5px solid var(--line)}
  .glance li:nth-child(n+3){margin-top:1.1rem;border-top:1.5px solid var(--line)}
}

/* ========================================================================
   25 · RESOURCES  ·  the page a dead QR code lands on
   ------------------------------------------------------------------------
   Read by somebody standing at a copier with a workbook open, who scanned a
   code and got nothing. So this band is sized for one glance: what went
   wrong, where the thing they want actually is, and what to do about it —
   above the fold, in that order, before any browsing starts.

   Which is why the measure is narrow and the type is large. Narrow by capping
   each line rather than by centring a column: the band sits directly on top of
   the finder's grid, and the two share the page's left edge so the eye runs
   straight down from "find your workbook" into the search box.

   The finder under it is the same component /catalog and /samples use and
   needs no styles of its own; everything here belongs to the explanation.
   ======================================================================== */
.rez{background:var(--paper-2);border-bottom:1.5px solid var(--line);
  padding-block:clamp(2.2rem,5.5vw,3.8rem)}

/* The dead link, said first and said plainly. Red edge because this is the one
   line on the page reporting a fault; everything under it is the answer. */
.rez__dead{display:flex;align-items:flex-start;gap:.85rem;max-width:64ch;background:var(--paper-card);
  border:1.5px solid var(--line);border-left:4px solid var(--red);border-radius:var(--r);
  padding:.9rem 1.1rem;margin-bottom:1.5rem;font-size:.95rem;color:var(--text-soft);line-height:1.5}
.rez__dead b{color:var(--ink);font-family:var(--display);font-weight:800;letter-spacing:-.015em}
.rez__qr{flex:none;width:1.55rem;height:1.55rem;fill:var(--ink);opacity:.85;margin-top:.05rem}

.rez__h1{margin:.55rem 0 1rem;color:var(--ink);max-width:18ch;
  font-size:clamp(1.95rem,4.4vw,3.1rem);line-height:1.03}
/* Same underline the About page's creed uses: the four words that are the
   whole message, marked without a second colour or a second type size. */
.rez__h1 em{font-style:normal;
  box-shadow:inset 0 -.42em 0 color-mix(in srgb,var(--green) 26%,transparent)}
.rez__lede{font-size:clamp(1.02rem,1.5vw,1.18rem);color:var(--text);max-width:58ch;line-height:1.6}

/* The one thing to do, given room to be the one thing. The numbered steps, the
   finder and the margin note that used to sit here went with the finder — the
   page is a statement and this button now. */
.rez__go{margin-top:1.9rem}

/* ========================================================================
   25b · PUBLIC · CONTACT
   ------------------------------------------------------------------------
   The page borrows .rez for its band and .field/.alert from the auth screens
   for its inputs, so what is left here is the card the form sits in and the
   one rule that has to exist: the honeypot's.
   ======================================================================== */
/* THE CENTRED COLUMN. Applied to the section, not to .rez itself — /resources
   and /samples share that class and are left-aligned prose with a button,
   which is the right shape for a statement and the wrong one for a form.
   max-width:none on the heading because 18ch is a measure chosen to give the
   left-aligned version a deliberate two-line break, and a centred heading
   broken at the same point looks accidental rather than composed. */
.rez--mid{text-align:center}
.rez--mid .rez__h1{max-width:none}
.rez--mid .rez__lede{margin-inline:auto}

/* The card. margin-inline:auto is the centring; the max-width is a measure,
   not a breakpoint — 560px keeps the label/input pairs from running wider than
   the eye tracks comfortably, and below that it is simply the column width, so
   there is no phone-sized case to handle separately. */
.contact{max-width:560px;margin:2rem auto 0}
.contact__form,.contact__done{background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);box-shadow:var(--shadow);padding:clamp(1.4rem,4vw,2rem)}
/* Fields go back to the left inside the centred column. A centred label above
   a full-width input has nothing to line the eye up on, and a form of them
   reads as a poster; the confirmation that replaces it stays centred, because
   that one IS a statement. */
.contact__form{text-align:left}
/* The first .field in the card would otherwise inherit the 1.2rem top margin
   that spaces it from the one above it, and sit oddly low under the border. */
.contact__form .field:first-of-type{margin-top:0}
.contact textarea{resize:vertical;min-height:9rem;line-height:1.55}
/* Full width and given room: it is the one button on the page. */
.contact__form .btn{width:100%;justify-content:center;margin-top:1.6rem}
/* The sum sits apart from the three real fields — it is a door check, not part
   of the message, and running it straight on from "Your message" reads as a
   fourth thing we are asking for. */
.contact__sum{margin-top:1.6rem;padding-top:1.3rem;border-top:1.5px dashed var(--line)}
.contact__sum input{max-width:9rem}
.contact__alt{margin-top:1.1rem;font-size:.85rem;color:var(--text-soft);text-align:center}
/* The reset makes every <a> inherit its colour and drop its underline, which
   is right for nav and cards and wrong for a mail address inside a sentence:
   on the confirmation it rendered as plain prose, so the one remaining way to
   reach us did not look like anything you could click. Both the fallback under
   the form and the one in the sent message are marked here.

   :not(.btn) because the confirmation's "Send another" is an <a> wearing a
   button, and marking it up as a link put blue underlined text on a green
   pill. Only prose links are meant here. */
.contact__alt a,.contact__done a:not(.btn){color:var(--blue);font-weight:600;
  text-decoration:underline;text-underline-offset:2px}

/* THE HONEYPOT, and the only rule on this page that is load-bearing.
   Off-screen rather than display:none — a bot sophisticated enough to check
   computed styles skips a display:none field, and one that is merely absent
   from the viewport looks like any other input to it. Height and opacity are
   belt and braces for a browser that ignores the position. If this rule is
   ever lost the field becomes visible and every visitor is asked for their
   website, which is the failure mode to watch for. */
.contact__trap{position:absolute;left:-9999px;top:auto;width:1px;height:1px;
  overflow:hidden;opacity:0;pointer-events:none}

.contact__done h2{font-size:1.5rem;color:var(--ink);margin:.7rem 0 .6rem;letter-spacing:-.02em}
.contact__done p{color:var(--text);line-height:1.6}
.contact__again{margin-top:1.5rem}

/* ========================================================================
   26 · ADMIN · ANALYTICS
   ------------------------------------------------------------------------
   Two colours carry data on these screens and no others: blue for page views,
   green for the visits inside them. They were checked for colour-blind
   separation before being chosen (ΔE 42 under deuteranopia, 48 under normal
   vision, against the card white) — the pair that reads as two things to
   everyone, not just to most people.

   Everything else here is length, not hue. The bar behind a table row is share
   of the biggest row; it is one colour on every table because the colour is not
   saying anything. Nothing on these screens is encoded by colour alone: the
   chart carries a legend and a per-column readout, and every bar sits in a
   table cell next to its own number.
   ======================================================================== */

/* --- the bar above every screen: tabs on the left, range on the right ---- */
.anbar{display:flex;justify-content:space-between;align-items:center;gap:1rem;
  flex-wrap:wrap;margin:0 0 1.6rem}

.tabnav{display:flex;gap:1.1rem;flex-wrap:wrap}
.tabnav a{font-weight:700;font-size:.95rem;color:var(--text-soft);
  padding:.35rem 0;border-bottom:2.5px solid transparent}
.tabnav a:hover{color:var(--ink)}
.tabnav a.is-on{color:var(--ink);border-bottom-color:var(--green)}

.anbar__ranges{display:flex;gap:.4rem;flex-wrap:wrap}
.chip{font-weight:600;font-size:.8rem;color:var(--text-soft);background:var(--paper-card);
  border:1.5px solid var(--line);border-radius:99px;padding:.34em .82em;white-space:nowrap}
.chip:hover{border-color:var(--blue);color:var(--ink)}
.chip.is-on{background:var(--ink);border-color:var(--ink);color:#fff}
/* The bot toggle is not a range and should not read as one of them. */
.chip--ghost{border-style:dashed}

/* --- layout helpers ----------------------------------------------------- */
.cols2{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(380px,100%),1fr));
  gap:clamp(1.2rem,2.4vw,2rem);align-items:start;margin-top:1.4rem}
.cols3{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));
  gap:clamp(1.2rem,2.4vw,2rem);align-items:start;margin-top:1.6rem}
.muted{color:var(--text-soft);font-size:.88rem;line-height:1.5}
.muted code{font-size:.85em;background:var(--paper-2);padding:.1em .4em;border-radius:4px;
  word-break:break-all}

/* Up is not automatically good — a rise in 404s is a rise in broken links — so
   the arrow and the colour say direction, and the label above says what of. */
.delta{display:block;font-family:var(--body);font-weight:600;font-size:.76rem;
  letter-spacing:0;margin-top:.35rem;color:var(--text-soft)}
.delta--up{color:var(--green-d)}
.delta--down{color:var(--red)}

/* --- the time series ---------------------------------------------------- */
.chart{--chart-h:190px;margin:0}
.chart__legend{display:flex;align-items:center;gap:1.1rem;flex-wrap:wrap;
  font-size:.8rem;color:var(--text-soft);margin-bottom:.8rem}
.chart__key{display:inline-flex;align-items:center;gap:.4rem;font-weight:600}
.chart__swatch{width:.85rem;height:.85rem;border-radius:3px;display:inline-block}
.chart__swatch--views{background:var(--blue)}
.chart__swatch--sessions{background:var(--s-science)}
.chart__peak{margin-left:auto;font-variant-numeric:tabular-nums}

.chart__plot{display:flex;align-items:flex-end;gap:2px;height:var(--chart-h);
  border-bottom:1.5px solid var(--line)}

/* The column is the hit target, not the bar: a day with two views is three
   pixels tall, and a tooltip you can only reach by hitting three pixels is a
   tooltip for the days that were already easy to read. */
.chart__col{position:relative;flex:1 1 0;min-width:2px;height:100%}
.chart__col:focus-visible{outline:none;background:color-mix(in srgb,var(--blue) 8%,transparent)}

.chart__bar{position:absolute;bottom:0;border-radius:4px 4px 0 0;
  background:var(--blue);transition:filter .12s}
.chart__bar--views{left:0;right:0;background:color-mix(in srgb,var(--blue) 82%,#fff)}
/* Visits are a subset of views, so they sit inside rather than beside: at
   ninety days a pair of side-by-side bars is two grey hairs. The 2px inline
   borders in the card colour are the gap that keeps the two marks readable as
   two marks where they overlap. */
.chart__bar--sessions{left:28%;right:28%;background:var(--s-science);
  border-inline:2px solid var(--paper-card)}
.chart__col:hover .chart__bar{filter:brightness(.9)}

.chart__tip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
  z-index:5;opacity:0;pointer-events:none;transition:opacity .12s;
  background:var(--ink);color:#fff;font-size:.78rem;line-height:1.45;
  padding:.5rem .7rem;border-radius:var(--r);white-space:nowrap;
  box-shadow:var(--shadow)}
.chart__tip strong{display:block;font-weight:700;color:var(--green-hi)}
.chart__col:hover .chart__tip,.chart__col:focus-visible .chart__tip{opacity:1}
/* The first and last columns' readouts would hang off the panel. */
.chart__col:first-child .chart__tip{left:0;transform:none}
.chart__col:last-child .chart__tip{left:auto;right:0;transform:none}

/* One label per bar, but only every nth is filled in — the empties are spacers
   that keep the rest sitting under the bar they belong to. */
.chart__axis{display:flex;gap:2px;margin-top:.5rem}
.chart__label{flex:1 1 0;min-width:0;text-align:center;overflow:visible;
  font-size:.68rem;color:var(--text-soft);white-space:nowrap}

/* --- tables with a share bar behind the first column -------------------- */
.table td.num,.table th.num{text-align:right;white-space:nowrap;
  font-variant-numeric:tabular-nums}
.table--meter td:first-child{position:relative;min-width:18rem}
.meter{position:absolute;left:0;top:0;bottom:0;width:var(--w,0%);
  background:color-mix(in srgb,var(--blue) 9%,transparent);
  border-right:2px solid color-mix(in srgb,var(--blue) 34%,transparent)}
.meter__name{position:relative;display:block;word-break:break-word}
.meter__name small{display:block;color:var(--text-soft);font-size:.78rem}
.meter__name .pill{margin-left:.4rem;vertical-align:1px}

/* Link kinds. Buy is the only one that gets a colour with weight behind it —
   it is the one row on the page that is nearly a sale. */
.pill--buy{background:color-mix(in srgb,var(--green) 18%,#fff);color:var(--green-ink)}
.pill--outbound{background:color-mix(in srgb,var(--purple) 12%,#fff);color:var(--purple)}
.pill--internal{background:var(--paper-2);color:var(--text-soft)}
.pill--download,.pill--mail,.pill--tel,.pill--anchor{background:var(--paper-2);color:var(--text-soft)}

/* A crawler's row on the live feed is still readable, just clearly not a person. */
.table tr.is-muted td{color:var(--text-soft);background:color-mix(in srgb,var(--paper-2) 30%,transparent)}

/* --- how much of the traffic survives each filter ----------------------- */
/* One bar, four segments, drawn in descending confidence so the eye reads it
   left to right as "everything, then what is left, then what is left of that".
   The gaps between the segments are the number worth looking at, which is why
   this is one bar and not four tiles. */
.trust{display:flex;height:1.4rem;border-radius:6px;overflow:hidden;
  border:1.5px solid var(--line);background:var(--paper-2);margin:.55rem 0 .7rem}
.trust__seg{position:relative;min-width:2px}
.trust__seg--verified{background:var(--s-science)}
.trust__seg--human{background:color-mix(in srgb,var(--blue) 55%,#fff)}
.trust__seg--bot{background:color-mix(in srgb,var(--ink) 22%,#fff)}
.trust__keys{display:flex;gap:1.1rem;flex-wrap:wrap;font-size:.8rem;color:var(--text-soft)}
.trust__key{display:inline-flex;align-items:center;gap:.4rem;font-weight:600}
.trust__key i{width:.85rem;height:.85rem;border-radius:3px;display:inline-block}

/* The looser count, under the headline it is a range with — every row is_bot
   did not catch, against the browser-verified rows above it. Deliberately quiet
   and deliberately present: a reader who wants the old, larger number should
   find it without going looking, and should never mistake it for the headline.
   Same colour as the trust bar's middle segment, so the tile and the bar below
   it are visibly talking about the same population. */
.tile dd small.tile__also{margin-top:.2rem;font-weight:600;font-size:.74rem;
  color:color-mix(in srgb,var(--text-soft) 78%,transparent)}

/* --- the state grid map -------------------------------------------------- */
/* Eleven by eight squares, one per state plus DC. Equal squares on purpose:
   a district in Rhode Island buying a class set is the same fact as one in
   Montana, and a geographic map would render the first as three pixels. */
.usmap{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));
  grid-template-rows:repeat(8,auto);gap:clamp(3px,.5vw,6px);
  max-width:46rem;margin:.4rem 0 1rem}
.usmap__cell{position:relative;aspect-ratio:1;border-radius:6px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  font-size:clamp(.55rem,1.1vw,.72rem);line-height:1.05;font-weight:700;
  border:1.5px solid var(--line);background:var(--paper-2);color:var(--text-soft)}
.usmap__cell:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
.usmap__n{font-family:var(--body);font-weight:600;font-size:.86em;opacity:.9;
  font-variant-numeric:tabular-nums}

/* Five steps rather than a continuous ramp: a colour a reader has to measure
   against a gradient is a colour they will misread, and five can be told apart
   at a glance. Text flips to white where the fill is dark enough to need it. */
.usmap__cell--l0{background:var(--paper-2)}
.usmap__cell--l1{background:color-mix(in srgb,var(--blue) 16%,#fff);color:var(--ink);
  border-color:color-mix(in srgb,var(--blue) 24%,transparent)}
.usmap__cell--l2{background:color-mix(in srgb,var(--blue) 38%,#fff);color:var(--ink);
  border-color:color-mix(in srgb,var(--blue) 46%,transparent)}
.usmap__cell--l3{background:color-mix(in srgb,var(--blue) 66%,#fff);color:#fff;
  border-color:color-mix(in srgb,var(--blue) 72%,transparent)}
.usmap__cell--l4{background:var(--blue);color:#fff;border-color:var(--blue)}

.usmap__tip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
  z-index:6;opacity:0;pointer-events:none;transition:opacity .12s;
  background:var(--ink);color:#fff;font-size:.76rem;font-weight:500;line-height:1.45;
  padding:.45rem .65rem;border-radius:var(--r);white-space:nowrap;box-shadow:var(--shadow)}
.usmap__tip strong{display:block;font-weight:700;color:var(--green-hi)}
.usmap__cell:hover .usmap__tip,.usmap__cell:focus-visible .usmap__tip{opacity:1}
/* The far-left and far-right columns' readouts would hang off the panel. */
.usmap__cell[style*="grid-column:1"] .usmap__tip{left:0;transform:none}
.usmap__cell[style*="grid-column:11"] .usmap__tip{left:auto;right:0;transform:none}

.usmap__legend{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;
  font-size:.78rem;color:var(--text-soft)}
.usmap__swatch{width:1.1rem;height:1.1rem;border-radius:4px;display:inline-block;
  border:1.5px solid var(--line)}
.usmap__note{margin-left:auto;font-style:italic}

/* --- the assistant's conversations -------------------------------------- */

/* Search and paging. Both are plain GET furniture — a search is a URL and a
   page is a URL — so neither needs to be anything more than a row. */
.anfind{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}
.anfind input[type=search]{flex:1 1 22rem;max-width:34rem;padding:.55rem .8rem;
  border:1px solid var(--rule);border-radius:8px;background:var(--paper-card);
  font:inherit;color:var(--ink)}
.anfind input[type=search]:focus{outline:none;border-color:var(--blue)}
.anpage{display:flex;gap:.8rem;align-items:center;margin-top:1rem}

/* A transcript. Read down the left edge and the visitor's turns are the ones
   that stand out — they are what the reader came for, and the assistant's
   answer is only interesting next to the question that produced it. */
.tscript{display:flex;flex-direction:column;gap:.9rem}
.tturn{border:1px solid var(--rule);border-radius:10px;padding:.8rem 1rem;
  background:var(--paper-card);border-left:4px solid var(--rule)}
.tturn--visitor{border-left-color:var(--blue);
  background:color-mix(in srgb,var(--blue) 4%,var(--paper-card))}
.tturn--assistant{border-left-color:var(--s-science)}
/* A refused turn is marked on the turn itself, not only in a pill: scanning a
   long transcript for the one the moderation screen stopped is the reason
   anybody opens one of these twice. */
.tturn--blocked{border-left-color:var(--s-cte);
  background:color-mix(in srgb,var(--s-cte) 6%,var(--paper-card))}
.tturn__hd{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;
  margin-bottom:.5rem;font-size:.8rem}
.tturn__cost{margin-left:auto;font-variant-numeric:tabular-nums;font-size:.72rem}
/* Preformatted, and wrapped: exactly the characters that were stored, with the
   line breaks the model wrote, and no horizontal scrollbar to read them by. */
.tturn__body{margin:0;font:inherit;white-space:pre-wrap;word-break:break-word}

@media (max-width:760px){
  .anbar{flex-direction:column;align-items:flex-start}
  .chart{--chart-h:150px}
  .chart__tip{white-space:normal;min-width:11rem}
  .tturn__cost{margin-left:0;width:100%}
}

/* --------------------------------------------------------------------------
   AD DESIGN AND THE MEDIA LIBRARY

   Four components. Everything else on these screens is .panel, .field, .btn
   and .kv, which is why there is so little here: an ad workspace is a form and
   a lot of pictures, and only the pictures needed new rules.

   Every image is a plain <img> inside a plain <a> with nothing layered over it,
   deliberately. An overlay that catches the pointer is what breaks right-click
   → Save image as, and these are files people are meant to take away.
   -------------------------------------------------------------------------- */

/* The library grid, and the campaign list. auto-fill rather than auto-fit so a
   single result stays card-sized instead of stretching across the page. */
.medias{display:grid;gap:clamp(.8rem,1.6vw,1.1rem);
  grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr))}
.media{display:flex;flex-direction:column;background:var(--paper-card);
  border:1.5px solid var(--line);border-radius:var(--r);overflow:hidden}
/* Checkerboard behind every thumbnail, so a transparent logo reads as
   transparent rather than as a white rectangle on a white card. */
.media__shot{display:block;aspect-ratio:1;background:
  linear-gradient(45deg,var(--paper-2) 25%,transparent 25%,transparent 75%,var(--paper-2) 75%) 0 0/16px 16px,
  linear-gradient(45deg,var(--paper-2) 25%,transparent 25%,transparent 75%,var(--paper-2) 75%) 8px 8px/16px 16px,
  var(--paper-card)}
.media__shot img{width:100%;height:100%;object-fit:contain}
.media__empty{display:flex;align-items:center;justify-content:center;height:100%;
  font-size:.8rem;color:var(--text-soft)}
.media__body{padding:.7rem .8rem;display:flex;flex-direction:column;gap:.25rem}
.media__body p{margin:0}
.media__body .panel__foot{padding-top:.5rem;gap:.5rem}
.media__more summary{cursor:pointer;padding:.4rem 0}
.media__more .field{margin-top:.6rem}

/* The cover picker. A checkbox that is a picture: the input itself is hidden
   and the label's border does the work, because a 14px tick next to a 200px
   cover is not where anybody aims. */
.picker{display:grid;gap:.6rem;margin-top:.9rem;
  grid-template-columns:repeat(auto-fill,minmax(min(120px,100%),1fr))}
.picker__item{position:relative;display:flex;flex-direction:column;gap:.4rem;
  padding:.5rem;border:2px solid var(--line);border-radius:var(--r);
  cursor:pointer;background:var(--paper-card);transition:border-color .12s}
.picker__item:hover{border-color:var(--text-soft)}
.picker__item img{width:100%;aspect-ratio:3/4;object-fit:contain}
.picker__item span{font-size:.74rem;line-height:1.3;color:var(--text-soft)}
.picker__item input{position:absolute;inset:.4rem auto auto .4rem;accent-color:var(--green)}
.picker__item:has(input:checked){border-color:var(--green);
  background:color-mix(in srgb,var(--green) 7%,var(--paper-card))}
.picker__item:has(input:checked) span{color:var(--ink);font-weight:600}
/* Once five are ticked the rest go quiet rather than disappearing — the limit
   is 1-5 covers and a greyed sixth explains itself; a vanished one does not. */
.picker[data-full] .picker__item:not(:has(input:checked)){opacity:.4}

/* The draft strip. One wide row per draft: the picture on the left at whatever
   shape it was drawn, the prompt that made it and the buttons on the right. */
.chain{display:flex;flex-direction:column;gap:1rem}
.draft{display:grid;grid-template-columns:minmax(200px,300px) 1fr;gap:1.2rem;
  background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);padding:1rem}
.draft--master{border-color:var(--green);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 14%,transparent)}
.draft__shot{display:block;align-self:start}
.draft__shot img{width:100%;border-radius:var(--r);border:1px solid var(--line)}
.draft__body{min-width:0}
.draft__body .field{margin-top:.6rem}
.variant{display:block;margin-top:.8rem}
.variant img{width:100%;border-radius:var(--r);border:1px solid var(--line)}

.libbar{display:flex;gap:.7rem;align-items:flex-end;flex-wrap:wrap}
.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* A generation is a synchronous 40-90 second request. The button says so and
   stops taking presses, because the failure mode otherwise is three identical
   ads, each paid for separately. */
[data-busy] button[disabled]{opacity:.55;cursor:progress}

@media (max-width:760px){
  .draft{grid-template-columns:1fr}
}

/* --------------------------------------------------------------------------
   THE AD BOARD

   A two-column workspace: a rail you type in and a canvas you look at. It
   breaks out of .wrap to full width, because a board bounded by a reading
   measure is a board that fits two ads.
   -------------------------------------------------------------------------- */

.studio{
  /* Full-bleed escape from the centred admin column. */
  width:100vw;margin-left:calc(50% - 50vw);
  display:grid;grid-template-columns:minmax(300px,360px) 1fr;
  height:calc(100vh - 210px);min-height:520px;
  border-top:1.5px solid var(--line);border-bottom:1.5px solid var(--line);
  background:var(--paper-2);position:relative}

/* --- the rail --- */
.rail{overflow-y:auto;overflow-x:hidden;padding:1rem 1.1rem 4rem;
  background:var(--paper-card);border-right:1.5px solid var(--line)}
.rail__block{border-bottom:1px dashed var(--line);padding:.7rem 0}
.rail__block summary{cursor:pointer;display:flex;align-items:center;
  justify-content:space-between;gap:.6rem;list-style:none}
.rail__block summary::-webkit-details-marker{display:none}
.rail__block summary::after{content:'▸';color:var(--text-soft);font-size:.8rem;
  transition:transform .15s}
.rail__block[open] summary::after{transform:rotate(90deg)}
.rail__block .field{margin-top:.8rem}
.rail__block textarea{resize:vertical;min-height:3.4rem}
.rail__selection{border-bottom:none}
.rail__actions{display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;
  margin-top:.8rem}

/* A denser picker than the one on a full-width form: the rail is 360px and
   these are recognised by their artwork, not read. */
.picker--rail{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:.4rem}
.picker--rail .picker__item{padding:.3rem;gap:.25rem}
.picker--rail .picker__item span{font-size:.66rem;line-height:1.2;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.picker--rail .picker__item img{aspect-ratio:3/4}

/* --- the layout buttons --- */
.layouts{display:grid;gap:.4rem;margin-top:.8rem}
.layout{display:grid;grid-template-columns:34px 1fr;gap:.6rem;align-items:center;
  text-align:left;padding:.5rem .6rem;border:1.5px solid var(--line);
  border-radius:var(--r);background:var(--paper-card);cursor:pointer;
  transition:border-color .12s,background .12s}
.layout:hover{border-color:var(--green);
  background:color-mix(in srgb,var(--green) 6%,var(--paper-card))}
.layout__name{font-weight:700;font-size:.86rem;color:var(--ink);grid-column:2}
.layout__blurb{grid-column:2;font-size:.72rem;color:var(--text-soft);line-height:1.3}
/* Each shape is a two-line diagram of what the layout does, drawn in CSS so
   there are no seven more image requests on a page that already has plenty. */
.layout__shape{grid-row:1 / span 2;width:34px;height:34px;border-radius:4px;
  background:var(--paper-2);border:1px solid var(--line);position:relative}
.layout__shape::before,.layout__shape::after{content:'';position:absolute;
  background:var(--text-soft);opacity:.5;border-radius:1px}
.layout--hero::before{inset:32% 30%}
.layout--fan::before{inset:45% 18% 20% 12%}
.layout--fan::after{inset:38% 30% 28% 24%}
.layout--grid::before{inset:20% 20% 52% 20%}
.layout--grid::after{inset:52% 20% 20% 20%}
.layout--desk::before{inset:50% 22% 22% 16%}
.layout--desk::after{inset:22% 55% 60% 22%}
.layout--quote::before{inset:26% 16% 56% 16%}
.layout--quote::after{inset:70% 60% 14% 16%}
.layout--bundle::before{inset:14% 16% 58% 16%}
.layout--bundle::after{inset:56% 26% 18% 26%}
.layout--banner::before{inset:40% 55% 40% 10%}
.layout--banner::after{inset:34% 12% 34% 62%}

/* --- the canvas --- */
.canvas{position:relative;overflow:hidden;cursor:grab;
  background:
    radial-gradient(circle at 1px 1px,color-mix(in srgb,var(--ink) 14%,transparent) 1px,transparent 0)
    0 0/24px 24px,var(--paper-2)}
.canvas.is-panning{cursor:grabbing}
.canvas:focus-visible{outline:2px solid var(--green);outline-offset:-2px}
.canvas__inner{position:absolute;inset:0;transform-origin:0 0;will-change:transform}
.canvas__empty{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;text-align:center;padding:2rem;
  pointer-events:none;max-width:26rem;margin:auto}
.canvas__hint{position:absolute;left:1rem;bottom:.7rem;margin:0;
  pointer-events:none;opacity:.65}

.zoombar{position:absolute;right:1rem;bottom:1rem;display:flex;gap:.25rem;
  align-items:center;background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:99px;padding:.25rem .3rem;box-shadow:0 2px 10px rgba(0,0,0,.08)}
.zoombar button{width:2rem;height:2rem;border-radius:50%;font-weight:700;
  color:var(--ink);background:transparent;cursor:pointer;border:none;font-size:.9rem}
.zoombar button:hover{background:var(--paper-2)}
.zoombar__level{font-size:.74rem;color:var(--text-soft);padding:0 .5rem;
  font-variant-numeric:tabular-nums;min-width:3.2rem;text-align:right}

/* --- an artboard --- */
.tile{position:absolute;top:0;left:0;background:var(--paper-card);
  border:1.5px solid var(--line);border-radius:var(--r);overflow:hidden;
  box-shadow:0 1px 3px rgba(0,0,0,.06);transition:border-color .12s,box-shadow .12s}
.tile.is-selected{border-color:var(--green);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 22%,transparent)}
.tile.is-dragging{box-shadow:0 12px 30px rgba(0,0,0,.18);z-index:5}
.tile.is-failed{border-color:var(--s-cte)}
.tile__bar{display:flex;align-items:baseline;justify-content:space-between;
  gap:.5rem;padding:.4rem .6rem;cursor:grab;background:var(--paper-2);
  border-bottom:1px solid var(--line)}
.tile.is-dragging .tile__bar{cursor:grabbing}
.tile__name{font-weight:700;font-size:.8rem;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tile__size{font-size:.68rem;color:var(--text-soft);
  font-variant-numeric:tabular-nums;white-space:nowrap}
.tile__frame{position:relative;display:flex;align-items:center;
  justify-content:center;background:var(--paper)}
.tile__img{width:100%;height:100%;object-fit:contain;display:block}
.tile__placeholder{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;font-size:.78rem;color:var(--text-soft);
  letter-spacing:.04em;text-transform:uppercase}
.tile__spinner{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:.6rem;
  background:color-mix(in srgb,var(--paper-card) 80%,transparent)}
.tile__spinner span{width:26px;height:26px;border-radius:50%;
  border:3px solid color-mix(in srgb,var(--ink) 15%,transparent);
  border-top-color:var(--green);animation:tilespin .8s linear infinite}
.tile__spinner em{font-style:normal;font-size:.74rem;color:var(--text-soft)}
@keyframes tilespin{to{transform:rotate(360deg)}}
.tile__foot{padding:.35rem .6rem;font-size:.72rem;color:var(--text-soft);
  border-top:1px solid var(--line);min-height:1.6rem}
.tile__error{color:var(--red,#b4321f);
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}

/* --- the version strip --- */
.versions{display:flex;gap:.35rem;flex-wrap:wrap;margin-top:.8rem}
.version{position:relative;width:54px;height:54px;padding:0;cursor:pointer;
  border:2px solid var(--line);border-radius:6px;overflow:hidden;
  background:var(--paper-2)}
.version img{width:100%;height:100%;object-fit:cover;display:block}
.version span{position:absolute;left:2px;bottom:1px;font-size:.6rem;
  font-weight:700;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.9)}
.version.is-on{border-color:var(--green)}
.version.is-approved::after{content:'✓';position:absolute;right:2px;top:0;
  font-size:.7rem;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.9)}

/* --- the transient line --- */
.studio__toast{position:absolute;left:50%;bottom:1.2rem;transform:translateX(-50%);
  margin:0;padding:.55rem 1rem;border-radius:99px;font-size:.85rem;font-weight:600;
  background:var(--ink);color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:20}
.studio__toast[data-tone="error"]{background:var(--red,#b4321f)}

@media (max-width:900px){
  /* The canvas needs a pointer and a screen. On a phone the rail is the whole
     feature, so it gets the whole width and the board scrolls under it. */
  .studio{grid-template-columns:1fr;height:auto}
  .rail{border-right:none;border-bottom:1.5px solid var(--line)}
  .canvas{height:70vh}
}

/* ========================================================================
   27 · PUBLIC PRODUCT PAGE  ·  /products/<slug>
   --------------------------------------------------------------------------
   The page every other public page exists to send someone to, and for most
   visitors the first page of this site they will ever see — a QR code in a
   printed workbook, a TPT listing, an ad, a search result. Rendered from
   templates/public/_product_page.html, which is also what the import wizard's
   step-4 preview draws, so anything here shows up in the admin too. That is
   the point of it: the preview cannot drift from the page.

   Last in the file on purpose. The admin sections above redeclare .chip and
   .muted without scoping them, so a public page that shares those class names
   loses to the admin's version of them. Being last is one half of the fix; the
   .pdp descendant selectors below are the other, and they hold whatever else
   gets appended to this file later.

   ONE COLOUR VARIABLE. `--c` is the subject accent, set inline on .pdp from
   storefront.ACCENTS, and every tint, rule, badge and gradient below is mixed
   from it. Nothing here names a colour, so a biology page is green and a CTE
   page is red without a single extra rule — and a subject added to the taxonomy
   next year is navy rather than broken.

   Layout is three shapes, not a grid system:
     up to 1020px+   cover column beside the pitch; two-up body sections
     760-1020px      the same, narrower cover, body sections stack
     under 760px     one column, cover centred and flat, buttons full width
   ======================================================================== */

.pdp{display:block;padding-bottom:clamp(2.6rem,6vw,4.5rem)}

/* --- 27.1 hero -----------------------------------------------------------
   Full-bleed and tinted. The wash is two layers: a soft accent glow anchored
   to the top-left, where the cover is, and a fade back to paper by the bottom
   edge so the band ends without a hard line across the page. */
.pdp__hero{position:relative;overflow:hidden;
  background:
    radial-gradient(115% 85% at 10% 0%,color-mix(in srgb,var(--c) 17%,transparent) 0%,transparent 62%),
    linear-gradient(180deg,color-mix(in srgb,var(--c) 8%,var(--paper)) 0%,var(--paper) 100%);
  border-bottom:1.5px solid var(--line);
  padding-block:clamp(1.1rem,2.6vw,1.8rem) clamp(2rem,5vw,3.4rem)}

/* The crumbs keep their own bottom margin, so the row collapses to exactly the
   old layout when there is no edit link beside them. */
.pdp__topline{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem}

.crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;
  font-size:.78rem;color:var(--text-soft);margin-bottom:clamp(1.2rem,3vw,2rem)}
.crumbs a{font-weight:600}
.crumbs a:hover{color:var(--ink);text-decoration:underline}

/* Admin only. Deliberately quiet and deliberately not .btn — it sits on the
   page a customer sees, and the moment it reads as part of the product it has
   changed the page it is there to let someone fix. Outlined, not filled, and
   it never takes the subject accent the rest of the hero runs on. */
.pdp__edit{display:inline-flex;align-items:center;gap:.35rem;flex:none;
  font-size:.76rem;font-weight:700;letter-spacing:.01em;
  padding:.34em .7em;border-radius:999px;
  color:var(--text-soft);background:var(--paper);
  border:1.5px dashed var(--line);transition:color .16s,border-color .16s}
.pdp__edit:hover{color:var(--ink);border-color:var(--ink);text-decoration:none}
.pdp__edit .ico{flex:none}

.pdp__lead{display:grid;grid-template-columns:minmax(240px,330px) 1fr;
  gap:clamp(1.6rem,4vw,3.4rem);align-items:start}

/* --- 27.2 the cover ------------------------------------------------------
   A flat JPEG of a paperback is a picture of a book. The tilt, the spine and
   the glow behind it are what make it read as the book itself, which is the
   whole job of the top-left corner of a product page.

   isolation:isolate is load-bearing: the glow is a z-index:-1 pseudo-element,
   and without a stacking context of its own it would slide behind the hero's
   background gradient and disappear. */
.pdp__coverwrap{position:relative;isolation:isolate;
  display:flex;flex-direction:column;align-items:center;gap:.9rem}
.pdp__coverwrap::before{content:"";position:absolute;z-index:-1;
  inset:10% -8% -6% -8%;border-radius:50%;
  background:radial-gradient(58% 50% at 50% 58%,
    color-mix(in srgb,var(--c) 32%,transparent),transparent 72%);
  filter:blur(24px)}

.pdp__cover{position:relative;width:100%;border-radius:13px;
  transform:perspective(1500px) rotateY(-7deg) rotateX(1.2deg);
  box-shadow:-16px 24px 44px -20px rgba(12,20,120,.55),0 2px 0 rgba(12,20,120,.05);
  transition:transform .3s ease,box-shadow .3s ease}
.pdp__cover img{width:100%;height:auto;display:block;border-radius:13px;
  border:1.5px solid var(--line);background:var(--paper-card)}
/* The spine, in the subject's colour — the same 8px edge a catalog card wears,
   so a workbook is recognisably the same object in the grid and on its page. */
.pdp__cover::before{content:"";position:absolute;left:0;top:0;bottom:0;width:9px;z-index:2;
  border-radius:13px 0 0 13px;background:var(--c);
  box-shadow:inset -1px 0 0 rgba(0,0,0,.12)}
.pdp__noimg{aspect-ratio:3/4;border-radius:13px;border:1.5px dashed var(--line);
  background:var(--paper-card);display:flex;flex-direction:column;justify-content:space-between;
  align-items:flex-start;gap:1rem;padding:1.1rem 1.1rem 1.1rem 1.4rem;
  font-family:var(--display);font-weight:800;font-size:1.2rem;line-height:1.05;
  letter-spacing:-.02em;color:var(--ink)}

/* The one affordance on the cover, and only when it leads somewhere. */
.pdp__peek{display:inline-flex;align-items:center;gap:.45rem;
  font-weight:700;font-size:.82rem;color:var(--ink);
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:99px;
  padding:.5em 1em;box-shadow:var(--shadow);transition:border-color .15s,transform .15s}
.pdp__peek:hover{border-color:var(--c);transform:translateY(-1px)}
.pdp__peek .ico{color:color-mix(in srgb,var(--c) 70%,#000)}

/* --- 27.3 the pitch ------------------------------------------------------ */
.pdp__series{color:color-mix(in srgb,var(--c) 66%,#000);margin-bottom:.7rem}
.pdp__title{font-family:var(--display);font-weight:800;
  font-size:clamp(1.9rem,4.2vw,3rem);line-height:1.02;letter-spacing:-.03em;color:var(--ink)}
.pdp__dek{margin-top:.85rem;max-width:52ch;font-weight:500;
  font-size:clamp(1.02rem,1.4vw,1.2rem);line-height:1.45;color:var(--text)}

/* .chip is redeclared unscoped by the admin sections above; these two rules are
   what keep a subject chip on a public page the subject's colour. */
.pdp__chips{margin-top:1.1rem}
.pdp .chip{border:0;white-space:nowrap;font-weight:600;font-size:.72rem;
  padding:.34em .72em;border-radius:99px;background:var(--paper-2);color:var(--text-soft)}
.pdp .chip--subject{background:color-mix(in srgb,var(--c) 15%,#fff);
  color:color-mix(in srgb,var(--c) 74%,#000)}

/* Built from whatever the row actually records, and dropped below two items —
   see the template. A strip of one fact is a widget, not a spec. */
.pdp__facts{list-style:none;display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.2rem}
.pdp__facts li{display:inline-flex;align-items:center;gap:.5rem;
  padding:.5em .9em;border-radius:99px;font-size:.86rem;color:var(--text-soft);
  background:var(--paper-card);border:1.5px solid var(--line)}
.pdp__facts strong{color:var(--ink);font-weight:800}
.pdp__facts .ico{color:color-mix(in srgb,var(--c) 68%,#000)}

.pdp__cta{display:flex;flex-direction:column;align-items:flex-start;gap:.8rem;margin-top:1.5rem}
.pdp__buttons{display:flex;flex-wrap:wrap;align-items:center;gap:.7rem}
/* The narrower buys, on their own line: beside the full workbook rather than
   competing with it. */
.pdp__editions{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem 1rem}
.pdp__editions a{font-weight:700;font-size:.88rem;color:var(--ink);padding-bottom:1px;
  border-bottom:2px solid color-mix(in srgb,var(--c) 35%,transparent)}
.pdp__editions a:hover{border-bottom-color:var(--c)}
.pdp__trust{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .7rem;
  font-weight:600;font-size:.87rem;color:var(--green-d)}
.pdp__dot{color:var(--line)}

/* --- 27.4 the bands ------------------------------------------------------
   Every section below the hero is a .pdp__band inside a .wrap. They are spaced
   rather than ruled: a page with seven horizontal lines across it reads as a
   form, and each of these is already a distinct shape. */
.pdp__band{margin-top:clamp(2.4rem,5.5vw,4rem)}
.pdp__bandhd{max-width:68ch;margin-bottom:clamp(1rem,2.4vw,1.5rem)}
.pdp__band h2{font-family:var(--display);font-weight:800;
  font-size:clamp(1.45rem,3vw,2.05rem);line-height:1.06;letter-spacing:-.025em;color:var(--ink)}
.pdp__band .eyebrow{display:block;margin-bottom:.45rem;color:color-mix(in srgb,var(--c) 62%,#000)}
.pdp__lede{margin-top:.45rem;font-size:1rem;line-height:1.5;color:var(--text-soft)}

/* --- 27.5 the free material ---------------------------------------------
   Four real buttons where there used to be four underlined links. This is the
   strongest thing the page has — a whole lesson, free, before any money changes
   hands — and /resources sends every scanned QR code here promising exactly
   this block. auto-fit rather than a fixed count because a workbook may have
   one of these or all four. */
.reskit{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(248px,100%),1fr));gap:.9rem}
.reskit__item{display:flex;align-items:center;gap:.9rem;padding:1rem 1.1rem;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--shadow);transition:transform .16s,box-shadow .16s,border-color .16s}
.reskit__item:hover{transform:translateY(-3px);box-shadow:var(--shadow-h);border-color:var(--c)}
.reskit__ico{flex:none;width:46px;height:46px;border-radius:12px;display:grid;place-items:center;
  background:color-mix(in srgb,var(--c) 13%,#fff);color:color-mix(in srgb,var(--c) 76%,#000)}
.reskit__text{display:flex;flex-direction:column;gap:.15rem;min-width:0}
.reskit__text strong{font-weight:700;font-size:.97rem;line-height:1.2;color:var(--ink)}
.reskit__text em{font-style:normal;font-size:.82rem;line-height:1.35;color:var(--text-soft)}
.reskit__arw{margin-left:auto;color:var(--text-soft);transition:transform .16s,color .16s}
.reskit__item:hover .reskit__arw{transform:translateX(3px);color:var(--ink)}

/* --- 27.6 what you get, and the prose -----------------------------------
   The list is narrower than the prose and comes first in the source, so it is
   also what comes first when the two stack on a phone: the scannable layer
   should never end up below the thing it exists to save you reading. */
.pdp__split{display:grid;grid-template-columns:minmax(270px,.82fr) 1.18fr;
  gap:clamp(1.6rem,4vw,3.2rem);align-items:start}

.pdp__highlights{position:relative;overflow:hidden;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  padding:clamp(1.2rem,2.6vw,1.7rem);box-shadow:var(--shadow)}
.pdp__highlights::before{content:"";position:absolute;left:0;top:0;bottom:0;width:5px;background:var(--c)}
.ticks{list-style:none;display:flex;flex-direction:column;gap:.72rem;margin-top:.95rem}
.ticks li{display:flex;align-items:flex-start;gap:.65rem;
  font-size:.97rem;line-height:1.45;color:var(--text)}
.ticks .ico{width:21px;height:21px;padding:2.5px;margin-top:.12rem;border-radius:50%;
  color:var(--green-d);background:color-mix(in srgb,var(--green) 20%,#fff)}
.pdp__audience{display:flex;align-items:flex-start;gap:.6rem;
  margin-top:1.1rem;padding-top:1rem;border-top:1px dashed var(--line);
  font-size:.92rem;line-height:1.5;color:var(--text-soft)}
.pdp__audience .ico{margin-top:.08rem;color:color-mix(in srgb,var(--c) 70%,#000)}

.pdp__desc p{max-width:64ch;margin-top:.9rem;font-size:1.02rem;line-height:1.68;color:var(--text)}
/* The opening paragraph carries the weight — it is the one a skimmer reads. */
.pdp__desc p:first-of-type{font-size:1.1rem;color:var(--ink)}
.pdp__desc--solo p{max-width:70ch}

/* --- 27.7 in every lesson ------------------------------------------------
   This catalog's actual differentiator is that the structure repeats: the same
   shape, twenty-five times. On the old page that was a subordinate clause in
   the middle of a paragraph. */
.lessonkit{list-style:none;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(196px,100%),1fr));gap:.7rem}
.lessonkit li{display:flex;align-items:center;gap:.7rem;padding:.8rem .9rem;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r);
  font-weight:600;font-size:.92rem;line-height:1.3;color:var(--ink)}
.lessonkit__n{flex:none;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  font-weight:800;font-size:.75rem;
  background:color-mix(in srgb,var(--c) 16%,#fff);color:color-mix(in srgb,var(--c) 76%,#000)}

/* --- 27.8 contents + sample pages ---------------------------------------
   .toc and .samples are shared with the admin (§20); these are the public
   page's version of them, scoped so the editor's tabs are untouched. */
.pdp .toc{grid-template-columns:repeat(auto-fill,minmax(min(286px,100%),1fr));gap:1rem}
.pdp .toc__unit{background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);padding:1.1rem 1.2rem}
.pdp .toc__unit strong{border-bottom-color:color-mix(in srgb,var(--c) 38%,var(--line))}
.pdp .toc__unit li{position:relative;padding-left:.95rem}
.pdp .toc__unit li::before{content:"";position:absolute;left:0;top:.62em;
  width:5px;height:5px;border-radius:50%;background:color-mix(in srgb,var(--c) 55%,#fff)}

.pdp .samples{grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr));margin-top:0}
.pdp .sample a{position:relative;display:block}
.sample__zoom{position:absolute;right:.55rem;bottom:.55rem;width:32px;height:32px;border-radius:50%;
  display:grid;place-items:center;background:var(--ink);color:#fff;box-shadow:var(--shadow);
  opacity:0;transform:translateY(4px);transition:opacity .16s,transform .16s}
.sample a:hover .sample__zoom,.sample a:focus-visible .sample__zoom{opacity:1;transform:none}
/* No hover on a touch screen, so the badge is simply always there — otherwise
   the one device most likely to open this page never learns the image opens. */
@media (hover:none){ .sample__zoom{opacity:1;transform:none} }

/* --- 27.8b reviews -------------------------------------------------------
   The band above the sample pages, and the rating line in the hero.

   Cards, not a carousel. Every review is readable at once and by a crawler, it
   prints, it needs no script, and it cannot hide the two-star one behind an
   arrow nobody clicks. `align-items:start` matters more than it looks: reviews
   vary from one line to ten, and a two-line quote stretched to the height of a
   ten-line one reads as a card with something missing from it.

   The accent appears as a rule along the top edge and in the initials disc, and
   nowhere in the words: these are the one part of the page 3andB did not write,
   and tinting a teacher's sentences in the house colour would blur that. */
.pdp__rated{display:inline-flex;align-items:center;gap:.5rem;margin-top:.85rem;
  padding:.3rem .75rem .3rem .6rem;border-radius:99px;
  background:color-mix(in srgb,var(--c) 8%,var(--paper-card));
  border:1.5px solid color-mix(in srgb,var(--c) 24%,var(--line));
  font-size:.88rem;font-weight:700;color:var(--ink);text-decoration:none}
.pdp__rated:hover{border-color:var(--c);text-decoration:none}
.pdp__rated .stars{color:color-mix(in srgb,var(--c) 78%,#000);font-size:.95rem}

.pdp__rating{display:flex;flex-wrap:wrap;align-items:center;gap:.15rem .55rem;
  margin-top:.55rem;color:var(--text-soft)}
.pdp__rating strong{color:var(--ink);font-weight:800}
.stars--lg{font-size:1.15rem;color:color-mix(in srgb,var(--c) 78%,#000)}

.reviews{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(320px,100%),1fr));
  gap:1.1rem;align-items:start}
.rev{position:relative;display:flex;flex-direction:column;gap:.55rem;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  padding:1.25rem 1.3rem 1.15rem;overflow:hidden}
.rev::before{content:"";position:absolute;left:0;right:0;top:0;height:4px;
  background:color-mix(in srgb,var(--c) 55%,var(--paper-card))}
.rev__stars{color:color-mix(in srgb,var(--c) 78%,#000);letter-spacing:.09em;font-size:.98rem}
.rev__title{font-family:var(--display);font-weight:800;font-size:1.02rem;
  line-height:1.3;color:var(--ink)}
/* A blockquote, so it is marked up as somebody else's words as well as looking
   like them. The quotation mark is decorative and sits outside the text flow —
   a real " in the content would be read out by a screen reader on every card. */
.rev__body{position:relative;font-size:.97rem;line-height:1.62;color:var(--text);
  margin:0;quotes:none}
.rev__body::before{content:"”";position:absolute;right:-.15rem;bottom:-1.9rem;
  font-family:var(--display);font-size:4.5rem;line-height:1;pointer-events:none;
  color:color-mix(in srgb,var(--c) 13%,transparent)}
.rev__by{display:flex;align-items:center;gap:.6rem;margin-top:auto;padding-top:.7rem;
  border-top:1px solid var(--line)}
/* A disc for two or three letters, a pill for more. The column allows eight
   characters and "M.B.O'C" in a fixed circle is letters spilling over an edge —
   min-width holds the round shape for the common case and the padding lets the
   rest grow sideways instead. */
.rev__mark{flex:none;display:grid;place-items:center;min-width:34px;height:34px;
  padding:0 .5em;border-radius:99px;
  background:color-mix(in srgb,var(--c) 16%,var(--paper-card));
  color:color-mix(in srgb,var(--c) 80%,#000);
  font-size:.76rem;font-weight:800;letter-spacing:.04em;white-space:nowrap}
.rev__by em{font-style:normal;font-size:.83rem;color:var(--text-soft)}

/* --- 27.9 standards ------------------------------------------------------
   One pill per framework the workbook is written against — see
   catalog.public_standards, and schema/041 for why this is a list of bodies
   rather than a table of individual codes. Renders only for workbooks that
   actually have standards recorded.

   The abbreviation carries the subject accent (--c, set on the page shell) and
   the full name sits beside it in the same pill: NGSS means nothing to a parent
   and everything to a department head, and the pill has to work for both. They
   wrap as a row of chips, so three frameworks read as three, not as a list. */
.stdpills{list-style:none;display:flex;flex-wrap:wrap;gap:.7rem}
.stdpill{display:flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .6rem;
  background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:99px;padding:.55rem 1.05rem}
.stdpill strong{font-family:var(--body);font-weight:800;font-size:.82rem;
  letter-spacing:.06em;white-space:nowrap;color:color-mix(in srgb,var(--c) 74%,#000)}
.stdpill span{font-size:.86rem;line-height:1.35;color:var(--text-soft)}

/* --- 27.10 FAQ ----------------------------------------------------------
   Native <details>. An accordion built out of JavaScript is closed to a crawler
   and to anyone whose script did not load, and these answers are the page's
   FAQPage structured data. */
.faq{display:flex;flex-direction:column;gap:.7rem;max-width:80ch}
.faq__item{background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);overflow:hidden}
.faq__item[open]{border-color:color-mix(in srgb,var(--c) 42%,var(--line));box-shadow:var(--shadow)}
.faq__item summary{display:flex;justify-content:space-between;align-items:center;gap:1rem;
  padding:1rem 1.2rem;cursor:pointer;list-style:none;
  font-weight:700;font-size:1rem;line-height:1.35;color:var(--ink)}
.faq__item summary::-webkit-details-marker{display:none}
.faq__item summary::marker{content:""}
/* A plus that becomes a minus. Two pseudo-elements rather than a rotating glyph:
   a rotated + is a ×, which reads as "close" rather than "collapse". */
.faq__mark{position:relative;flex:none;width:18px;height:18px}
.faq__mark::before,.faq__mark::after{content:"";position:absolute;
  background:var(--text-soft);border-radius:2px;transition:transform .18s,opacity .18s}
.faq__mark::before{left:0;right:0;top:8px;height:2.5px}
.faq__mark::after{top:0;bottom:0;left:8px;width:2.5px}
.faq__item[open] .faq__mark::after{transform:scaleY(0);opacity:0}
.faq__a{padding:0 1.2rem 1.15rem;max-width:70ch;font-size:.96rem;line-height:1.62;color:var(--text)}
.faq__a p+p{margin-top:.7rem}

/* --- 27.11 the second ask ------------------------------------------------
   Someone who reached this has read the contents, the sample pages and the FAQ.
   Making them scroll back to the top to act on that is the easiest sale on this
   site to lose. */
.pdp__close{margin-top:clamp(2.6rem,5.5vw,4rem);border-radius:var(--r-lg);
  padding:clamp(1.5rem,4vw,2.6rem);color:#fff;
  display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1.4rem;
  background:linear-gradient(135deg,var(--ink) 0%,color-mix(in srgb,var(--c) 42%,var(--ink)) 100%)}
.pdp__close h2{color:#fff;font-size:clamp(1.35rem,2.6vw,1.95rem)}
.pdp__close p{margin-top:.35rem;font-size:.94rem;color:rgba(255,255,255,.78)}

/* --- 27.12 more like this ------------------------------------------------ */
.relgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(212px,100%),1fr));gap:1rem}
.relcard{display:flex;flex-direction:column;overflow:hidden;
  background:var(--paper-card);border:1.5px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--shadow);transition:transform .16s,box-shadow .16s}
.relcard:hover{transform:translateY(-4px);box-shadow:var(--shadow-h)}
.relcard__cover{position:relative;aspect-ratio:3/4;overflow:hidden;
  background:var(--paper-2);display:grid;place-items:center}
.relcard__cover img{width:100%;height:100%;object-fit:contain;padding:.5rem}
.relcard__body{flex:1;display:flex;flex-direction:column;gap:.35rem;padding:.85rem 1rem 1rem}
.relcard__body strong{font-family:var(--display);font-weight:800;font-size:1rem;
  line-height:1.15;letter-spacing:-.02em;color:var(--ink)}
.relcard__body em{font-style:normal;font-size:.83rem;line-height:1.4;color:var(--text-soft)}
.relcard__foot{margin-top:auto;padding-top:.5rem;display:inline-flex;gap:.4em;
  font-weight:700;font-size:.85rem;color:color-mix(in srgb,var(--c) 62%,#000)}
.relcard:hover .arw{transform:translateX(3px)}

/* --- 27.13 the sticky buy bar -------------------------------------------
   Phones and small tablets only: on a laptop the hero's buttons are still on
   screen for most of the page, and a bar would be covering content to repeat
   something already visible.

   Visible by default; main.js adds data-armed and then toggles is-on. Without
   JavaScript the bar simply stays put, which is correct — the failure mode of
   the other arrangement is a buy button that never appears at all. */
.buybar{display:none}
@media (max-width:900px){
  .buybar{display:block;position:fixed;left:0;right:0;bottom:0;z-index:70;
    padding:.6rem var(--gut) calc(.6rem + env(safe-area-inset-bottom,0px));
    background:color-mix(in srgb,var(--paper-card) 94%,transparent);backdrop-filter:blur(10px);
    border-top:1.5px solid var(--line);box-shadow:0 -10px 30px -22px rgba(12,20,120,.7);
    transition:transform .22s ease,opacity .22s ease}
  .buybar__in{max-width:var(--wrap);margin-inline:auto;display:flex;align-items:center;gap:.9rem}
  .buybar__title{flex:1;min-width:0;font-weight:700;font-size:.9rem;color:var(--ink);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .buybar .btn{flex:none}
  .buybar[data-armed]{transform:translateY(105%);opacity:0;pointer-events:none}
  .buybar[data-armed].is-on{transform:none;opacity:1;pointer-events:auto}

  /* Room for the bar under the footer, and the chat launcher lifted clear of
     it. The launcher is fixed by chat.css — under 620px it pins itself rather
     than riding on .chatdock — so both are moved, and both selectors are more
     specific than the rules they are overriding rather than relying on this
     file loading second (it doesn't). */
  body.has-buybar{padding-bottom:76px}
  body.has-buybar .chatdock,
  body.has-buybar .chatdock__fab{bottom:calc(76px + env(safe-area-inset-bottom,0px))}
}

/* --- 27.14 the three shapes --------------------------------------------- */
@media (max-width:1020px){
  .pdp__lead{grid-template-columns:minmax(200px,264px) 1fr;gap:clamp(1.4rem,3.5vw,2.4rem)}
  /* The prose and the tick list stop being side by side well before the page
     does: at this width the list is too narrow to hold a 12-word highlight on
     one line, and a bullet that wraps stops being scannable. */
  .pdp__split{grid-template-columns:1fr;gap:1.8rem}
}
@media (max-width:760px){
  .pdp__lead{grid-template-columns:1fr;gap:1.5rem}
  .pdp__coverwrap{width:min(100%,248px);margin-inline:auto}
  /* Flat, not tilted. A tilted book at this size is just a crooked one, and
     the perspective transform costs more than it earns on a phone. */
  .pdp__cover{transform:none;box-shadow:var(--shadow-h)}
  .pdp__facts li{font-size:.82rem;padding:.45em .8em}
}
@media (max-width:520px){
  /* Full width, stacked, TPT on top: on a phone the buy buttons are the page,
     and two half-width buttons side by side are two buttons that are hard to
     hit. */
  .pdp__buttons{width:100%}
  .pdp__buttons .btn{flex:1 1 100%;justify-content:center}
  .pdp__close{flex-direction:column;align-items:stretch;text-align:left}
  .pdp .samples,.relgrid{grid-template-columns:repeat(2,1fr)}
  .reskit{grid-template-columns:1fr}
}

/* Everything above that moves, doesn't. The tilt is the one piece of decoration
   here that is genuinely motion-adjacent, so it goes too. */
@media (prefers-reduced-motion:reduce){
  .pdp__cover{transform:none;transition:none}
  .reskit__item,.relcard,.buybar,.sample__zoom,.faq__mark::before,.faq__mark::after{transition:none}
  .reskit__item:hover,.relcard:hover{transform:none}
}

/* The two links inside a lede — the subject crumb's twin at the bottom of the
   page. Underlined rather than coloured: this sits in a grey sentence, and a
   coloured word in grey type reads as an accident. */
.pdp__lede a{font-weight:700;color:var(--text-soft);
  border-bottom:1.5px solid color-mix(in srgb,var(--c) 40%,transparent)}
.pdp__lede a:hover{color:var(--ink);border-bottom-color:var(--c)}

/* ========================================================================
   28 · MAINTENANCE  ·  the closed sign
   ------------------------------------------------------------------------
   One page, no header, no nav — see templates/public/maintenance.html for why.
   Centred in the viewport rather than sitting at the top of an empty page: the
   whole document is four short lines, and pinned to the top they read as a
   page that failed to finish loading.
   ======================================================================== */
body.closed{min-height:100dvh;display:grid;place-items:center;padding:2rem 1.2rem;
  background:
    radial-gradient(120% 80% at 50% 0%,color-mix(in srgb,var(--green) 12%,transparent) 0%,transparent 60%),
    var(--paper)}
.closed__in{max-width:34rem;text-align:center}
.closed__logo{width:clamp(96px,18vw,150px);height:auto;margin-bottom:1.4rem}
.closed__h1{font-family:var(--display);font-weight:800;letter-spacing:-.02em;
  font-size:clamp(1.7rem,5vw,2.6rem);line-height:1.1;color:var(--ink);margin:.3rem 0 1rem}
.closed__lede{font-size:1.05rem;line-height:1.6;color:var(--text)}
.closed__contact{margin-top:1.6rem;font-size:.94rem;color:var(--text-soft)}
.closed__contact a{font-weight:700;color:var(--ink);border-bottom:1.5px solid var(--green)}
.closed__sig{margin-top:1.8rem;font-size:1.05rem;color:var(--text-soft)}
/* The way back in. Quiet enough that a visitor slides past it, solid enough to
   find in a hurry — during an outage this is the only door, and the reason it
   is here is that it once wasn't. */
.closed__staff{margin-top:2.6rem;font-size:.82rem}
.closed__staff a{color:var(--text-soft);border-bottom:1px dotted var(--line)}
.closed__staff a:hover{color:var(--ink);border-bottom-color:var(--ink)}

/* ========================================================================
   29 · POP-UP MESSAGE  ·  the announcement panel
   ------------------------------------------------------------------------
   A centred card over a dimmed page. One is rendered at most, from base.html,
   when popup.notice() has something to say — see templates/public/_popup.html
   for why it is in the document visible rather than revealed by script.

   The three tones set one custom property between them and everything else
   below reads it. That is the whole theming mechanism: a fourth tone is a
   three-line block, not a second copy of the card.
   ======================================================================== */
.popup{position:fixed;inset:0;z-index:80;display:grid;place-items:center;
  padding:1.2rem;--c:var(--green);--c-ink:var(--green-ink)}
.popup--info{--c:var(--blue);--c-ink:#FFFFFF}
.popup--warn{--c:var(--amber);--c-ink:#FFFFFF}

/* Hidden by main.js for anyone who has already closed this message. The
   attribute rather than a class: [hidden] is already forced to display:none at
   the top of this file, and a panel that is display:grid needs it to be. */
.popup[hidden]{display:none!important}

.popup__veil{position:absolute;inset:0;background:rgba(12,20,120,.42);
  backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}

.popup__card{position:relative;width:min(34rem,100%);max-height:88dvh;overflow-y:auto;
  background:var(--paper-card);border-radius:var(--r-lg);
  border:1px solid var(--line);border-top:5px solid var(--c);
  box-shadow:0 30px 70px -30px rgba(12,20,120,.6);
  padding:2rem clamp(1.3rem,4vw,2.2rem) 1.6rem;text-align:center;
  animation:popup-in .28s cubic-bezier(.2,.8,.3,1) both}

@keyframes popup-in{from{opacity:0;transform:translateY(14px) scale(.98)}
  to{opacity:1;transform:none}}

/* The logo, small and quiet. It is here so the panel reads as this site
   speaking rather than as something the browser injected — which is exactly
   what an unbranded modal over a page you didn't ask for looks like. */
.popup__mark img{width:54px;height:auto;margin:0 auto .9rem}

.popup__h{font-family:var(--display);font-weight:800;letter-spacing:-.02em;
  font-size:clamp(1.25rem,3.4vw,1.6rem);line-height:1.15;color:var(--ink);
  margin-bottom:.7rem}
.popup__p{color:var(--text);line-height:1.6;margin-bottom:.7rem}
.popup__p:last-of-type{margin-bottom:0}

/* Wraps to one button per line on a phone, which is the width at which two
   side by side stop being tappable. */
.popup__links{display:flex;flex-wrap:wrap;gap:.6rem;justify-content:center;
  margin-top:1.3rem}
.popup__link{flex:1 1 12rem;justify-content:center}
/* The primary button wears the tone rather than the brand green, so an amber
   panel isn't a green button on an amber card. */
.popup--info .btn--primary,.popup--warn .btn--primary{background:var(--c);
  color:var(--c-ink);border-color:var(--c);box-shadow:none}

/* The way out, twice. The corner × is the one people reach for and the worded
   link at the bottom is the one people find — on a phone, with a thumb, the
   bottom of the card is reachable and the top-right corner is not. */
.popup__x{position:absolute;top:.5rem;right:.75rem;width:2rem;height:2rem;
  display:grid;place-items:center;border-radius:50%;
  font-size:1.5rem;line-height:1;color:var(--text-soft)}
.popup__x:hover{background:var(--paper-2);color:var(--ink)}
.popup__dismiss{display:inline-block;margin-top:1.1rem;font-size:.9rem;
  font-weight:600;color:var(--text-soft);border-bottom:1.5px solid var(--line)}
.popup__dismiss:hover{color:var(--ink);border-bottom-color:var(--c)}

/* The admin's live preview: the same card, rendered in the flow of the page
   instead of over it. One class, so what the panel looks like is defined once
   and the preview cannot drift away from the thing it is previewing.
   
   It keeps the card's real 34rem measure rather than filling the panel. A
   preview stretched to 1100px is not a preview of anything a visitor sees —
   the line lengths are the part most worth checking before it goes out. */
.popup--inline{position:static;padding:0;display:block}
.popup--inline .popup__veil{display:none}
.popup--inline .popup__card{max-height:none;animation:none;margin-inline:auto;
  box-shadow:var(--shadow)}
.popup--inline .popup__x{pointer-events:none;opacity:.4}

/* Admin · the form on /admin/popup. Held to a readable measure inside a
   full-width panel: a text input as wide as the page is one where you lose the
   start of the line you are typing, and the hints under the fields become
   single-line paragraphs 1100px across. */
.popupform form,.popupform>p{max-width:46rem}
/* A button's wording and its address are not two equal columns — two words
   against a URL. Kept side by side down to tablet width, then stacked, which
   is where 40% of the row stops being enough for either half. */
.linkrow{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.7fr);
  gap:0 1.1rem;max-width:46rem}
.linkrow>*{min-width:0}
@media (max-width:780px){ .linkrow{grid-template-columns:1fr} }

@media (prefers-reduced-motion:reduce){
  .popup__card{animation:none}
  .popup__veil{backdrop-filter:none;-webkit-backdrop-filter:none}
}
