/*
 * Public article reading layout — phase 2 of Interactive Articles (SEO/GEO).
 *
 * Self-contained: served by the Node server at /assets/articles/article.css,
 * no Tailwind build. Colors mirror the client's theme-v2 palette
 * (client/src/index.css .theme-v2: cobalt rgb(10 100 248), gold
 * rgb(251 205 9)); look = pill buttons, soft cards, single-column measure
 * (60rem on the article page since D23, with 22px body text from 1280px wide). The shared
 * ed-* editorial kit follows the base rules, between its start and end
 * markers, and the page sections that compose it follow the kit (Articles
 * library, articles-index-redesign phase 4; Editorial article header, phase
 * 6; Article audience, article-audience phase 3) — source order base → kit →
 * pages (docs/architecture/modules/
 * articles-seo.md → Visual system).
 *
 * Brand fonts are SELF-HOSTED, same-origin (latin-subset woff2 under
 * /assets/articles/fonts/) so the router CSP already carries `font-src 'self'`
 * — no CSP widening and no third-party render-blocking request on this
 * LCP-sensitive surface (D5). Nunito (wordmark/display) + Hanken Grotesk
 * (chrome UI) mirror the homepage; `font-display: swap` keeps text visible
 * during the swap. The article BODY prose stays Charter (see --a-serif).
 */

/* Nunito 800 — the wordmark + display headings. */
@font-face {
  font-family: "Nunito";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("/assets/articles/fonts/nunito-800.woff2") format("woff2");
}

/* Hanken Grotesk 400/500/600/700 — chrome UI / widget sans. 500 is the
   marketing nav's link weight (LandingNav's font-medium); without its own face
   the browser would draw those links at 400. */
@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/articles/fonts/hanken-400.woff2") format("woff2");
}
@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/articles/fonts/hanken-500.woff2") format("woff2");
}
@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/articles/fonts/hanken-600.woff2") format("woff2");
}
@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/articles/fonts/hanken-700.woff2") format("woff2");
}

:root {
  --a-brand: rgb(10 100 248);
  --a-brand-hover: rgb(7 86 223);
  --a-brand-subtle: rgb(234 242 255);
  --a-accent: rgb(251 205 9);
  --a-accent-subtle: rgb(255 243 198);
  --a-text: rgb(22 26 36);
  --a-text-muted: rgb(91 100 117);
  --a-surface: #ffffff;
  --a-surface-sunken: rgb(247 249 252);
  --a-border: rgb(227 232 240);
  --a-success: rgb(22 163 74);
  --a-success-subtle: rgb(240 253 244);
  --a-error: rgb(220 38 38);
  --a-error-subtle: rgb(254 242 242);
  --a-gray: rgb(60 66 82);
  --a-gray-subtle: rgb(247 249 252);

  /* Theme-v2 tokens for the ed-* editorial kit and the page sections after it
     (the end of this file) and the later editorial pages: the kit and the
     page sections (Articles library, Editorial article header) read
     brand-100, ink-2, subtle, line-2 and ease; nothing reads brand-200,
     brand-900 or accent-ink yet. */
  --a-brand-100: #d4e4ff; /* primary-100 — light cobalt for backing-card gradients */
  --a-brand-200: #aecbff; /* primary-200 — mid-light cobalt */
  --a-brand-900: #013db5; /* primary-900 — deepest cobalt; the kit's shadows hard-code it as rgb(1 61 181 / …) */
  --a-accent-ink: #8a6700; /* the ONLY gold allowed as text on white (~5.2:1); --a-accent never is */
  --a-ink-2: #3c4252; /* neutral-700 — secondary ink for meta lines, taglines, section heads */
  --a-subtle: #8a93a6; /* neutral-500 — quiet ink for separators and dots */
  --a-line-2: #eef1f6; /* neutral-100 — hairline ring on the pill row and cover card */
  --a-ease: cubic-bezier(.2, .7, .2, 1); /* the kit's shared easing curve */

  /* Serif for reading prose (Medium-style); sans for chrome/UI/widget;
     display sans for the title, section headings, and avatar. */
  --a-serif: Charter, "Bitstream Charter", "Iowan Old Style",
    "Palatino Linotype", Georgia, Cambria, "Times New Roman", serif;
  --a-sans: "Hanken Grotesk", Inter, system-ui, -apple-system, "Segoe UI",
    Roboto, sans-serif;
  --a-display: Nunito, Inter, system-ui, sans-serif;
  --a-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Reading-prose ink — a touch softer than heading ink, like Medium. */
  --a-prose: rgb(41 41 41);

  /* Homepage chrome palette (theme-v2 light-mode values from
     client/src/index.css .theme-v2; this surface is light-only and
     self-contained). The sticky nav bar + the dark marketing footer mirror
     LandingNav/LandingFooter. */
  --a-neutral-100: rgb(238 241 246);
  --a-neutral-200: rgb(227 232 240);
  --a-neutral-300: rgb(212 219 230);
  --a-neutral-400: rgb(185 194 210);
  --a-neutral-500: rgb(138 147 166);
  --a-neutral-800: rgb(38 43 56);
  --a-neutral-900: rgb(22 26 36);
  --a-primary-400: rgb(77 139 255);
  --a-primary-600: rgb(10 100 248);
  --a-primary-700: rgb(7 86 223);
}

::selection { background: var(--a-brand-subtle); }

* { box-sizing: border-box; }

/*
 * The widget's Check/Try-again state machine toggles the `hidden` attribute.
 * Author-origin display values (e.g. .article-btn's inline-flex) beat the UA
 * stylesheet's `[hidden] { display: none }` regardless of specificity, so
 * hidden elements must be forced off here.
 */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  background: var(--a-surface);
  color: var(--a-text);
  font-family: var(--a-sans);
  font-size: 1.0625rem;
  line-height: 1.7;
  /* html sets no overflow, so body's value propagates to the viewport, where
     it acts as hidden: it stops desktop sideways scrolling caused by the 100vw
     full-bleed washes (100vw includes a visible scrollbar). It does NOT hide
     genuine overflow on phones, so nothing may overflow. */
  overflow-x: clip;
}

/* ---------------------------------------------------------------- chrome */

/* Homepage nav bar (LandingNav.tsx reference: the bar's fixed <nav> wrapper,
   `bg-surface-raised/80 backdrop-blur-xl border-b`): translucent white with a
   backdrop blur and a hairline bottom border. ONE DELIBERATE DIVERGENCE (D7):
   the homepage uses `position: fixed` because the landing page owns its whole
   scroll; here it is `position: sticky; top: 0` so a long article's prose is
   never underlapped by a fixed bar and no body top-padding hack is needed. */
.article-site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(255, 255, 255, 0.8);
  -webkit-backdrop-filter: blur(24px); /* backdrop-blur-xl */
  backdrop-filter: blur(24px);
  border-bottom: 1px solid var(--a-neutral-100);
  /* The homepage draws all its text this way (client/src/index.css body);
     without it the bar's labels render heavier on macOS. Header only: the
     article prose keeps the platform default. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Inner max-width wrapper (homepage `max-w-7xl` = 80rem) with responsive
   gutters (px-4 / sm:px-6 / lg:px-8) and the fixed bar height (h-16, xl:h-20).
   ONE row at every width (unified-marketing-nav): below the nav breakpoint the
   bar holds only the logo and the menu button, so nothing wraps. It is the
   phone menu panel's containing block (position: relative) — never let the
   panel go position: fixed, because the header's backdrop-filter makes the
   header the containing block for fixed descendants anyway. */
.article-site-header-inner {
  position: relative;
  max-width: 80rem;
  margin: 0 auto;
  padding: 0 1rem;
  height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: nowrap;
}

@media (min-width: 640px) {
  .article-site-header-inner { padding: 0 1.5rem; }
}

@media (min-width: 1024px) {
  .article-site-header-inner { padding: 0 2rem; }
}

@media (min-width: 1280px) {
  .article-site-header-inner { height: 5rem; }
}

.article-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
  flex-shrink: 0;
}

/* Portrait glyph mark — 3.5rem tall (homepage h-14), width auto so it never
   distorts and deliberately overflows the 4rem bar like the homepage. The
   <img> also carries width/height attributes; aspect-ratio keeps the reserved
   box correct once `height` overrides the attribute height, so the wordmark
   beside it never shifts as the asset loads. */
.article-brand-logo {
  height: 3.5rem;
  width: auto;
  aspect-ratio: 579.4 / 874.5;
  display: block;
}

/* Two-tone Nunito wordmark (BrandWordmark reference): 'Edu' in brand, 'Park'
   in neutral-900. The two-tone spans are aria-hidden; an unbroken 'EduPark' is
   emitted visually-hidden alongside (D10) so a screen reader never hears
   "Edu Park". Never white-labeled — this is EduPark's apex marketing surface. */
.article-brand-name {
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 1.5rem;
  color: var(--a-neutral-900);
  line-height: 2rem; /* text-2xl, so the glyphs sit where the homepage's do */
}

.article-brand-name .article-brand-edu { color: var(--a-brand); }

/* Visually-hidden but readable by assistive tech (mirrors Tailwind sr-only). */
.article-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The base link look below is what app mode's lone '← Back to EduPark' link
   uses. The MARKETING nav's links, Sign In, Start Free and menus are restyled
   further down ("The marketing nav") to match LandingNav exactly.
   min-width: 0 lets app mode's link wrap inside the bar instead of pushing it
   wider. */
.article-site-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.article-nav-link {
  color: var(--a-text);
  font-family: var(--a-sans);
  font-weight: 600;
  font-size: 0.9375rem;
  text-decoration: none;
  padding: 0 0.875rem;
  /* ≥44px tap target for the text links */
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

.article-nav-link:hover { color: var(--a-brand); }

/* The 'Start Free' CTA matches the HOMEPAGE, not the friendly pill (D8):
   LandingNav's PRIMARY_CTA_CLASS — linear-gradient(primary-600 → primary-700),
   rounded-2xl (1rem), px-6 py-2.5, text-sm (14/20) font-semibold, so 40px
   tall like the homepage's. It shows only from the nav breakpoint (1024px);
   below it the phone menu's full-width copy (further down) is 48px tall.
   article.css is outside the lint:buttons ratchet (it is not client/src),
   documented in design-system.md, so nothing is suppressed.
   SCOPED to the nav (0-2-0) on purpose: a bare `.article-nav-cta` ties 0-1-0
   with `.article-btn`, declared LATER, so it lost on source order — the
   declarations here were silently inert. nowrap: the wide-screen row never
   wraps, so a squeezed label overflows (and the width sweep catches it)
   rather than breaking onto a second line. */
.article-site-nav .article-nav-cta {
  min-height: 0;
  border: 0;
  padding: 0.625rem 1.5rem;
  margin-left: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  border-radius: 1rem;
  background: linear-gradient(
    to right,
    var(--a-primary-600),
    var(--a-primary-700)
  );
  color: #fff;
  white-space: nowrap;
}

/* The homepage CTA keeps its gradient on hover (.article-btn-primary:hover
   would flatten it) and lifts: hover:shadow-lg shadow-primary-600/25
   hover:-translate-y-0.5, transition-all 200ms. The phone menu's full-width
   copy has no hover lift, like LandingNav's. */
.article-site-nav .article-nav-cta:hover {
  background: linear-gradient(
    to right,
    var(--a-primary-600),
    var(--a-primary-700)
  );
}

.article-nav-auth .article-nav-cta {
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.article-nav-auth .article-nav-cta:hover {
  box-shadow:
    0 10px 15px -3px rgb(10 100 248 / 0.25),
    0 4px 6px -4px rgb(10 100 248 / 0.25);
  transform: translateY(-0.125rem);
}

/* The marketing nav (unified-marketing-nav phase 2). The header renders
   the shared MARKETING_NAV_ITEMS (siteHeader in articleTemplates.ts): a link
   row + Sign In / Start Free from the nav breakpoint up, and below it one menu
   button whose panel repeats them. Both menus are native <details>; site-nav.js
   only adds closes. The three phone spacing bands that used to squeeze three
   links beside the logo (<=440px, <=480px, 481-520px, articles-index-redesign
   K8) are gone: below the breakpoint the bar is one fixed 4rem row holding
   only the logo and the menu button. */

/* List resets (article.css has no general ul reset). They sit BEFORE the nav
   breakpoint block below, so its `.article-nav-links { margin-inline: auto }`
   wins the 0-1-0 tie by source order. */
.article-nav-links,
.article-nav-resources-panel,
.article-nav-mobile-links,
.article-nav-mresources-panel {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The marketing nav looks EXACTLY like LandingNav: every value from here to
   the end of the nav block mirrors a named class constant in LandingNav.tsx,
   and navParity.test.ts reads those constants, resolves what this file gives
   each element of the rendered header, and fails when the two drift apart.
   Change them together. nowrap ONLY on the marketing lists —
   never on the bare .article-nav-link, so app mode's lone '← Back to EduPark'
   link can still wrap inside the bar. */

/* A desktop row item (DESKTOP_ITEM_CLASS): text-sm font-medium
   text-neutral-600, px-3 -mx-3 (room for the focus ring without moving the
   label), at least 40px tall, rounded-lg. The row's gap-8 is the list gap in
   the nav breakpoint block below. */
.article-nav-links .article-nav-link {
  white-space: nowrap;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--a-text-muted);
  padding: 0 0.75rem;
  margin: 0 -0.75rem;
  min-height: 2.5rem;
  border-radius: 0.5rem;
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Desktop Sign In (SIGN_IN_CLASS): px-5 py-2.5 text-sm font-semibold
   text-neutral-700, 40px tall. */
.article-nav-auth .article-nav-link {
  white-space: nowrap;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  color: var(--a-ink-2);
  padding: 0.625rem 1.25rem;
  min-height: 0;
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* hover:text-primary-600 on both. 0-3-0, so it beats the two 0-2-0 rules
   above (the base .article-nav-link:hover is 0-2-0 and declared earlier). */
.article-nav-links .article-nav-link:hover,
.article-nav-auth .article-nav-link:hover {
  color: var(--a-brand);
}

/* Below the nav breakpoint: only the menu button shows. */
.article-nav-links,
.article-nav-auth {
  display: none;
}

/* Disclosure chrome (no nav disclosure recipe exists under server/src/public). */
.article-site-header summary {
  list-style: none;
  cursor: pointer;
  border-radius: 0.5rem;
}

.article-site-header summary::-webkit-details-marker {
  display: none;
}

.article-nav-summary {
  gap: 0.25rem;
}

.article-nav-chevron {
  flex-shrink: 0;
  transition: transform 0.15s var(--a-ease);
}

.article-site-header details[open] > summary .article-nav-chevron {
  transform: rotate(180deg);
}

/* Keyboard focus: LandingNav's focus-visible:ring-2 ring-brand — a 2px ring
   on the item's own rounded box, no offset. */
.article-site-header summary:focus-visible,
.article-site-nav .article-nav-link:focus-visible,
.article-nav-item:focus-visible {
  outline: 2px solid var(--a-brand);
  outline-offset: 0;
}

/* The highlight for the page's own Resources entry (siteHeader `current`).
   0-3-0, so it beats the 0-2-0 and 0-3-0 marketing link colors above (it is
   declared after the hover rule). The SPA never marks a current item. */
.article-site-nav .article-nav-link.is-current {
  color: var(--a-brand);
}

/* Desktop Resources menu: an OPAQUE panel (the header's backdrop blur doesn't
   reach an absolutely positioned child, and the on-wash header starts clear). */
.article-nav-links .article-nav-resources {
  position: relative;
}

/* RESOURCES_PANEL_CLASS: top-full mt-2, w-64, p-2, bg-surface-raised,
   rounded-2xl, ring-1 ring-neutral-100, shadow-xl. */
.article-nav-resources-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  width: 16rem;
  padding: 0.5rem;
  background: var(--a-surface);
  border-radius: 1rem;
  box-shadow:
    0 0 0 1px var(--a-neutral-100),
    0 20px 25px -5px rgb(0 0 0 / 0.1),
    0 8px 10px -6px rgb(0 0 0 / 0.1);
}

/* A menu row: label over description, a ≥44px target. */
.article-nav-item {
  display: block;
  min-height: 2.75rem;
  font-family: var(--a-sans);
  text-decoration: none;
}

.article-nav-item-title,
.article-nav-item-desc {
  display: block;
}

/* The row's description (RESOURCE_DESC_CLASS): text-sm text-text-muted. */
.article-nav-item-desc {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--a-text-muted);
}

/* A desktop panel row (DESKTOP_RESOURCE_ROW_CLASS): rounded-xl px-3 py-2.5,
   the tappable row-list highlight (brand-subtle at 40% on hover, 60% on
   press); its label (DESKTOP_RESOURCE_LABEL_CLASS) is font-semibold text-text
   at the page's 16px. */
.article-nav-resources-panel .article-nav-item {
  padding: 0.625rem 0.75rem;
  border-radius: 0.75rem;
  color: var(--a-text);
  font-size: 1rem;
  line-height: 1.5rem;
  transition: background-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

.article-nav-resources-panel .article-nav-item:hover {
  background: rgb(234 242 255 / 0.4);
}

.article-nav-resources-panel .article-nav-item:active {
  background: rgb(234 242 255 / 0.6);
}

.article-nav-resources-panel .article-nav-item-title {
  font-weight: 600;
  color: var(--a-text);
}

.article-nav-item[aria-current="true"] .article-nav-item-title {
  color: var(--a-brand);
}

/* The phone menu button (a <summary>): LandingNav's w-11 h-11 text-neutral-700
   square with a 20px icon — the menu icon while closed, an X while open (the
   <details>' own open state, so it swaps with no script). SCOPED to the header
   (0-2-0) on purpose: a bare `.article-nav-menu-btn` (0-1-0) loses to the
   disclosure rule `.article-site-header summary` (0-1-1) above. Square
   corners, like the homepage button. */
.article-site-header .article-nav-menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  color: var(--a-ink-2);
  border-radius: 0;
}

.article-nav-mobile[open] > summary .article-nav-icon-menu,
.article-nav-mobile:not([open]) > summary .article-nav-icon-close {
  display: none;
}

/* The phone menu panel (LandingNav's phone panel: top-16, bg-surface-raised,
   border-b border-neutral-200, shadow-xl, p-4 space-y-1): full width under the
   bar, against .article-site-header-inner (position: relative) — never
   position: fixed. Scrolls when taller than the viewport (100vh fallback
   first). Every block in it sits 0.25rem apart (space-y-1). */
.article-nav-mobile-panel {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  padding: 1rem;
  background: var(--a-surface);
  border-bottom: 1px solid var(--a-neutral-200);
  box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
  max-height: calc(100vh - 4rem);
  max-height: calc(100dvh - 4rem);
  overflow-y: auto;
}

.article-nav-mobile-links,
.article-nav-mresources-panel {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* A full-width phone row (MOBILE_ROW_CLASS): py-2.5, at least 44px tall,
   text-neutral-700 font-medium at the page's 16px, flush with the panel's
   padding. No hover colour: a tapped row would keep it on a touch screen, and
   this rule (0-2-0, after the base .article-nav-link:hover) wins on hover too;
   only the current-page highlight (0-3-0) recolours a row. */
.article-nav-mobile-panel .article-nav-link {
  display: flex;
  width: 100%;
  padding: 0.625rem 0;
  min-height: 2.75rem;
  border-radius: 0;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 500;
  color: var(--a-ink-2);
}

.article-nav-mobile-panel .article-nav-summary {
  justify-content: space-between;
}

/* The phone Resources chevron is 18px (the desktop one is 16px). */
.article-nav-mobile-panel .article-nav-chevron {
  width: 1.125rem;
  height: 1.125rem;
}

/* The phone Resources rows expand IN PLACE (MOBILE_ROW_CLASS + pl-4): the
   label takes the row's weight and colour, the description is text-sm
   text-text-muted, and there is no hover tint. */
.article-nav-mresources-panel .article-nav-item {
  padding: 0.625rem 0 0.625rem 1rem;
  color: var(--a-ink-2);
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 500;
}

/* Divider (border-neutral-200), 0.25rem from the list and from Sign In. */
.article-nav-mobile-panel hr {
  border: 0;
  border-top: 1px solid var(--a-neutral-200);
  margin: 0.25rem 0;
}

/* The phone Sign In (MOBILE_SIGN_IN_CLASS): w-full py-3, centred,
   text-neutral-700 font-semibold — 48px tall. */
.article-nav-mobile-panel > .article-nav-link {
  justify-content: center;
  padding: 0.75rem 0;
  min-height: 0;
  font-weight: 600;
}

/* The full-width Start Free at the bottom of the panel (MOBILE_PRIMARY_CTA_CLASS:
   w-full py-3, font-semibold at 16px, rounded-2xl — 48px tall). 0-3-0, so it
   beats the 0-2-0 `.article-site-nav .article-nav-cta` regardless of order. */
.article-site-nav .article-nav-mobile-panel .article-nav-cta {
  display: flex;
  width: 100%;
  margin-left: 0;
  margin-top: 0.25rem;
  padding: 0.75rem 0;
  font-size: 1rem;
  line-height: 1.5rem;
}

/* The nav breakpoint = MARKETING_NAV_DESKTOP_MIN_PX (shared/constants/
   marketing.ts; LandingNav reads the same constant, and articleCssKit.test.ts
   pins this query to it). From here the full row shows: the links centered in
   the space between the brand and the auth cluster (their auto inline
   margins take the free space first), like LandingNav's three-group
   justify-between row: the bar drops its 1rem gap here so the links land on
   the same x as the homepage's, 2rem apart (gap-8), with Sign In 0.75rem off
   Start Free (gap-3). flex-end keeps app mode's lone '← Back to EduPark' link
   at the right edge. */
@media (min-width: 1024px) {
  .article-site-header-inner {
    gap: 0;
  }

  .article-site-nav {
    flex: 1;
    justify-content: flex-end;
    gap: 0;
  }

  .article-nav-links {
    display: flex;
    align-items: center;
    gap: 2rem;
    margin-inline: auto;
  }

  .article-nav-auth {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  .article-nav-mobile {
    display: none;
  }
}

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

/* Dark marketing footer (LandingFooter.tsx:55-129 reference): neutral-900
   background, a 5-column grid where the brand block spans 2, then the
   PRODUCT / COMPANY / HELP & LEGAL columns, then a copyright row. */
.article-site-footer {
  margin-top: 4rem;
  background: var(--a-neutral-900);
  color: var(--a-neutral-400);
  font-family: var(--a-sans);
}

.article-footer-inner {
  max-width: 80rem;
  margin: 0 auto;
  padding: 3rem 1rem;
}

@media (min-width: 640px) {
  .article-footer-inner { padding-left: 1.5rem; padding-right: 1.5rem; }
}

@media (min-width: 1024px) {
  .article-footer-inner { padding: 4rem 2rem; }
}

.article-footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2.5rem;
}

@media (min-width: 640px) {
  .article-footer-grid { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 1024px) {
  .article-footer-grid {
    grid-template-columns: repeat(5, 1fr);
    gap: 3rem;
  }
}

/* Brand block spans 2 of the 5 columns on lg. */
.article-footer-brand { grid-column: auto; }

@media (min-width: 1024px) {
  .article-footer-brand { grid-column: span 2 / span 2; }
}

.article-footer-brand-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

/* Footer wordmark: white 'Park', primary-400 'Edu' accent on the dark bg. */
.article-footer-brand-name {
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 1.25rem;
  color: #fff;
  line-height: 1;
}

.article-footer-brand-name .article-brand-edu { color: var(--a-primary-400); }

.article-footer-desc {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--a-neutral-400);
  max-width: 24rem;
  margin: 0;
}

.article-footer-col-title {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--a-neutral-300);
  margin: 0 0 1rem;
}

.article-footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.article-footer-col a {
  color: var(--a-neutral-400);
  text-decoration: none;
  font-size: 0.875rem;
  /* ≥44px tap target on touch, matching the header/nav and pagination links. */
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

.article-footer-col a:hover { color: #fff; }

.article-footer-bottom {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--a-neutral-800);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  font-size: 0.75rem;
  color: var(--a-neutral-500);
}

@media (min-width: 640px) {
  .article-footer-bottom {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

.article-preview-banner {
  background: var(--a-accent-subtle);
  color: rgb(146 64 14);
  text-align: center;
  padding: 0.625rem 1.25rem;
  font-weight: 600;
}

/* ----------------------------------------------------------------- page */

/*
 * The reader is a WIDE container (56rem) holding a NARROW reading column: prose
 * stays at the ~42rem Medium measure while the hero and interactive problem
 * widgets break out to the full width, so visuals use the side space the text
 * shouldn't. /exams/:code (.article-page.article-index) overrides this width for
 * its card grid; the /articles listings render in .article-library instead
 * (the Articles library section at the end of this file). The article page's
 * own <main> also carries .article-reading, which the Editorial article header
 * section at the end of this file widens to 76rem / 60rem (D21, then D23); these values
 * stay for every other .article-page.
 */
.article-page {
  max-width: 56rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 3rem;
}

/* Text-level content holds the reading measure, centered in the wide container.
   Scoped away from .article-index (/exams/:code, whose header spans the full grid width). The high
   specificity is deliberate: it must beat the per-element `.article-body p {
   margin: 0 0 1.5rem }` rules below, which would otherwise zero the auto side
   margins and left-align the column. `margin-inline` centers without disturbing
   the vertical rhythm those rules own. */
.article-page:not(.article-index) .article-header > h1,
.article-page:not(.article-index) .article-header > .article-dek,
.article-page:not(.article-index) .article-header > .article-byline-top,
.article-page:not(.article-index) .article-header > .article-tags,
.article-page:not(.article-index) .article-body > *,
.article-page:not(.article-index) .article-cta,
.article-page:not(.article-index) .article-byline-card,
.article-page:not(.article-index) .article-read-next {
  max-width: 42rem;
  margin-inline: auto;
}

/* Visuals + interactive widgets break out to the full container width. (The hero
   already fills its container; the uncapped .article-header lets it span too.) */
.article-page:not(.article-index) .article-body > .article-problem,
.article-page:not(.article-index) .article-body > .article-problem-fallback,
.article-page:not(.article-index) .article-body > figure,
.article-page:not(.article-index) .article-body > img,
.article-page:not(.article-index) .article-body > table {
  max-width: 100%;
}

.article-header h1 {
  font-family: var(--a-display);
  font-size: clamp(2rem, 5vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 800;
  margin: 0 0 0.5rem;
  overflow-wrap: break-word;
}

/* Subtitle / deck under the title (Medium-style). */
.article-dek {
  font-family: var(--a-serif);
  font-size: 1.375rem;
  line-height: 1.4;
  color: var(--a-text-muted);
  margin: 0 0 1.25rem;
}

.article-meta {
  color: var(--a-text-muted);
  font-size: 0.9375rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

/* Top author row: initials avatar + name + date · read time. */
.article-byline-top {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.5rem 0 0;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--a-border);
}

.article-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 9999px;
  background: var(--a-brand-subtle);
  color: var(--a-brand);
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 0.9375rem;
}

.article-byline-top-text {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.article-byline-top-name {
  font-family: var(--a-display);
  font-weight: 700;
  font-size: 0.9375rem;
  color: var(--a-text);
}

.article-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.875rem 0;
}

.article-tag {
  display: inline-flex;
  align-items: center;
  min-height: 2rem;
  padding: 0.125rem 0.875rem;
  border-radius: 9999px;
  background: var(--a-brand-subtle);
  color: var(--a-brand);
  font-size: 0.875rem;
  font-weight: 600;
  text-decoration: none;
}

.article-tag-active {
  background: var(--a-brand);
  color: #fff;
}

.article-hero {
  width: 100%;
  max-width: 100%;
  border-radius: 0.75rem;
  margin: 1.75rem 0 0;
}

/* ----------------------------------------------------------------- body */

/* The reading prose — serif, larger, softer ink (Medium-style). The problem
   widget and code below reset back to sans/mono so only prose is serif. */
.article-body {
  margin-top: 2rem;
  font-family: var(--a-serif);
  font-size: 1.3125rem;
  line-height: 1.7;
  color: var(--a-prose);
}

.article-body p { margin: 0 0 1.5rem; }
.article-body ul, .article-body ol { margin: 0 0 1.5rem; padding-left: 1.5rem; }
.article-body li { margin: 0.375rem 0; }
.article-body li::marker { color: var(--a-text-muted); }

/* Drop cap — only when the article opens with prose (not a heading), so a
   mid-article paragraph never gets one. */
.article-body > p:first-child::first-letter {
  float: left;
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 3.5rem;
  line-height: 0.78;
  padding: 0.28rem 0.55rem 0 0;
  color: var(--a-text);
}

.article-body h2, .article-body h3 {
  font-family: var(--a-display);
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 2.5rem 0 0.75rem;
}

.article-body h2 { font-size: 1.65rem; }
.article-body h3 { font-size: 1.3rem; }

.article-body img { max-width: 100%; height: auto; border-radius: 0.75rem; }

/* Standalone images are wrapped as <figure> by the renderer (see
   articleRenderService's standalone_image_figure rule) so the breakout rule
   above can widen them to the full container; own the vertical rhythm and
   center the image within the figure. */
.article-body figure { margin: 1.75rem 0; text-align: center; }
.article-body figcaption {
  margin-top: 0.5rem;
  font-size: 0.9375rem;
  color: var(--a-text-muted);
}

/*
 * Prose links only — button-styled anchors (.article-btn, e.g. the in-body
 * ::cta and post-answer nudge) are excluded: without :not() this rule's
 * specificity (0-1-1) beats .article-btn-primary's (0-1-0) and paints the
 * button label brand-on-brand (invisible).
 */
.article-body a:not(.article-btn) {
  color: var(--a-brand);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

/* Pull quote — large italic serif with a left rule (no box). */
.article-body blockquote {
  margin: 2rem 0;
  padding: 0.25rem 0 0.25rem 1.5rem;
  border-left: 3px solid var(--a-brand);
  font-style: italic;
  font-size: 1.5rem;
  line-height: 1.5;
  color: var(--a-text-muted);
}

.article-body blockquote p { margin: 0; }

.article-body pre {
  background: var(--a-surface-sunken);
  border: 1px solid var(--a-border);
  border-radius: 0.75rem;
  padding: 1rem;
  overflow-x: auto;
}

.article-body code {
  font-family: var(--a-mono);
  background: var(--a-surface-sunken);
  border-radius: 0.375rem;
  padding: 0.125rem 0.375rem;
  font-size: 0.875em;
}

.article-body pre code { font-size: 0.9375rem; }

.article-body pre code { background: none; padding: 0; }

/* Wide content must scroll inside its own container, never the page. */
.article-body table {
  border-collapse: collapse;
  display: block;
  max-width: 100%;
  width: fit-content;
  margin-left: auto;
  margin-right: auto;
  overflow-x: auto;
}

.article-body th, .article-body td {
  border: 1px solid var(--a-border);
  padding: 0.5rem 0.875rem;
  text-align: left;
}

.article-body th { background: var(--a-surface-sunken); }

/* ::compare block (server/src/services/articleRenderService.ts) — a
   comparison table whose data-column tint/icon is declared per article via
   ::compare{tones="..."}, not hardcoded to "left=bad, right=good".
   `.article-body th` (class+element, specificity 0,1,1) outranks a bare
   `.article-compare-header` (0,1,0) — the extra `.article-body` qualifier
   below matches it so the center-align actually wins the cascade. */
.article-body .article-compare-header { text-align: center; }

.article-compare-cell--red { background: var(--a-error-subtle); }
.article-compare-cell--green { background: var(--a-success-subtle); }
.article-compare-cell--amber { background: var(--a-accent-subtle); }
.article-compare-cell--blue { background: var(--a-brand-subtle); }
.article-compare-cell--gray { background: var(--a-gray-subtle); }

.article-compare-cell--red::before,
.article-compare-cell--green::before {
  display: inline-block;
  margin-right: 0.5em;
  font-weight: 700;
}
.article-compare-cell--red::before { content: "\2715"; color: var(--a-error); }
.article-compare-cell--green::before { content: "\2713"; color: var(--a-success); }

/* texmath display-math wrapper */
.article-body section { overflow-x: auto; }
eq { display: inline; }
eqn { display: block; }

/* -------------------------------------------------------------- buttons */

.article-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem; /* ≥44px tap target */
  padding: 0.5rem 1.5rem;
  border-radius: 9999px; /* pill */
  border: 1px solid transparent;
  font: inherit;
  font-weight: 700;
  font-size: 1rem;
  text-decoration: none;
  cursor: pointer;
}

.article-btn-primary {
  background: var(--a-brand);
  color: #fff;
}

.article-btn-primary:hover { background: var(--a-brand-hover); }

.article-btn-ghost {
  background: transparent;
  color: var(--a-brand);
  border-color: var(--a-border);
}

.article-btn-ghost:hover { background: var(--a-brand-subtle); }

/* --------------------------------------------------------- problem card */

.article-problem,
.article-problem-fallback {
  /* Reset the interactive widget back to sans — only the reading prose is
     serif. (KaTeX sets its own font-family, so math is unaffected.) */
  font-family: var(--a-sans);
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--a-text);
  border: 1px solid var(--a-border);
  border-radius: 1rem;
  padding: 1.25rem;
  margin: 1.75rem 0;
  background: var(--a-surface);
  box-shadow: 0 4px 16px rgb(10 100 248 / 0.06);
}

.article-problem-fallback { color: var(--a-text-muted); }

/* text-align centers the SVG (a replaced inline element, same technique as
   .article-body figure above) — without it the raw <svg> keeps its own
   intrinsic width and sits flush-left in a wider reading column, while its
   sibling .article-problem-svg-caption below renders its text centered
   across that same full width, visibly misaligning the figure/caption pair. */
.article-problem-svg { text-align: center; }
.article-problem-svg svg { display: inline-block; max-width: 100%; height: auto; }

/* Student-visible muted caption under a problem's SVG figure (svg_caption).
   Distinct from the figure's own aria-label (svg_description), which is
   never rendered as visible text. */
.article-problem-svg-caption {
  margin-top: 0.25rem;
  font-size: 0.875rem;
  color: var(--a-text-muted);
  text-align: center;
}

.article-problem-image { max-width: 100%; height: auto; }

.article-choice {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  min-height: 2.75rem; /* ≥44px tap target */
  padding: 0.625rem 0.875rem;
  margin: 0.5rem 0;
  border: 1px solid var(--a-border);
  border-radius: 0.75rem;
  cursor: pointer;
}

.article-choice:hover { border-color: var(--a-brand); }

.article-choice input { margin-top: 0.3rem; accent-color: var(--a-brand); }

.article-choice-letter {
  font-weight: 700;
  color: var(--a-brand);
}

.article-choice.is-correct {
  border-color: var(--a-success);
  background: var(--a-success-subtle);
}

.article-choice.is-incorrect {
  border-color: var(--a-error);
  background: var(--a-error-subtle);
}

.article-problem-actions {
  display: flex;
  gap: 0.75rem;
  margin-top: 0.875rem;
  flex-wrap: wrap;
}

.article-problem-result { font-weight: 700; }

.article-problem-result.is-correct { color: var(--a-success); }

.article-problem-result.is-incorrect { color: var(--a-error); }

.article-explanation {
  margin-top: 1rem;
  border-top: 1px solid var(--a-border);
  padding-top: 0.875rem;
}

/*
 * Problem text and explanations are BLOCK-rendered markdown (intern-bank
 * explanations use '## Observation' / '#### Step 1' headings); scale their
 * headings down to card-local sub-structure, not article-section size.
 */
.article-problem h1, .article-problem h2, .article-problem h3 {
  font-size: 1.125rem;
  margin: 1.25rem 0 0.5rem;
}

.article-problem h4, .article-problem h5, .article-problem h6 {
  font-size: 1rem;
  margin: 1rem 0 0.375rem;
}

.article-explanation summary {
  cursor: pointer;
  font-weight: 700;
  color: var(--a-brand);
  min-height: 2.75rem;
  display: flex;
  align-items: center;
}

/* ------------------------------------------------- VE walkthrough (stepper) */

/*
 * The stepwise visual-explanation walkthrough rendered inside the <details>
 * reveal (visualExplanationArticleRenderer.ts). With JS OFF every step is
 * visible and STACKED (crawl-first, S2); widget.js adds `.is-enhanced` to the
 * container + the UA `hidden` attribute to non-current steps to page through
 * them one at a time — so the one-at-a-time layout applies ONLY under
 * `.article-ve.is-enhanced`. Hiding rides on the global
 * `[hidden]{display:none!important}` rule above (author CSS can't override it).
 */
.article-ve { margin-top: 0.25rem; }

.article-ve-steps {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Stacked (no-JS / crawler) view: each step is a discrete block with a divider
   so the flat list still reads as separate steps. */
.article-ve-step {
  margin: 0 0 1.25rem;
  padding: 0 0 1.25rem;
  border-bottom: 1px solid var(--a-border);
}

.article-ve-step:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}

/* Enhanced view: the single visible step stands alone — no divider/spacing. */
.article-ve.is-enhanced .article-ve-step {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}

.article-ve-step-title {
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 1.0625rem;
  margin: 0 0 0.5rem;
  color: var(--a-text);
}

.article-ve-step-instruction { margin: 0 0 0.75rem; }

/* Bare-LaTeX work lines / equation transforms are KaTeX displayMode blocks;
   give each its own rhythm and let wide math scroll inside its own line so it
   never forces horizontal PAGE overflow. */
.article-ve-math { margin: 0.75rem 0; }

.article-ve-math-line,
.article-ve-transform-before,
.article-ve-transform-after {
  margin: 0.375rem 0;
  overflow-x: auto;
}

.article-ve-transform {
  margin: 0.75rem 0;
  padding-left: 0.875rem;
  border-left: 3px solid var(--a-brand-subtle);
}

.article-ve-transform-op {
  margin: 0.25rem 0;
  color: var(--a-text-muted);
  font-size: 0.9375rem;
}

.article-ve-concept { margin: 0.75rem 0 0; color: var(--a-text-muted); }

.article-ve-concept-term { color: var(--a-text); }

/* Keep KaTeX display blocks inside the card. overflow-y:hidden turns the block
   into a clip box, and a stacked fraction or a tall radical is taller than the
   line box KaTeX lays it in, so its numerator lost its top pixels (Albert,
   article preview 2026-09-16). The vertical padding gives the glyphs room. */
.article-ve .katex-display {
  margin: 0.375rem 0;
  padding: 0.35rem 0;
  overflow-x: auto;
  overflow-y: hidden;
}

/* ------ answer analysis (correct + trap cards; revealed on the last step) --- */

.article-ve-analysis {
  margin-top: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
}

.article-ve-correct,
.article-ve-trap {
  border: 1px solid var(--a-border);
  border-radius: 0.75rem;
  padding: 0.875rem 1rem;
}

.article-ve-correct {
  border-color: var(--a-success);
  background: var(--a-success-subtle);
}

.article-ve-trap {
  border-color: var(--a-error);
  background: var(--a-error-subtle);
}

.article-ve-trap-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.article-ve-analysis-heading {
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 1rem;
  margin: 0 0 0.625rem;
}

.article-ve-answer-label { font-weight: 700; margin: 0 0 0.25rem; }

.article-ve-key-insight { margin-top: 0.5rem; font-weight: 600; }

.article-ve-trap-misconception {
  margin-top: 0.375rem;
  color: var(--a-text-muted);
  font-size: 0.9375rem;
}

/* ---------------------------------- expert tips ---------------------------- */

.article-ve-tips {
  margin-top: 1.25rem;
  background: var(--a-accent-subtle);
  border-radius: 0.75rem;
  padding: 0.875rem 1rem;
}

.article-ve-tips-heading {
  font-family: var(--a-display);
  font-weight: 800;
  font-size: 1rem;
  margin: 0 0 0.5rem;
}

.article-ve-tip-list { margin: 0; padding-left: 1.25rem; }

.article-ve-tip { margin: 0.25rem 0; }

/* ------------------------------ Back / Next controls ----------------------- */

/* Injected by widget.js — they only exist under JS. Wrap on narrow screens and
   inherit the ≥44px tap target from .article-btn. */
.article-ve-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  margin-top: 1rem;
}

/* Disabled Back/Next (first/last step) reads as inert. */
.article-ve-controls .article-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* --------------------------- progress bar (client-injected, top) ----------- */

/*
 * widget.js injects the progress bar at the TOP of the enhanced stepper (D4) —
 * it only ever exists under JS, so every rule is scoped to .is-enhanced. Mirrors
 * the app's StepProgressBar: a brand fill over a track, with display-only step
 * dots overlaid (D2 — never buttons). Dots are capped at ≤10 steps in widget.js
 * (D7); above that the fill + label carry progress alone.
 */
.article-ve.is-enhanced .article-ve-progress {
  margin-top: 0.25rem;
  margin-bottom: 1rem;
}

.article-ve.is-enhanced .article-ve-progress-label {
  font-weight: 700;
  font-size: 0.9375rem;
  color: var(--a-text-muted);
  margin-bottom: 0.5rem;
}

/* The progress bar is a relatively-positioned box holding TWO stacked layers:
   the clipped track (fill) and an UNCLIPPED dots overlay above it. Mirrors the
   app's StepProgressBar. Stays within the embed width — never forces horizontal
   PAGE overflow. */
.article-ve.is-enhanced .article-ve-progress-bar {
  position: relative;
  height: 0.5rem;
}

/* The track holds ONLY the fill; overflow:hidden clips the fill's ends to the
   pill radius — NOT the dots, which live in the sibling overlay above and would
   otherwise be clipped into lozenges (they are taller than the 8px track). */
.article-ve.is-enhanced .article-ve-progress-track {
  position: absolute;
  inset: 0;
  background: var(--a-border);
  border-radius: 9999px;
  overflow: hidden;
}

.article-ve.is-enhanced .article-ve-progress-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  background: var(--a-brand);
  transition: width 300ms ease-in-out;
}

/* Display-only dot overlay — sits ABOVE the clipped track (not inside it) so the
   dots render as full circles, the is-current scale included. z-index promotes it
   over the positioned fill so the current dot's leading edge is never covered by
   the fill's advancing edge (fill is position:absolute; equal-z siblings would
   otherwise fall back to tree order). */
.article-ve.is-enhanced .article-ve-progress-dots {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 0.25rem;
}

/* Upcoming dot — NOTE there is no --a-surface-raised token on this surface, so
   use --a-surface (D2 note in the plan). */
.article-ve.is-enhanced .article-ve-progress-dot {
  flex: 0 0 auto;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 9999px;
  background: var(--a-surface);
  border: 1px solid var(--a-border);
  transition: transform 200ms, background 200ms;
}

.article-ve.is-enhanced .article-ve-progress-dot.is-current {
  background: var(--a-brand);
  transform: scale(1.25);
}

.article-ve.is-enhanced .article-ve-progress-dot.is-done {
  background: var(--a-brand);
}

/* --------------------------- incoming-step + analysis motion --------------- */

/*
 * Pure-CSS motion keyed off the EXISTING [hidden] display toggle — ZERO JS.
 * render() flips each step's (and the analysis block's) `hidden`
 * (display:none↔block); a display change auto-replays a CSS `animation`, so
 * :not([hidden]) fades each newly-shown step/analysis in. Opacity/transform
 * only — no layout shift.
 */
@keyframes articleVeStepIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

.article-ve.is-enhanced .article-ve-step:not([hidden]) {
  animation: articleVeStepIn 180ms ease-out;
}

/* Mirrors the app's WrongAnswerCard slideDown; animates the single
   .article-ve-analysis CONTAINER (not each card) so reduced-motion has ONE
   selector to pin. */
@keyframes articleVeSlideDown {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

.article-ve.is-enhanced .article-ve-analysis:not([hidden]) {
  animation: articleVeSlideDown 200ms ease-out;
}

/* ------------------------ reduced motion (D3, first motion gate) ----------- */

/*
 * The FIRST @media(prefers-reduced-motion) in this file, placed AFTER the VE
 * motion above so it wins. Selectors EXACTLY match the transitions/keyframes
 * above, so reduced-motion readers get the final state instantly (bar jumps,
 * dots snap, step + cards appear) with no animation.
 */
@media (prefers-reduced-motion: reduce) {
  .article-ve.is-enhanced .article-ve-progress-fill { transition: none; }
  .article-ve.is-enhanced .article-ve-progress-dot { transition: none; }
  .article-ve.is-enhanced .article-ve-step:not([hidden]) { animation: none; }
  .article-ve.is-enhanced .article-ve-analysis:not([hidden]) { animation: none; }
}

/* --------------------------------------------------------- CTA surfaces */

.article-cta,
.article-nudge {
  background: var(--a-brand-subtle);
  border: 1px solid var(--a-border);
  border-radius: 1rem;
  padding: 1.25rem;
  margin: 1.75rem 0 0;
  text-align: center;
}

.article-nudge { margin-top: 1rem; }

.article-subtle-link { margin: 1rem 0 0; }

.article-subtle-link a { color: var(--a-brand); font-weight: 600; }

.article-byline-card {
  border-top: 1px solid var(--a-border);
  margin-top: 2.5rem;
  padding-top: 1.25rem;
}

.article-byline-name { font-weight: 700; margin: 0; }

.article-byline-bio { color: var(--a-text-muted); margin: 0.25rem 0 0; }

/* --------------------------------------------------------- read next (D20) */

/* Related-articles row after the byline card — a top border sets it off from
   the article, then the index's story cards render in .article-story-grid (the
   Articles library section breaks the --wide row out of the reading measure).
   On the article page the Editorial article header section drops that border:
   the row's .ed-section-h heading draws its own hairline. */
.article-read-next {
  border-top: 1px solid var(--a-border);
  margin-top: 2.5rem;
  padding-top: 1.75rem;
}

.article-read-next-heading {
  font-family: var(--a-display);
  font-size: 1.375rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin: 0;
}

/* Matches nothing since articles-index-redesign phase 4: Read next renders
   .article-story-grid (3-across at ≥1024px on its own), not .article-card-grid.
   Kept because that plan edits no base rule (D16). Its original job: at ≥1024px
   go 3-across so the up-to-3 cards sit on ONE balanced line instead of a
   lopsided 2+1 wrap, scoped to read-next (0-2-0) so it beat the base grid's
   2-col rule. */
@media (min-width: 1024px) {
  .article-read-next .article-card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ---------------------------------------------------------------- index */
/* Since articles-index-redesign phase 4 the /articles listings render in
   .article-library (the Articles library section); these rules style
   /exams/:code's .article-index page and its feature cards. */

.article-index-subtitle { color: var(--a-text-muted); font-size: 1.0625rem; }

.article-card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  margin-top: 1.5rem;
}

@media (min-width: 640px) {
  .article-index { max-width: 52rem; }
  .article-card-grid { grid-template-columns: 1fr 1fr; }
}

/* Desktop: widen to the home content width (max-w-6xl) and go 3-across so the
   card grid fills the side space like the landing page. */
@media (min-width: 1024px) {
  .article-index { max-width: 72rem; }
  .article-card-grid { grid-template-columns: repeat(3, 1fr); }
}

.article-card {
  display: block;
  border: 1px solid var(--a-border);
  border-radius: 1rem;
  padding: 1.25rem;
  text-decoration: none;
  color: inherit;
  box-shadow: 0 4px 16px rgb(10 100 248 / 0.06);
}

.article-card:hover { border-color: var(--a-brand); }

.article-card h2,
.article-card h3 {
  font-family: var(--a-display);
  font-size: 1.25rem;
  margin: 0 0 0.375rem;
  /* Clamp long titles to 2 lines (like .article-card-desc) so a wordy headline
     never wraps to 3+ lines and desyncs card heights across the grid — the
     /exams/:code feature cards (h2); the /articles listings and Read next use
     .article-story-title instead. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.article-card-desc {
  color: var(--a-text-muted);
  margin: 0 0 0.625rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.article-card-meta {
  color: var(--a-text-muted);
  font-size: 0.875rem;
  margin: 0;
}

.article-empty {
  text-align: center;
  color: var(--a-text-muted);
  padding: 3rem 0;
}

/* -------------------------------------------------- index search + paging */
/* Shared with /whats-new. The /articles listings use the compact masthead
   search and restyle pagination only under .article-library. */

.article-search {
  display: flex;
  gap: 0.5rem;
  margin: 1.25rem 0 0.5rem;
}

.article-search-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--a-border);
  border-radius: 9999px;
  background: var(--a-surface);
  color: var(--a-text);
  font: inherit;
}

.article-search-input:focus-visible {
  outline: 2px solid var(--a-brand);
  outline-offset: 1px;
}

.article-search-btn { flex: 0 0 auto; }

.article-results-count {
  color: var(--a-text-muted);
  font-size: 0.9375rem;
  margin: 1rem 0 0;
}

.article-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin-top: 2rem;
}

.article-page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 2.25rem;
  padding: 0.25rem 0.625rem;
  border: 1px solid var(--a-border);
  border-radius: 0.5rem;
  color: var(--a-brand);
  font-weight: 600;
  text-decoration: none;
}

.article-page-link:hover { border-color: var(--a-brand); }

.article-page-current {
  background: var(--a-brand);
  border-color: var(--a-brand);
  color: #fff;
}

.article-page-ellipsis {
  padding: 0 0.25rem;
  color: var(--a-text-muted);
}

/* Error pages hold the reading measure, not the 56rem article width — their
   short, centered copy shouldn't stretch across the wide container. */
.article-error-page { max-width: 42rem; text-align: center; padding-top: 4rem; }

.article-error-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  flex-wrap: wrap;
}

/* ---------------------------------------------------------------- Editorial kit (ed-*) */
/* Shared by /articles (plan articles-index-redesign) and /whats-new (plan whats-new-editorial-redesign).
   Presentation-only classes; page styles compose them. Motion is finite (nothing animates past 5s, WCAG 2.2.2)
   and every animation/transition except the scroll-linked ed-header-solidify stops under prefers-reduced-motion.
   Source: docs/articles/design/index-redesign/kit.css, ported verbatim by articles-index-redesign phase 1. */
/* Tints: one per hub (and per What's New issue). wash = backgrounds, deep = text/ink on the wash (all >= 4.5:1 on white),
   deep-line = deep at 18% for hairlines. Every tint-dependent piece (.ed-wash, .ed-blob-a, .ed-cover::before, .ed-photo,
   .ed-pill-dot, .ed-kicker) needs an .ed-tint-* on itself or an ancestor. No color-mix() anywhere: older Safari/Chrome drop
   the whole declaration. */
.ed-tint-cobalt   { --wash: #eaf2ff; --wash-2: #d4e4ff; --deep: #0756df; --deep-line: rgb(7 86 223 / .18); }
.ed-tint-gold     { --wash: #fffaeb; --wash-2: #fff3c6; --deep: #8a6700; --deep-line: rgb(138 103 0 / .18); }
.ed-tint-mint     { --wash: #e9faf1; --wash-2: #d3f5e4; --deep: #0f766e; --deep-line: rgb(15 118 110 / .18); }
.ed-tint-peach    { --wash: #fef1ea; --wash-2: #fde1d3; --deep: #b4461b; --deep-line: rgb(180 70 27 / .18); }
.ed-tint-lavender { --wash: #f3edfb; --wash-2: #e6d9f7; --deep: #5b3fa8; --deep-line: rgb(91 63 168 / .18); }
.ed-tint-sky      { --wash: #eaf8fb; --wash-2: #d1f0f5; --deep: #0e6f86; --deep-line: rgb(14 111 134 / .18); }

.ed-sparkle { fill: var(--a-accent); }

.ed-wash { position: absolute; inset: 0 calc(50% - 50vw); z-index: -1; overflow: hidden;
  background: linear-gradient(180deg, var(--wash) 0%, rgb(255 255 255 / 0) 100%); }
.ed-blob { position: absolute; border-radius: 9999px; filter: blur(60px); opacity: .55; }
.ed-blob-a { width: 34rem; height: 20rem; left: 8%; top: -6rem; background: var(--wash-2); animation: ed-drift 5s ease-in-out 1 both; }
.ed-blob-b { width: 22rem; height: 14rem; right: 10%; top: 2rem; background: var(--a-accent-subtle); animation: ed-drift 4.6s ease-in-out .4s 1 reverse both; }
/* Motion is FINITE (drift once, twinkle twice): nothing moves for more than 5s, so no pause control is needed (WCAG 2.2.2). */
@keyframes ed-drift { from { transform: translate3d(-4%, 0, 0) } to { transform: translate3d(6%, 8%, 0) } }

.ed-meta { margin: 0; display: inline-flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  font: 700 .75rem/1.2 var(--a-sans); letter-spacing: .12em; text-transform: uppercase; color: var(--a-ink-2);
  font-variant-numeric: tabular-nums; }
.ed-meta-sep { color: var(--a-subtle); }

.ed-kicker { font: 700 .75rem/1.2 var(--a-sans); letter-spacing: .14em; text-transform: uppercase; color: var(--deep, var(--a-primary-700)); }
.ed-dot { color: var(--a-subtle); margin: 0 .375rem; }

.ed-section-h { margin: 0 0 1.25rem; font: 700 .8125rem/1 var(--a-sans); letter-spacing: .16em; text-transform: uppercase;
  color: var(--a-ink-2); display: flex; align-items: center; gap: .75rem; }
.ed-section-h::after { content: ""; flex: 1; height: 1px; background: var(--a-border); }

.ed-tagline { margin: 0 auto; text-align: center; font: 700 .8125rem/1.5 var(--a-sans); letter-spacing: .2em;
  text-transform: uppercase; color: var(--a-ink-2); text-wrap: balance; }
.ed-tagline .ed-sparkle { width: .8rem; height: .8rem; vertical-align: -1px; margin: 0 .35rem; }

/* Pill row — What's New's segmented switch, generalised to N links that WRAP (flex-wrap; each pill is
   flex: 0 0 auto with white-space: nowrap, so a pill never shrinks — the row breaks onto a new line instead). */
.ed-pills { display: flex; flex-wrap: wrap; justify-content: center; gap: .375rem; padding: .3rem;
  max-width: max-content; margin: 0 auto;
  background: rgb(255 255 255 / .85); border-radius: 1.5rem; box-shadow: 0 1px 2px rgb(0 0 0 / .05), 0 0 0 1px var(--a-line-2); }
.ed-pill { flex: 0 0 auto; display: inline-flex; align-items: center; gap: .45rem; min-height: 2.75rem; padding: 0 1rem;
  border-radius: 9999px; color: var(--a-ink-2); text-decoration: none; white-space: nowrap;
  font: 600 .875rem/1 var(--a-sans); transition: background .2s, color .2s; }
.ed-pill:hover { background: var(--a-brand-subtle); color: var(--a-primary-700); }
.ed-pill.is-active { background: var(--a-brand); color: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .1); }
.ed-pill-dot { width: .5rem; height: .5rem; border-radius: 9999px; background: var(--deep); flex: none; }
.ed-pill.is-active .ed-pill-dot { background: #fff; }
.ed-pill:focus-visible { outline: 2px solid var(--a-primary-700); outline-offset: 2px; }
.ed-pill.is-active:focus-visible { box-shadow: 0 0 0 2px #fff; }

/* Cover index-card (shared with the What's New cover story). */
.ed-cover { position: relative; display: block; margin: .5rem auto 4rem; max-width: 60rem; color: inherit; text-decoration: none; }
.ed-cover::before { content: ""; position: absolute; inset: .5rem -.5rem -.75rem .75rem; border-radius: 1.75rem;
  background: linear-gradient(135deg, var(--wash-2), var(--a-brand-100)); transform: rotate(1.6deg); z-index: 0;
  transition: transform .5s var(--a-ease); }
.ed-cover:hover::before { transform: rotate(2.4deg) translate(4px, 2px); }
/* Focus ring on the card, not the link box: the link's own outline would paint under the tilted ::before backing card. */
.ed-cover:focus-visible { outline: none; }
.ed-cover:focus-visible .ed-cover-card { outline: 3px solid var(--a-brand); outline-offset: 4px; }
.ed-cover-card { position: relative; padding: 2rem 1.5rem 2rem 2.5rem; border-radius: 1.5rem; background:
    linear-gradient(90deg, transparent 1.5rem, rgb(251 205 9 / .7) 1.5rem, rgb(251 205 9 / .7) calc(1.5rem + 2px), transparent calc(1.5rem + 2px)),
    repeating-linear-gradient(180deg, transparent 0 2.05rem, rgb(10 100 248 / .07) 2.05rem calc(2.05rem + 1px)),
    #fff;
  box-shadow: 0 30px 60px -30px rgb(1 61 181 / .35), 0 0 0 1px var(--a-line-2); z-index: 1;
  transition: transform .5s var(--a-ease), box-shadow .5s var(--a-ease); }
.ed-cover:hover .ed-cover-card { transform: translateY(-4px) rotate(-.6deg); box-shadow: 0 40px 70px -30px rgb(1 61 181 / .4), 0 0 0 1px var(--a-line-2); }
@media (min-width: 768px) { .ed-cover-card { padding: 3rem 3.5rem 2.5rem 4.25rem; }
  .ed-cover-card { background:
    linear-gradient(90deg, transparent 1.75rem, rgb(251 205 9 / .7) 1.75rem, rgb(251 205 9 / .7) calc(1.75rem + 2px), transparent calc(1.75rem + 2px)),
    repeating-linear-gradient(180deg, transparent 0 2.05rem, rgb(10 100 248 / .07) 2.05rem calc(2.05rem + 1px)), #fff; } }
.ed-cover-title { margin: 1rem 0 1.25rem; font: 800 clamp(2rem, 5.4vw, 3.75rem)/1.04 var(--a-display);
  letter-spacing: -.035em; color: var(--a-text); text-wrap: balance; overflow-wrap: break-word; }
.ed-cover-dek { margin: 0; max-width: 40rem; font: 400 clamp(1.0625rem, 2.2vw, 1.3125rem)/1.6 var(--a-serif); color: var(--a-ink-2); }
.ed-cover-meta { margin: 1.5rem 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: .25rem 1rem;
  font: 600 .875rem/1.3 var(--a-sans); color: var(--a-text-muted); }
.ed-cover-cta { color: var(--a-brand); }
.ed-cover:hover .ed-cover-cta { text-decoration: underline; text-underline-offset: 3px; }

/* Gold highlighter stroke. Width is FIXED (not the title's width) so it reads as a flourish under
   titles of any length/line count. Draws in once on load — CSS only, no JS. */
.ed-underline { display: block; width: min(100%, 16rem); height: .875rem; margin-top: .375rem; overflow: visible; }
.ed-underline path { fill: none; stroke: var(--a-accent); stroke-width: 9; stroke-linecap: round;
  stroke-dasharray: 700; stroke-dashoffset: 0; animation: ed-draw 1.1s var(--a-ease) .35s both; }
@keyframes ed-draw { from { stroke-dashoffset: 700 } to { stroke-dashoffset: 0 } }

.ed-sticker-star { position: absolute; display: grid; place-items: center; border-radius: 9999px;
  width: 4.5rem; height: 4.5rem; right: -.75rem; bottom: -1.25rem; background: var(--a-brand);
  box-shadow: 0 10px 24px -8px rgb(1 61 181 / .55), inset 0 -3px 0 rgb(0 0 0 / .15); transform: rotate(10deg); }
.ed-sticker-star .ed-sparkle { width: 2.25rem; height: 2.25rem; animation: ed-twinkle 1.6s ease-in-out 2; }
@keyframes ed-twinkle { 0%, 100% { transform: scale(1) rotate(0) } 50% { transform: scale(.8) rotate(18deg) } }

/* Framed image on a tilted tinted backing card (branded graphics — NOT the What's New browser frame,
   which is reserved for real product captures). */
.ed-photo { position: relative; margin: 0; }
.ed-photo::before { content: ""; position: absolute; inset: .75rem -.625rem -.625rem .75rem; border-radius: 1.25rem;
  background: linear-gradient(135deg, var(--wash-2), var(--a-brand-100)); transform: rotate(2deg); z-index: 0; }
.ed-photo img { position: relative; z-index: 1; display: block; width: 100%; height: auto; aspect-ratio: 1200 / 630; object-fit: cover;
  border-radius: 1rem; background: var(--wash); box-shadow: 0 24px 44px -24px rgb(1 61 181 / .38), 0 0 0 1px rgb(22 26 36 / .06); }

/* Cover WITH a hero: the framed image overlaps the card's right edge (>= 1024px) or tucks under it (below). */
.ed-cover.has-media { max-width: 76rem; }
.ed-cover-media { position: relative; z-index: 2; margin: -1.25rem .75rem 0 1.5rem; transform: rotate(1.4deg); transition: transform .5s var(--a-ease); }
.ed-cover:hover .ed-cover-media { transform: rotate(0) translateY(-4px); }
@media (max-width: 1023px) { .ed-cover.has-media::before { display: none; } .ed-cover-media { max-width: 36rem; margin-left: auto; } .ed-sticker-star { right: .75rem; } }   /* the sticker tucks inside the card below 1024px so it never pokes past a phone's gutter */
@media (min-width: 1024px) {
  .ed-cover.has-media { display: grid; grid-template-columns: minmax(0, 11fr) minmax(0, 10fr); align-items: center; }
  .ed-cover.has-media::before { right: 42%; }
  .ed-cover.has-media .ed-cover-title { font-size: clamp(2rem, 3.6vw, 3rem); }
  .ed-cover-media { margin: 0 0 0 -3.5rem; transform: rotate(2.2deg); }
}

/* Site header on a wash — opt-in via siteHeader({ onWash: true }) on pages whose first block is a full-bleed wash.
   The page runs its wash up behind the sticky header (each page owns that rule). Fallback (no scroll-driven
   animations, e.g. Firefox): a half-frosted header, readable over any content, with no hairline. Where
   animation-timeline is supported: fully clear at the top of the page, solidifying over the first 6rem of scroll
   to a frosted bar with the standard bar's 24px blur and hairline (article.css .article-site-header), in white at
   .85 where the standard bar is .8. Scroll-linked, not time-based: nothing moves on its own. CSS only — no JS. */
.article-site-header--on-wash { background: rgb(255 255 255 / .5); border-bottom-color: transparent; }
@supports (animation-timeline: scroll()) {
  .article-site-header--on-wash { animation: ed-header-solidify linear both; animation-timeline: scroll(root block); animation-range: 0 6rem; }
}
@keyframes ed-header-solidify {
  from { background: rgb(255 255 255 / 0); border-bottom-color: transparent; -webkit-backdrop-filter: blur(0); backdrop-filter: blur(0); }
  to { background: rgb(255 255 255 / .85); border-bottom-color: var(--a-neutral-100); -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
}

@media (prefers-reduced-motion: reduce) {
  .ed-cover-media { transition: none; }
  .ed-blob, .ed-sticker-star .ed-sparkle { animation: none; }
  .ed-underline path { animation: none; stroke-dashoffset: 0; }
  .ed-cover::before, .ed-cover-card, .ed-pill { transition: none; }
}
/* ---------------------------------------------------------------- end Editorial kit (ed-*) */

/* ---- Articles library (index, hubs, search, Read next) */
/* articles-index-redesign phase 4: docs/articles/design/index-redesign/pages.css SECTION A, ported whole after the
   Editorial kit so source order matches the mockup (base → kit → pages). Composes ed-* classes; never redefines them.
   Shared /whats-new and /exams/:code classes are restyled ONLY under .article-library (the pagination and empty-state rules);
   articleRoutes.test.ts (d) checks every selector here mechanically. */
/* Library container: every listing layout renders in <main class="article-library"> */
/* Same width and gutters as the site header's inner row (article.css .article-site-header-inner: 80rem; 1rem /
   1.5rem ≥640px / 2rem ≥1024px), so the masthead's left and right edges line up with the logo and the CTA. */
.article-library { max-width: 80rem; margin: 0 auto; padding: 0 1rem 4rem; }
@media (min-width: 640px) { .article-library { padding: 0 1.5rem 4rem; } }
@media (min-width: 1024px) { .article-library { padding: 0 2rem 5rem; } }

/* The masthead wash must start at the very top of the page, behind the kit's clear .article-site-header--on-wash
   header, whose height varies (4rem, 5rem ≥1280px). Overshoot instead of matching it: anything above the top of the document is never
   visible or scrollable. */
.article-masthead > .ed-wash { top: -12rem; }

/* Masthead */
.article-masthead { position: relative; isolation: isolate; padding: 1.25rem 0 2.5rem; margin-bottom: 1rem; }
.article-masthead-bar { display: flex; justify-content: space-between; align-items: center; gap: .75rem 1.5rem; flex-wrap: wrap;
  padding-bottom: .875rem; border-bottom: 1px solid var(--deep-line); }
.article-masthead-title { margin: 2rem 0 .75rem; text-align: center; font: 800 clamp(2.75rem, 9vw, 6.25rem)/.95 var(--a-display);
  letter-spacing: -.045em; color: var(--a-text); text-wrap: balance; }
.article-masthead-accent { position: relative; color: var(--a-brand); white-space: nowrap; }
.article-masthead-accent .ed-sparkle { position: absolute; width: .3em; height: .3em; right: -.36em; top: -.02em;
  animation: ed-twinkle 1.6s ease-in-out 2; }
.article-masthead .ed-tagline { margin-bottom: 1.75rem; }
@media (max-width: 479px) { .article-masthead .ed-tagline { display: none; } .article-masthead-title { margin-bottom: 1.5rem; } }
/* On phones FRONT's hidden tagline is still the masthead's last child, so the h1 above it gives up its margin instead
   and FRONT ends as close to the cover as every other library masthead ends to its content. */
@media (max-width: 479px) { .article-masthead-title:has(+ .ed-tagline:last-child) { margin-bottom: 0; } }
@media (prefers-reduced-motion: reduce) { .article-masthead-accent .ed-sparkle { animation: none; } }

/* Hub-style masthead variant (LIST, SEARCH, LEGACY): left-aligned title and the meta line */
.article-masthead--hub .article-masthead-title { text-align: left; margin: 1.75rem 0 1rem; font-size: clamp(2.5rem, 7vw, 4.75rem); overflow-wrap: anywhere; }
.article-masthead--hub .ed-meta { color: var(--deep); }
/* Only the meta line under the h1, not the crumb row in the bar, so the bar is the same height as FRONT's. */
.article-masthead--hub > .ed-meta { margin-bottom: 1.75rem; }
.article-crumbs { display: inline-flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.article-crumbs a { display: inline-flex; align-items: center; min-height: 2.75rem; color: var(--a-ink-2); text-decoration: none; }
.article-crumbs a:hover { color: var(--a-brand); }
.article-crumbs .is-hub { color: var(--deep); }
.article-crumbs-sep { color: var(--a-subtle); }

/* Topic masthead (/articles/topics/<hub>): the FRONT masthead's layout in the hub's tint (Albert, 2026-09-23, post-PR),
   with the pill row under the bar like every library masthead, so switching pills never moves it.
   The meta row carries the crumb and the count; the intro sits where the front page's tagline does and stays visible at every width. */
.article-masthead--topic .article-masthead-title { overflow-wrap: anywhere; }
.article-masthead--topic .article-masthead-accent { color: var(--deep); }
.article-masthead--topic .article-hub-intro { margin: 0 auto 1.75rem; max-width: 42rem; text-align: center; text-wrap: balance;
  font: 400 clamp(1.0625rem, 2.2vw, 1.3125rem)/1.6 var(--a-serif); color: var(--a-ink-2); }
/* The library renders still (Albert, 2026-09-23, post-PR): switching pills never replays motion, and CSS can't tell a
   first visit from a click on All without JavaScript. The blobs rest where ed-drift leaves them (blob-a ends on `to`,
   blob-b runs in reverse and ends on `from`), in every motion setting; the underline and sparkles are fully drawn.
   Same specificity as the accent-sparkle rule above, and later, so this wins. The kit's own rules are untouched, so
   other pages that compose the kit keep its motion. */
.article-library .ed-blob-a { animation: none; transform: translate3d(6%, 8%, 0); }
.article-library .ed-blob-b { animation: none; transform: translate3d(-4%, 0, 0); }
.article-library .ed-underline path, .article-library .ed-sparkle { animation: none; }
/* The bar's meta row is as tall as a crumb link (44px) on every layout, so on phones, where the bar wraps, the search
   field sits at the same height on every library page whose meta row fits on one line. */
.article-masthead-bar > .ed-meta { min-height: 2.75rem; }
/* The meta row never wraps onto a second line, which would push the tab bar down on phones: a long crumb label ("For
   educators & schools") wraps inside itself instead, and up to three lines of this 12px text stay within the 44px row. */
.article-masthead-bar > .ed-meta { flex-wrap: nowrap; }
.article-masthead-bar > .ed-meta > * { min-width: 0; }
.article-masthead-bar > .ed-meta > .ed-meta-sep, .article-masthead-bar > .ed-meta > .ed-meta-sep + * { flex: none; white-space: nowrap; }
.article-masthead-bar .article-crumbs { flex-wrap: nowrap; }
.article-masthead-bar .article-crumbs > a, .article-masthead-bar .article-crumbs-sep { flex: none; }
.article-masthead-bar .article-crumbs > [aria-current] { min-width: 0; }
/* The pill row is a tab bar (Albert, 2026-09-23, post-PR): it sits directly under the bar, centered, on every library
   page. The bar is the same height everywhere, so switching pages never moves it; only the headline and what follows
   change. Whatever ends the masthead (the tagline, the intro, the meta line) gives up its bottom margin to the
   masthead's own padding. */
.article-masthead > .ed-pills { margin-top: 1.25rem; }
.article-masthead > :last-child { margin-bottom: 0; }

/* Compact search (GET form, no JS) */
.article-search-compact { display: flex; align-items: center; gap: .25rem; height: 3.125rem; padding: 0 .1875rem 0 .875rem;
  flex: 0 1 17rem; min-width: 0;
  border-radius: 9999px; background: rgb(255 255 255 / .9); box-shadow: 0 0 0 1px var(--a-border); }
.article-search-compact:focus-within { box-shadow: 0 0 0 2px var(--a-brand); }
.article-search-compact svg { width: 1rem; height: 1rem; flex: none; stroke: var(--a-subtle); fill: none; stroke-width: 2; stroke-linecap: round; }
.article-search-compact input { flex: 1; min-width: 0; height: 100%; border: 0; background: transparent; outline: none;
  font: 500 .9375rem/1 var(--a-sans); color: var(--a-text); }
.article-search-compact input::placeholder { color: var(--a-subtle); }
.article-search-compact button { flex: none; display: grid; place-items: center; width: 2.75rem; height: 2.75rem; border: 0; border-radius: 9999px;
  background: var(--a-brand); color: #fff; cursor: pointer; font: 700 .9375rem/1 var(--a-sans); }
.article-search-compact button:hover { background: var(--a-brand-hover); }
@media (max-width: 639px) { .article-masthead-bar { justify-content: center; } .article-search-compact { flex: 1 1 100%; } }
/* From 640px the bar is one row on every library page: the search keeps its full 17rem and a long topic label wraps
   inside the meta row, so the bar never wraps and the tab bar under it never moves (at 640-725px it used to wrap on
   "For educators & schools"). */
@media (min-width: 640px) { .article-masthead-bar { flex-wrap: nowrap; } .article-search-compact { flex-shrink: 0; } }

/* Articles-only cover tweak: on a cover WITH a hero the image takes the sticker's role (What's New keeps its sticker). */
.article-cover.has-media .ed-sticker-star { display: none; }

/* Plain text links in page chrome (Clear search, All articles, Browse all articles): 44px tap target. */
.article-text-link { display: inline-flex; align-items: center; min-height: 2.75rem; color: var(--a-brand); font: 600 .9375rem/1.2 var(--a-sans); text-decoration: none; letter-spacing: 0; text-transform: none; }
.article-text-link:hover { text-decoration: underline; text-underline-offset: 3px; }

/* Hub kicker (card + cover + article header) */
.article-hub-kicker { font: 700 .75rem/1.2 var(--a-sans); letter-spacing: .12em; text-transform: uppercase; color: var(--deep); }

/* Story cards — the ONE card renderer for the index grid, hub pages and Read-next */
.article-stories { margin: 0 0 3.5rem; }
.article-story-grid { display: grid; gap: 1.5rem; }
@media (min-width: 640px) { .article-story-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .article-story-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.75rem; } }
.article-story-grid--pair { margin-bottom: 1.75rem; }
@media (min-width: 1024px) { .article-story-grid--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.article-story { display: flex; flex-direction: column; min-width: 0; border-radius: 1.5rem; overflow: hidden; background: #fff;
  color: inherit; text-decoration: none;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / .05), 0 0 0 1px var(--a-line-2);
  transition: transform .35s var(--a-ease), box-shadow .35s var(--a-ease); }
.article-story:hover { transform: translateY(-3px) rotate(-.4deg); box-shadow: 0 18px 30px -16px rgb(1 61 181 / .28), 0 0 0 1px var(--a-brand-100); }
.article-story:focus-visible, .article-hub-tile:focus-visible { outline: 3px solid var(--a-brand); outline-offset: 4px; }
.article-story-media { position: relative; aspect-ratio: 1200 / 630; background: var(--wash); overflow: hidden; }
.article-story-media img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--a-ease); }
.article-story:hover .article-story-media img { transform: scale(1.03); }
/* No hero: a tinted hub panel with a big faint sparkle and the hub name */
.article-story-fallback { display: flex; align-items: flex-end; padding: 1.125rem 1.25rem;
  background: radial-gradient(120% 120% at 100% 0%, var(--wash-2) 0%, var(--wash) 62%); }
.article-story-fallback .ed-sparkle { position: absolute; right: -1rem; top: -1.25rem; width: 8rem; height: 8rem; fill: var(--deep); opacity: .13; }
.article-story-fallback .ed-sparkle + .ed-sparkle { right: auto; left: 38%; top: 28%; width: 2rem; height: 2rem; fill: var(--a-accent); opacity: 1; }
.article-story-fallback span { font: 800 1.125rem/1.1 var(--a-display); letter-spacing: -.01em; color: var(--deep); }
.article-story-body { display: flex; flex-direction: column; gap: .5rem; padding: 1.125rem 1.25rem 1.25rem; flex: 1; }
.article-story-title { margin: 0; font: 800 1.1875rem/1.22 var(--a-display); letter-spacing: -.015em; color: var(--a-text); text-wrap: balance;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: break-word; }
.article-story-desc { margin: 0; font: 400 .9375rem/1.55 var(--a-serif); color: var(--a-ink-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.article-story-exam { display: inline-block; margin-left: .5rem; padding: .125rem .5rem; border-radius: 9999px; background: var(--a-surface-sunken); color: var(--a-ink-2); font: 700 .6875rem/1.4 var(--a-sans); letter-spacing: .06em; text-transform: uppercase; }
.article-story-meta { margin: auto 0 0; padding-top: .375rem; font: 600 .8125rem/1.3 var(--a-sans); color: var(--a-text-muted); font-variant-numeric: tabular-nums; }
/* The pair (page 1, cards 2–3) — bigger type */
.article-story--wide .article-story-body { padding: 1.375rem 1.5rem 1.5rem; }
.article-story--wide .article-story-title { font-size: clamp(1.3125rem, 2.2vw, 1.625rem); }
.article-story--wide .article-story-desc { font-size: 1.0625rem; -webkit-line-clamp: 3; }

/* Browse by topic */
.article-hubs { margin: 1rem 0 0; }
.article-hub-grid { display: grid; gap: 1.25rem; }
@media (min-width: 640px) { .article-hub-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .article-hub-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.article-hub-tile { display: flex; flex-direction: column; border-radius: 1.5rem; overflow: hidden; background: #fff; color: inherit; text-decoration: none;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / .05), 0 0 0 1px var(--a-line-2); transition: transform .35s var(--a-ease), box-shadow .35s var(--a-ease); }
.article-hub-tile:hover { transform: translateY(-4px) rotate(-.6deg); box-shadow: 0 20px 34px -18px rgb(1 61 181 / .3), 0 0 0 1px var(--a-border); }
.article-hub-tile-art { position: relative; height: 6.5rem; padding: 1rem 1.25rem; display: flex; align-items: flex-start; overflow: hidden;
  background: radial-gradient(120% 120% at 100% 0%, var(--wash-2) 0%, var(--wash) 60%); }
.article-hub-tile-art .ed-sparkle { position: absolute; right: -.75rem; bottom: -1.25rem; width: 6rem; height: 6rem; fill: var(--deep); opacity: .12;
  transition: transform .5s var(--a-ease); }
.article-hub-tile:hover .article-hub-tile-art .ed-sparkle { transform: rotate(25deg) scale(1.1); }
.article-hub-tile-count { font: 700 .75rem/1 var(--a-sans); letter-spacing: .12em; text-transform: uppercase; color: var(--deep); }
.article-hub-tile-body { display: flex; flex-direction: column; gap: .5rem; padding: 1rem 1.25rem 1.25rem; flex: 1; }
.article-hub-tile-name { margin: 0; font: 800 1.25rem/1.2 var(--a-display); letter-spacing: -.015em; color: var(--a-text); }
.article-hub-tile-blurb { margin: 0; font: 400 .9375rem/1.5 var(--a-serif); color: var(--a-ink-2); }
.article-hub-tile-cta { margin-top: auto; padding-top: .25rem; font: 600 .875rem/1 var(--a-sans); color: var(--a-brand); }

/* Pagination: the classes are shared with /whats-new, so every rule is scoped under .article-library */
.article-library .article-pagination { display: flex; justify-content: center; flex-wrap: wrap; gap: .5rem; margin: 0 0 3.5rem; }
.article-library .article-page-link, .article-library .article-page-current { display: inline-grid; place-items: center; min-width: 2.75rem; height: 2.75rem; padding: 0 .875rem;
  border-radius: 9999px; font: 600 .9375rem/1 var(--a-sans); text-decoration: none; }
.article-library .article-page-link { color: var(--a-ink-2); box-shadow: 0 0 0 1px var(--a-border); }
.article-library .article-page-link:hover { color: var(--a-brand); box-shadow: 0 0 0 1px var(--a-brand); }
.article-library .article-page-current { background: var(--a-brand); color: #fff; }

/* Empty state: also shared with /whats-new, so scoped the same way. The search no-results line echoes the reader's
   query, and a 64-character query (ARTICLE_SEARCH_MAX_LENGTH) with no break opportunity would otherwise widen the page
   past a 375px viewport. It wraps like the search h1 (.article-masthead--hub .article-masthead-title). */
.article-library .article-empty { overflow-wrap: anywhere; }

/* Read next (article page): three story cards across the full article container (76rem since D23) */
.article-page:not(.article-index) .article-read-next.article-read-next--wide { max-width: 100%; }
.article-read-next-heading.ed-section-h { font-family: var(--a-sans); font-size: .8125rem; margin-bottom: 1.25rem; }

@media (prefers-reduced-motion: reduce) {
  .article-story, .article-story-media img, .article-hub-tile, .article-hub-tile-art .ed-sparkle { transition: none; }
  .article-story:hover, .article-story:hover .article-story-media img, .article-hub-tile:hover, .article-hub-tile:hover .article-hub-tile-art .ed-sparkle { transform: none; }
}

/* ---- Editorial article header */
/* articles-index-redesign phase 6: docs/articles/design/index-redesign/pages.css SECTION B, ported after the Articles
   library section so source order matches the mockup (base → kit → pages), plus the two overrides at the end. Composes
   ed-* classes (.ed-tint-*, .ed-underline, .ed-photo); never redefines them. Every selector carries .article-reading (only
   the article page's <main>) or .article-header--editorial (only its header), except the draft-preview banner rule, and
   articleRoutes.test.ts (p6) checks that mechanically, so the branded 404/500, /exams/:code and /whats-new are untouched. */
/* Wider reading page (Albert 2026-09-23: "the articles are too skinny", then, after the PR, "still too small"). The
   article page's <main> carries .article-reading (the 404/500 pages share .article-page and keep today's width). Prose
   42rem → 48rem (D21) → 60rem (D23), and the container that hero, problems, tables and Read next break out to 56rem →
   68rem → 76rem. From 880px wide the body text grows with the column, 21px up to 24px once the column is its full
   960px (at 1000px), so a full line stays at about 89 characters (Albert, 2026-09-23: 22px text gave about 97). The
   prose h2/h3 scale with it (direct children only, so a problem widget's own headings keep its 1.125rem), and so does
   the pull quote, which keeps its 8:7 size over the text. 2.5vw - 1px is the column width (100vw less .article-page's
   2.5rem gutters) over 40, and this serif runs about 0.45em a character, so width / font-size stays near 89 characters
   (between 880 and about 1015px, a couple fewer beside a classic, non-overlay scrollbar, which 100vw counts; above
   that the column and the text are both at their caps). These repeat the base reading-measure selectors (the
   `.article-page:not(.article-index) .article-header > h1, …` block in the page section above) with .article-reading,
   so they tie on specificity and win on source order; no base rule is edited. (`.article-tags` no longer renders on the article page since phase 6; it
   stays in the list so the list mirrors the base block one for one.) */
.article-page.article-reading { max-width: 76rem; }
.article-page.article-reading .article-header > h1,
.article-page.article-reading .article-header > .article-dek,
.article-page.article-reading .article-header > .article-byline-top,
.article-page.article-reading .article-header > .article-tags,
.article-page.article-reading .article-body > *,
.article-page.article-reading .article-cta,
.article-page.article-reading .article-byline-card,
.article-page.article-reading .article-read-next { max-width: 60rem; }
@media (min-width: 880px) {
  .article-page.article-reading .article-body { font-size: clamp(1.3125rem, 2.5vw - 1px, 1.5rem); }
  .article-page.article-reading .article-body > h2 { font-size: 1.25em; }
  .article-page.article-reading .article-body > h3 { font-size: 1.05em; }
  .article-page.article-reading .article-body > blockquote { font-size: 1.1429em; }
}
/* Article page — editorial header (D8). The problem widget and the body's rendering are untouched; the measure and the
   desktop text size are the wider-page rules above. */
.article-header--editorial { position: relative; isolation: isolate; padding-top: .5rem; }
.article-header--editorial::before { content: ""; position: absolute; z-index: -1; left: calc(50% - 50vw); right: calc(50% - 50vw); top: -14.5rem; height: 38rem;   /* overshoots behind the header (see the masthead wash note); bottom edge unchanged at 23.5rem */
  background: linear-gradient(180deg, var(--wash) 0%, rgb(255 255 255 / 0) 100%); }
.article-page:not(.article-index) .article-header--editorial > .article-crumbs { display: flex; max-width: 60rem; margin: 0 auto 1rem; }
.article-header--editorial .article-crumbs { font: 700 .75rem/1.2 var(--a-sans); letter-spacing: .12em; text-transform: uppercase; }
.article-header--editorial h1 { font-size: clamp(2.125rem, 5.4vw, 3.25rem); line-height: 1.06; letter-spacing: -.03em; text-wrap: balance; margin-bottom: 1rem; }
.article-header--editorial .article-dek { color: var(--a-ink-2); font-size: clamp(1.1875rem, 2.4vw, 1.375rem); line-height: 1.5; }
.article-header--editorial .article-byline-top { border-bottom: 0; padding-bottom: 0; }
.article-page:not(.article-index) .article-header--editorial > .ed-photo { max-width: 100%; margin: 2.25rem .625rem 0 0; }
.article-header--editorial .ed-photo img.article-hero { margin: 0; }
.article-header--editorial .ed-photo img.article-hero { aspect-ratio: auto 1200 / 630; height: auto; }   /* reserve the frame before load; keep a non-1200x630 upload at its own ratio */
/* Read next: its "READ NEXT ———" heading (.ed-section-h) draws its own hairline, so the base .article-read-next top
   border would be a second rule about 35px above it. Dropped on the article page only (the base rule is not edited). */
.article-page.article-reading .article-read-next { border-top: 0; }
/* The draft preview (admin, ?preview=) renders its banner ABOVE the site header, and the header wash above overshoots
   up behind both. The wash sits in the positioned, isolated header, which paints over a non-positioned block, so the
   banner gets a stacking position: above the wash (z-index 1), below the sticky site header (z-index 50) as it scrolls
   away. articlePageHtml's draft preview is the only page that renders this banner. */
.article-preview-banner { position: relative; z-index: 1; }

/* ---- Article audience (label, pointer) */
/* article-audience phase 3, appended after the Editorial article header section (neither it nor the kit is edited).
   Every selector carries .article-reading or .article-header--editorial, so only the article page is styled. Both
   pieces sit in the article page's reading column: 60rem since redesign D23 (the plan's "48rem" was the D21 measure),
   set explicitly because the Editorial article header section narrows only the selectors it names.
   - The audience label (D8; none where the educators crumb already names it) sits between the crumbs and the h1, in the crumbs' selector shape and specificity. It is a
     static sentence-case pill (neutral fill, secondary ink) led by the kit's .ed-pill-dot in the header tint (--deep),
     so it reads as "who this is for" rather than a second gold crumb row. It is not a link: no hover, and it stays
     visually quieter than the tappable hub pills.
   - The one-line cross-persona pointer (D12): --top under a b2b page's header, --end after a b2c/both page's CTA (or
     body) and before the byline card. Its link is a 44px tap target: display inline-block, 1rem/1.5 text (24px) plus
     .625rem padding above and below. Long leads and links wrap (overflow-wrap: anywhere) so nothing overflows at 375px. */
.article-page:not(.article-index) .article-header--editorial > .article-audience-kicker { display: flex; max-width: 60rem; margin: 0 auto 1rem; }
.article-page:not(.article-index) .article-header--editorial > .article-audience-kicker .article-audience-badge { display: inline-flex; align-items: center; gap: .5rem; max-width: 100%; padding: .3rem .8rem; border-radius: 9999px; background: var(--a-neutral-100); color: var(--a-ink-2); font: 600 .875rem/1.4 var(--a-sans); overflow-wrap: anywhere; }
.article-page.article-reading .article-audience-pointer { max-width: 60rem; margin-inline: auto; color: var(--a-ink-2); font: 400 1rem/1.5 var(--a-sans); overflow-wrap: anywhere; }
.article-page.article-reading .article-audience-pointer--top { margin: 1.5rem auto 0; }
.article-page.article-reading .article-audience-pointer--end { margin: 2rem auto 0; border-top: 1px solid var(--a-border); padding-top: 1rem; }
.article-page.article-reading .article-audience-pointer a { display: inline-block; padding-block: .625rem; color: var(--a-brand); font-weight: 600; text-decoration: none; }
.article-page.article-reading .article-audience-pointer a:hover { text-decoration: underline; }
