/* ==========================================================================
   RankLab Academy — Design System
   Goals: fast (no framework), CLS-safe ad slots, accessible, responsive.
   ========================================================================== */

:root {
  --bg: #ffffff;
  --bg-alt: #f6f8fb;
  --bg-soft: #eef2f8;
  --surface: #ffffff;
  --border: #dfe5ee;
  --border-strong: #97a5b8;  /* darkened: 1.46:1 -> >=3:1 (WCAG 1.4.11 UI) */
  --text: #14212e;
  --text-soft: #4a5b6d;
  --text-mute: #5d6e82;     /* darkened: 4.02:1 -> >=4.5:1 on --bg-alt */
  --brand: #0b5fff;
  --brand-dark: #0741b8;
  --brand-soft: #dfe9ff;    /* deepened so --brand reaches 4.5:1 on it */
  --accent: #0a7d5c;        /* darkened: 3.06:1 -> >=4.5:1 on --accent-soft */
  --accent-soft: #e2f6ef;
  --warn: #8a4f08;          /* darkened: 3.84:1 -> >=4.5:1 on --warn-soft */
  --warn-soft: #fdf3e2;
  --danger: #c0392b;
  --danger-soft: #fdeceb;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow-sm: 0 1px 2px rgba(16, 32, 51, .06), 0 1px 3px rgba(16, 32, 51, .08);
  --shadow-md: 0 6px 24px rgba(16, 32, 51, .09);
  --maxw: 1180px;
  /* One source of truth for the sticky header height. Previously three
     different numbers (62px nav offset, 84px scroll-padding, 78px aside top)
     described one measured 63px header, so anchor jumps landed ~21px off. */
  --header-h: 63px;
  --sticky-gap: .9rem;
  /* System stack, deliberately. The homepage used to be the ONLY page that
     loaded Inter from Google Fonts, so it rendered in a different typeface
     from all 14 other pages. Rather than add a render-blocking webfont to
     every page, the site commits to the platform UI font: consistent
     everywhere, zero network cost, no FOUT. "Inter" is kept first so it is
     used when the visitor happens to have it installed locally. */
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}

[data-theme="dark"] {
  --bg: #0e1420;
  --bg-alt: #131b2a;
  --bg-soft: #182236;
  --surface: #141d2c;
  --border: #26334a;
  --border-strong: #35455f;
  --text: #eaf0f8;
  --text-soft: #b6c3d4;
  --text-mute: #8d9cb1;
  --brand: #6c9bff;
  --brand-dark: #a8c2ff;
  --brand-soft: #1b2942;
  --accent: #3ed3a3;
  --accent-soft: #14302a;
  --warn: #f0b357;
  --warn-soft: #2d2415;
  --danger: #ff8b7d;
  --danger-soft: #33201e;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-md: 0 8px 30px rgba(0, 0, 0, .45);
}

* { box-sizing: border-box; }

/* Tell the UA to render native controls, scrollbars and form popups in the
   active theme — without this, dark mode keeps light scrollbars and selects. */
:root { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--sticky-gap));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.7;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

img, svg, video { max-width: 100%; height: auto; }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 .6em; font-weight: 700; letter-spacing: -.015em; }
h1 { font-size: clamp(1.9rem, 4.4vw, 2.9rem); }
h2 { font-size: clamp(1.45rem, 3vw, 2rem); margin-top: 2.2em; }
h3 { font-size: clamp(1.15rem, 2.2vw, 1.35rem); margin-top: 1.8em; }
h4 { font-size: 1.05rem; margin-top: 1.4em; }
p, ul, ol, table, pre, blockquote { margin: 0 0 1.1em; }
ul, ol { padding-left: 1.3em; }
li { margin-bottom: .4em; }
strong { font-weight: 650; }
small { font-size: .85rem; }

code {
  font-family: var(--mono);
  font-size: .88em;
  background: var(--bg-soft);
  border: 1px solid var(--border);
  padding: .1em .35em;
  border-radius: 5px;
}

pre {
  font-family: var(--mono);
  font-size: .86rem;
  line-height: 1.6;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1rem 1.1rem;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}
pre code { background: none; border: 0; padding: 0; font-size: 1em; }

blockquote {
  border-left: 4px solid var(--brand);
  background: var(--brand-soft);
  padding: .9rem 1.1rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--text-soft);
}

hr { border: 0; border-top: 1px solid var(--border); margin: 2.5rem 0; }

/* ---------- Layout ---------- */
.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 1.1rem; }
.wrap-narrow { max-width: 860px; }
/* padding-block, NOT the `padding: 3rem 0` shorthand. Nearly every generated
   view emits `class="section wrap"` on ONE element; the shorthand set
   padding-left/right to 0 and, being declared after `.wrap`, won the cascade —
   so those views sat flush against the window edge on every screen size.
   Setting only the block axis leaves `.wrap`'s 1.1rem side gutter intact. */
.section { padding-block: 3rem; }
.section-alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.center { text-align: center; }
.muted { color: var(--text-mute); }
.lead { font-size: 1.12rem; color: var(--text-soft); }

/* A grid is a block in the prose flow (`::: grid` directives sit between
   paragraphs, blockquotes and callouts), so it needs the same trailing rhythm
   as `p`/`blockquote`. Without it a grid touched the next element exactly. */
.grid { display: grid; gap: 1.1rem; margin: 0 0 1.3rem; }
.grid:last-child { margin-bottom: 0; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(255px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); }

/* ---------- Skip link ----------
   Uses clip-path rather than left:-9999px: the legacy off-screen technique can
   trigger a horizontal scroll jump when focused. */
.skip-link {
  position: absolute; left: 0; top: 0; z-index: 999;
  background: var(--brand); color: #fff; padding: .7rem 1rem; border-radius: 0 0 8px 0;
  clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus { clip-path: none; }

/* ---------- Header ---------- */
.site-header {
  position: sticky; top: 0; z-index: 60;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(160%) blur(10px);
  border-bottom: 1px solid var(--border);
  /* Must stay visible: the mega-menu panel is absolutely positioned to this
     element. Any overflow:hidden here would clip the panel. */
  overflow: visible;
}
/* min-height is derived from the same token as scroll-padding and the sticky
   asides (header = this + 1px bottom border), so the three can never drift. */
.header-inner { display: flex; align-items: center; gap: 1rem; min-height: calc(var(--header-h) - 1px); }
.brand { display: flex; align-items: center; gap: .55rem; font-weight: 800; color: var(--text); font-size: 1.05rem; letter-spacing: -.02em; }
.brand:hover { text-decoration: none; }
.brand-mark {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px;
  background: linear-gradient(135deg, var(--brand), var(--accent));
  color: #fff; display: grid; place-items: center; font-size: .82rem; font-weight: 800;
}
.brand-mark span { color: #fff; }
.main-nav { margin-left: auto; display: flex; align-items: center; gap: .2rem; }
/* Direct children only. Scoping with `>` stops these styles leaking into the
   mega-menu panels, where they used to override .btn colours and produce
   unreadable low-contrast buttons. */
.main-nav > a {
  color: var(--text-soft); padding: .45rem .6rem; border-radius: 7px; font-size: .93rem; font-weight: 550;
}
.main-nav > a:hover, .main-nav > a[aria-current="page"] { background: var(--bg-soft); color: var(--text); text-decoration: none; }
.header-actions { display: flex; align-items: center; gap: .4rem; margin-left: .4rem; }
.icon-btn {
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  width: 36px; height: 36px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; font-size: 1rem;
}
.icon-btn:hover { border-color: var(--border-strong); color: var(--text); }
.nav-toggle { display: none; }

/* ---------- Global focus ring ----------------------------------------------
   Buttons, card links and icon buttons previously relied on the UA default
   outline, which is invisible against the brand fill on .btn and is removed
   entirely by some UA stylesheets on <a class="card-link">. A keyboard user
   could tab through a whole course page without ever seeing where they were.
   :focus-visible keeps mouse clicks ring-free. */
.btn:focus-visible,
.icon-btn:focus-visible,
.card-link:focus-visible,
.toc a:focus-visible,
summary:focus-visible,
.site-footer a:focus-visible,
.breadcrumbs a:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
  border-radius: 6px;
}
/* On the solid brand button the ring needs a light gap to stay visible. */
.btn:focus-visible { outline-color: var(--brand-dark); box-shadow: 0 0 0 2px var(--bg); }
.card-link:focus-visible { outline-offset: 3px; }

@media (max-width: 940px) {
  .nav-toggle { display: grid; }
  .main-nav {
    position: absolute; top: var(--header-h); left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--bg); border-bottom: 1px solid var(--border);
    padding: .5rem 1.1rem 1rem; box-shadow: var(--shadow-md);
    display: none;
  }
  .main-nav.open { display: flex; }
  .main-nav > a, .main-nav > .nav-item > .nav-link { padding: .7rem .4rem; border-bottom: 1px solid var(--border); border-radius: 0; }
  /* Long menus must scroll, not overflow off-screen. 100dvh (with a 100vh
     fallback) is required on mobile browsers: with 100vh the panel is sized to
     the viewport *without* the address bar, so its last item sits underneath
     the browser chrome and cannot be tapped. */
  .main-nav { max-height: calc(100vh - var(--header-h)); overflow-y: auto; overscroll-behavior: contain; }
}
@supports (height: 100dvh) {
  @media (max-width: 940px) {
    .main-nav { max-height: calc(100dvh - var(--header-h)); }
  }
}

/* ---------- Mega menu -----------------------------------------------------
   Each panel lives INSIDE its .nav-item, which is inside #main-nav. That one
   structural decision removes both classic mega-menu bug classes:

   1. POSITION BUGS — on desktop the panel is position:absolute and resolves
      against .site-header (position:sticky = a containing block), because
      .nav-item and .main-nav are both position:static. Placement is pure CSS
      (top:100%; left:0; right:0) so it stays correct on resize, zoom and RTL
      with no JS measurement. On mobile the panel is position:static inside the
      collapsible nav, so it behaves as a normal accordion and can never sit on
      top of the page body.
   2. HIDDEN-ELEMENT BUGS — "closed" is always the [hidden] attribute plus
      display:none. Never opacity:0, visibility:hidden, height:0 or off-screen
      positioning, so a closed panel cannot intercept clicks, trap focus or be
      announced by a screen reader. With no JS nothing is hidden at all.
--------------------------------------------------------------------------- */
.nav-item { position: static; display: flex; align-items: center; }

.main-nav .nav-link {
  display: inline-flex; align-items: center; gap: .3rem;
  color: var(--text-soft); padding: .45rem .6rem; border-radius: 7px;
  font-size: .93rem; font-weight: 550; font-family: inherit; line-height: 1.7;
  background: none; border: 0; cursor: pointer;
}
.main-nav .nav-link:hover,
.main-nav .nav-link[aria-expanded="true"],
.main-nav .nav-link[aria-current="page"] { background: var(--bg-soft); color: var(--text); text-decoration: none; }
.main-nav .nav-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.mega-btn .chev { font-size: .7em; transition: transform .15s ease; }
.mega-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }

/* Panel. `background-color` is a fully opaque token — never a color-mix or
   alpha value — so page content can never bleed through an open panel. */
.mega-panel {
  background-color: var(--bg);
  border-bottom: 1px solid var(--border);
}
.has-js .mega-panel {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 70;
  background-color: var(--bg);
  box-shadow: var(--shadow-md);
  animation: megaIn .14s ease-out;
}
/* The entrance animation NEVER touches opacity or visibility. If an
   environment pauses or disables CSS animations (headless screenshotting,
   assistive tooling, print, reduced-motion), a keyframe starting at opacity:0
   would freeze the open panel permanently invisible — a hidden-element bug.
   Animating only `transform` means the worst case is a 6px offset: the panel
   is always readable and clickable. */
@keyframes megaIn { from { transform: translateY(-6px); } to { transform: none; } }

/* Closed state. `[hidden]` wins over everything, so the menu always fails
   CLOSED rather than failing open on top of the page content. */
[hidden] { display: none !important; }
.has-js .mega-panel { display: none; }
.has-js .mega-panel.is-open { display: block; }

.mega-inner {
  width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 1.4rem 1.1rem 1.6rem;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.4rem;
}
@media (max-width: 1060px) { .mega-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.mega-col h3 {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-mute); margin: 0 0 .6rem; font-weight: 700;
}
.mega-col ul { list-style: none; margin: 0; padding: 0; }
.mega-col li { margin: 0 0 .15rem; }
/* Scoped with .main-nav so these win over the generic `.main-nav a` rule. */
.main-nav .mega-link {
  display: block; padding: .38rem .5rem; border-radius: 7px;
  color: var(--text); font-size: .9rem; line-height: 1.4; font-weight: 500;
  border: 0; border-bottom: 0;
}
.main-nav .mega-link:hover { background: var(--bg-alt); text-decoration: none; color: var(--text); }
.main-nav .mega-link:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.main-nav .mega-link small { display: block; color: var(--text-mute); font-size: .78rem; font-weight: 400; }
.main-nav .mega-link .mega-num { color: var(--brand); font-weight: 700; margin-right: .3rem; }
.mega-btn[data-section-active="true"] { color: var(--text); background: var(--bg-soft); }

/* ---------- Third menu level: lessons inside a course --------------------
   Level 1 is the column heading (a curriculum tier), level 2 the course link,
   level 3 this list. The twisty is a SEPARATE control from the course link so
   that expanding never navigates and navigating never expands — one target,
   one outcome.

   Closed is `hidden` + display:none, the same contract as the panel itself,
   so a collapsed lesson list can never intercept a click or trap focus. */
.mega-branch-head { display: flex; align-items: flex-start; gap: .15rem; }
.mega-branch-head .mega-link { flex: 1 1 auto; min-width: 0; }
.main-nav .mega-twisty {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; min-height: 30px; margin-top: .2rem;
  background: none; border: 0; border-radius: 6px; cursor: pointer;
  color: var(--text-mute); font-family: inherit; padding: 0;
}
.main-nav .mega-twisty:hover { background: var(--bg-alt); color: var(--text); }
.main-nav .mega-twisty:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.main-nav .mega-twisty .chev { font-size: .7em; transition: transform .15s ease; }
.main-nav .mega-twisty[aria-expanded="true"] .chev { transform: rotate(180deg); }

.mega-lessons {
  list-style: none; margin: .1rem 0 .45rem; padding: 0 0 0 .55rem;
  border-left: 1px solid var(--border);
}
.mega-lessons li { margin: 0; }
.main-nav .mega-sublink {
  display: flex; gap: .4rem; padding: .28rem .45rem; border-radius: 6px;
  color: var(--text-soft); font-size: .84rem; line-height: 1.35;
  font-weight: 450; border: 0; border-bottom: 0;
}
.main-nav .mega-sublink:hover { background: var(--bg-alt); color: var(--text); text-decoration: none; }
.main-nav .mega-sublink:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.main-nav .mega-sublink[aria-current="page"] {
  background: var(--brand-soft); color: var(--brand); font-weight: 650;
}
.main-nav .mega-sublink .mega-subnum {
  flex: 0 0 auto; color: var(--text-mute); font-variant-numeric: tabular-nums; font-weight: 600;
}
.mega-lessons-note {
  padding: .3rem .5rem; font-size: .82rem; color: var(--text-mute);
}
/* Buttons inside a panel must keep their own palette, never the nav palette. */
.main-nav .mega-panel .btn { color: #fff; }
.main-nav .mega-panel .btn:hover { color: #fff; text-decoration: none; }
.main-nav .mega-panel .btn-outline,
.main-nav .mega-panel .btn-ghost { color: var(--text); }
.mega-feature {
  grid-column: span 1; background: var(--bg-alt); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .9rem 1rem;
}
.mega-feature h3 { color: var(--text); font-size: .95rem; text-transform: none; letter-spacing: 0; }
.mega-feature p { font-size: .85rem; color: var(--text-soft); margin-bottom: .7rem; }
.mega-foot {
  border-top: 1px solid var(--border); background: var(--bg-alt);
}
.mega-foot-inner {
  max-width: var(--maxw); margin: 0 auto; padding: .7rem 1.1rem;
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
  font-size: .84rem; color: var(--text-mute);
}

/* Mobile: panels become inline accordions INSIDE the collapsible nav, so they
   scroll with it and never overlay the page body. */
@media (max-width: 940px) {
  .nav-item { display: block; }
  .main-nav .nav-link { width: 100%; justify-content: space-between; padding: .7rem .4rem; border-radius: 0; border-bottom: 1px solid var(--border); }
  .has-js .mega-panel {
    position: static; box-shadow: none; animation: none;
    border-bottom: 1px solid var(--border); background-color: var(--bg-alt);
  }
  /* Without JS on a small screen the panels would make the header enormous,
     so they collapse into the nav flow rather than stacking over content. */
  .mega-panel { background-color: var(--bg-alt); }
  .mega-inner { grid-template-columns: 1fr; gap: .9rem; padding: .8rem .4rem 1rem; }
  .mega-foot-inner { padding: .6rem .4rem; }
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  padding: .7rem 1.15rem; border-radius: 9px; font-weight: 600; font-size: .96rem;
  border: 1px solid transparent; cursor: pointer; background: var(--brand); color: #fff;
  transition: transform .06s ease, background .15s ease;
}
.btn:hover { background: var(--brand-dark); text-decoration: none; color: #fff; }
.btn:active { transform: translateY(1px); }
.btn-outline { background: transparent; color: var(--text); border-color: var(--border-strong); }
.btn-outline:hover { background: var(--bg-soft); color: var(--text); }
.btn-ghost { background: var(--bg-soft); color: var(--text); }
.btn-ghost:hover { background: var(--border); color: var(--text); }
.btn-sm { padding: .4rem .7rem; font-size: .85rem; border-radius: 7px; }
.btn-block { width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: .6rem; }

/* ---------- Cards ---------- */
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.15rem 1.2rem; box-shadow: var(--shadow-sm);
}
.card-link { display: block; color: inherit; }
.card-link:hover { text-decoration: none; border-color: var(--brand); box-shadow: var(--shadow-md); }
.card h3 { margin-top: 0; }
.card p:last-child { margin-bottom: 0; }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; }

.badge {
  display: inline-block; font-size: .72rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: .2rem .5rem; border-radius: 999px; background: var(--brand-soft); color: var(--brand-dark);
}
.badge-accent { background: var(--accent-soft); color: var(--accent); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-mute { background: var(--bg-soft); color: var(--text-mute); }

.pill-row { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; padding: 0; list-style: none; }
.pill-row li {
  margin: 0; font-size: .82rem; color: var(--text-soft);
  border: 1px solid var(--border); border-radius: 999px; padding: .18rem .65rem; background: var(--surface);
}

/* ---------- Hero ---------- */
.hero {
  background:
    radial-gradient(900px 420px at 12% -10%, var(--brand-soft), transparent 60%),
    radial-gradient(700px 380px at 92% 0%, var(--accent-soft), transparent 55%);
  border-bottom: 1px solid var(--border);
  padding: 3.2rem 0 2.6rem;
}
.hero h1 { margin-bottom: .5rem; }
.hero .lead { max-width: 62ch; }
.hero-grid { display: grid; grid-template-columns: 1.25fr .75fr; gap: 2rem; align-items: center; }
@media (max-width: 900px) { .hero-grid { grid-template-columns: 1fr; } }
.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .8rem; }
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .9rem 1rem; }
.stat b { display: block; font-size: 1.5rem; line-height: 1.2; }
.stat span { font-size: .82rem; color: var(--text-mute); }

/* ---------- Tables ---------- */
.table-wrap { overflow-x: auto; margin: 0 0 1.3rem; border: 1px solid var(--border); border-radius: var(--radius); }
table { width: 100%; border-collapse: collapse; font-size: .93rem; margin: 0; background: var(--surface); }
th, td { text-align: left; padding: .65rem .8rem; border-bottom: 1px solid var(--border); vertical-align: top; }
th { background: var(--bg-alt); font-weight: 650; font-size: .84rem; text-transform: uppercase; letter-spacing: .03em; color: var(--text-soft); }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--bg-alt); }

/* ---------- Callouts ---------- */
.callout {
  border: 1px solid var(--border); border-left: 4px solid var(--brand);
  background: var(--bg-alt); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: .95rem 1.1rem; margin: 0 0 1.3rem;
}
.callout > :last-child { margin-bottom: 0; }
.callout-title { display: block; font-weight: 700; margin-bottom: .3rem; }
.callout-tip { border-left-color: var(--accent); background: var(--accent-soft); }
.callout-warn { border-left-color: var(--warn); background: var(--warn-soft); }
.callout-danger { border-left-color: var(--danger); background: var(--danger-soft); }

/* ---------- Ad slots (reserved space = no layout shift) ---------- */
.ad-slot {
  display: block; margin: 2rem auto; text-align: center; position: relative;
  background: var(--bg-alt); border: 1px dashed var(--border-strong); border-radius: var(--radius);
  /* `contain: layout` only. `paint` additionally clips anything drawn outside
     the padding box, which silently truncates expandable AdSense creatives and
     any focus ring on an interactive ad. Layout containment alone is what
     actually delivers the CLS guarantee here. */
  overflow: hidden; contain: layout;
}
.ad-slot::before {
  content: "Advertisement";
  position: absolute; top: 4px; left: 8px;
  font-size: .62rem; letter-spacing: .09em; text-transform: uppercase; color: var(--text-mute);
}
.ad-slot .ad-inner {
  display: grid; place-items: center; width: 100%; height: 100%;
  color: var(--text-mute); font-size: .8rem;
  /* Extra top padding clears the absolutely-positioned "Advertisement" label,
     which used to overlap the first line of the slot's own content. */
  padding: 1.75rem .8rem 1.2rem;
}
.ad-slot .ad-inner code { font-size: .74rem; }
/* Reserved heights: mobile-first, matched to common AdSense responsive sizes */
.ad-leaderboard { min-height: 110px; max-width: 970px; }
@media (min-width: 768px) { .ad-leaderboard { min-height: 100px; } }
.ad-rectangle { min-height: 260px; max-width: 336px; }
.ad-inarticle { min-height: 280px; max-width: 728px; }
/* 600px, not 620px: 300x600 is the actual half-page unit AdSense serves, and
   the extra 20px was pure dead space at the bottom of every sidebar. */
.ad-sidebar { min-height: 600px; max-width: 300px; }
.ad-footer { min-height: 110px; max-width: 970px; margin-bottom: 0; }
.ad-slot ins { display: block; }

/* ---------- Article / course layout ---------- */
.page-head { padding: 2.2rem 0 1.2rem; border-bottom: 1px solid var(--border); background: var(--bg-alt); }
.breadcrumbs { font-size: .84rem; color: var(--text-mute); margin-bottom: .7rem; }
.breadcrumbs ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: .35rem; }
.breadcrumbs li { margin: 0; }
.breadcrumbs li + li::before { content: "/"; margin-right: .35rem; color: var(--border-strong); }

.doc-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr) 300px; gap: 2rem; padding: 2rem 0 3rem; align-items: start; }
/* Below 1180px the right rail used to be display:none — which destroyed the
   sidebar ad unit and the resume panel for every tablet visitor. It now
   reflows to the end of the main column instead, so no content is deleted at
   any width. */
@media (max-width: 1180px) {
  .doc-layout { grid-template-columns: 240px minmax(0, 1fr); }
  /* .doc-aside-right also carries .doc-aside, whose sticky rule appears LATER
     in this file. Media queries add no specificity, so a single-class selector
     here loses to it and the rail stays sticky with a clipped max-height. The
     selector is doubled up to win without !important and without reordering. */
  .doc-layout .doc-aside.doc-aside-right {
    grid-column: 2; position: static; top: auto;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 1.2rem; align-items: start; margin-top: .5rem;
  }
  .doc-aside-right > * { margin: 0; }
  .doc-aside-right .ad-sidebar { max-width: 300px; margin-inline: auto; }
}
@media (max-width: 900px) { .doc-layout { grid-template-columns: 1fr; gap: 1rem; } .doc-layout .doc-aside.doc-aside-right { grid-column: 1; } }

/* Sticky offset comes from --header-h so it tracks the real header height.

   NO INNER SCROLLBAR. This used to carry max-height + overflow-y:auto, which
   put a second scrolling region inside the page: the wheel stalled when the
   pointer crossed the rail, the scrollbar cut through the panel's rounded
   border, and on a trackpad it was easy to scroll the rail by accident while
   trying to scroll the page. A sticky element is allowed to be taller than the
   viewport — it simply stops sticking once you pass its end, which is the
   behaviour a reader actually expects from a table of contents.

   The rail therefore sizes to its content and the page is the only scroller. */
.doc-aside { position: sticky; top: calc(var(--header-h) + var(--sticky-gap)); }
@media (max-width: 900px) { .doc-aside { position: static; } }
.toc { border: 1px solid var(--border); border-radius: var(--radius); padding: .9rem 1rem; background: var(--surface); font-size: .9rem; }
.toc h2 { font-size: .78rem; text-transform: uppercase; letter-spacing: .07em; color: var(--text-mute); margin: 0 0 .6rem; }
.toc ol { list-style: none; margin: 0; padding: 0; counter-reset: toc; }
.toc li { margin: 0; }
.toc a { display: block; padding: .3rem .45rem; border-radius: 6px; color: var(--text-soft); border-left: 2px solid transparent; }
.toc a:hover { background: var(--bg-alt); text-decoration: none; }
.toc a.active { color: var(--brand); background: var(--brand-soft); border-left-color: var(--brand); font-weight: 600; }
.toc .done::after { content: " ✓"; color: var(--accent); font-weight: 700; }

.lesson {
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
  padding: 1.3rem 1.4rem; margin: 0 0 1.6rem;
}
.lesson > h2 { margin-top: 0; display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.lesson > :first-child { margin-top: 0; }
.lesson-num { font-size: .78rem; font-weight: 800; color: var(--brand); background: var(--brand-soft); border-radius: 6px; padding: .12rem .45rem; letter-spacing: .04em; }
/* Global heading margins (h3 = 1.8em, h4 = 1.4em) are tuned for long-form body
   copy. Inside a lesson card the sub-headings are dense scaffolding labels
   ("Tools needed", "Lab", "Artifact"), and the inherited margins opened gaps
   large enough to read as whole section breaks. */
.lesson h3 { font-size: 1.08rem; margin-top: 1.5em; }
.lesson h4 { margin-top: 1.15em; }
.lesson pre + h3, .lesson .table-wrap + h3, .lesson figure + h3 { margin-top: 1.2em; }
.lesson-foot {
  display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; justify-content: space-between;
  margin-top: 1.2rem; padding-top: .9rem; border-top: 1px dashed var(--border);
}
/* ---------- Lesson permalink + prev/next pager ---------------------------
   A lesson is its own route, so it needs two things the course page does not:
   a link out to its own URL, and a way to move to its neighbours.

   The pager is a two-column grid rather than flex(space-between) because the
   FIRST and LAST lessons only have one neighbour: with space-between, a lone
   "next" button jumps to the left edge where the "previous" button normally
   sits, which reads as the wrong control. Explicit columns keep "previous" on
   the left and "next" on the right no matter which one is missing. */
.lesson-permalink {
  font-size: .78rem; font-weight: 600; color: var(--text-mute);
  padding: .2rem .45rem; border-radius: 6px; white-space: nowrap;
}
.lesson-permalink:hover { background: var(--bg-alt); color: var(--brand); text-decoration: none; }

.lesson-pager {
  display: grid; grid-template-columns: 1fr 1fr; gap: .8rem;
  align-items: stretch; margin: 1.6rem 0 0;
}
.lesson-pager > * { min-width: 0; }
/* Column 1 is always "previous", column 2 always "next" — even when empty. */
.lesson-pager > :only-child:last-child { grid-column: 2; }
.lesson-pager .btn {
  justify-content: flex-start; text-align: left; gap: .45rem;
  /* Long lesson titles must wrap inside the button instead of forcing the
     grid wider than the column and overflowing the page on a phone. */
  white-space: normal; overflow-wrap: anywhere; line-height: 1.35;
}
.lesson-pager > :last-child:not(:only-child) .btn,
.lesson-pager > :last-child.btn:not(:only-child) { justify-content: flex-end; text-align: right; }
.lesson-pager > .btn:last-child:not(:first-child) { justify-content: flex-end; text-align: right; }
@media (max-width: 560px) {
  .lesson-pager { grid-template-columns: 1fr; }
  .lesson-pager > :only-child:last-child { grid-column: 1; }
  .lesson-pager > .btn:last-child:not(:first-child) { justify-content: flex-start; text-align: left; }
}

/* min-height 44px gives the whole label a comfortable pointer target even
   though the box itself is 24px (WCAG 2.5.8 minimum). */
.complete-toggle { display: inline-flex; align-items: center; gap: .55rem; font-size: .9rem; cursor: pointer; user-select: none; min-height: 44px; }
.complete-toggle input { width: 24px; height: 24px; flex: 0 0 24px; accent-color: var(--accent); cursor: pointer; }
.complete-toggle.is-done { color: var(--accent); font-weight: 600; }

/* ---------- Locked (paid) lessons ----------------------------------------
   A locked lesson stays IN the document and in the accessibility tree. The
   body text is visually faded with a mask, never display:none, so crawlers
   and assistive tech still reach it and the page cannot "lose" content.
   Every control inside is given the real `disabled` attribute by access.js,
   so there is nothing invisible-but-clickable and no focus trap. */
.lesson.is-locked { position: relative; }
/* access.js moves the whole lesson body (everything except the <h2>) into a
   single .lesson-preview wrapper, and appends .lesson-gate AFTER it. Clipping
   the wrapper as one block gives a predictable 190px preview; clipping each
   child individually (the old rule) let a long lesson stay fully readable. */
/* No pointer-events:none / user-select:none here. Every control inside is
   given the real `disabled` attribute by access.js, which is the accessible
   way to say "not available". pointer-events:none additionally made the
   preview text unselectable and un-copyable and suppressed the native tooltip
   on disabled controls, which reads as a broken page rather than a locked one. */
.lesson.is-locked > .lesson-preview {
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 60px, transparent 190px);
  mask-image: linear-gradient(to bottom, #000 0, #000 60px, transparent 190px);
  max-height: 190px; overflow: hidden;
}
.lesson.is-locked > .lesson-gate,
.lesson.is-locked > h2 { max-height: none; overflow: visible; pointer-events: auto; }

.lesson-gate {
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--bg-alt); padding: 1.1rem 1.2rem; margin-top: -.6rem;
  text-align: center;
}
.lesson-gate .gate-title {
  font-weight: 700; margin: 0 0 .3rem;
}
.lesson-gate .gate-title::before { content: "🔒 "; }
.lesson-gate .gate-copy { font-size: .92rem; color: var(--text-soft); margin: 0 0 .9rem; }
.lesson-gate .btn-row { justify-content: center; margin: 0; }

/* FAQ disclosure list — plain <details>, so it works with JS disabled and
   the answer text is always in the DOM for crawlers and assistive tech. */
.faq {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); padding: .75rem .95rem; margin-bottom: .6rem;
}
.faq > summary {
  cursor: pointer; font-weight: 600; font-size: .95rem; list-style: none;
  display: flex; justify-content: space-between; gap: 1rem; align-items: center;
}
.faq > summary::-webkit-details-marker { display: none; }
.faq > summary::after { content: "+"; color: var(--text-mute); font-weight: 700; font-size: 1.1rem; }
.faq[open] > summary::after { content: "−"; }
.faq > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }
.faq > p { font-size: .92rem; color: var(--text-soft); margin: .6rem 0 0; }

/* Price ladder */
.price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
.price-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.1rem 1.2rem; display: flex; flex-direction: column;
}
.price-card.is-free { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.price-card.is-featured { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand) inset; }
.price-amount { font-size: 2rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; }
.price-amount small { font-size: .8rem; font-weight: 500; color: var(--text-mute); display: block; }
.price-card ul { list-style: none; padding: 0; margin: .8rem 0; font-size: .9rem; }
.price-card li { padding-left: 1.3em; position: relative; }
.price-card li::before { content: "✓"; position: absolute; left: 0; color: var(--accent); font-weight: 700; }
.price-card .btn { margin-top: auto; }
.tier-badge { font-size: .7rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }

/* ---------- Engine-rendered chrome -----------------------------------------
   Two components exist only in the JSON-driven shell (js/engine/chrome.js).

   .is-planned marks a link whose destination does not exist yet. nav.json
   flags these with "status":"planned" and the engine renders a <span> instead
   of an <a>, so an unbuilt page can never produce a 404. It must still read as
   deliberately unavailable rather than as a broken link.

   .locale-select is only emitted when a SECOND locale is enabled in
   content/index.json, so a single-language site shows no useless dropdown. */
.is-planned {
  color: var(--text-mute);
  cursor: not-allowed;
  text-decoration: none;
  opacity: .72;
}
.is-planned::after { content: " ·"; opacity: 0; }        /* keeps inline metrics identical to a link */
a.is-planned, span.is-planned { pointer-events: none; }
.mega-link.is-planned { display: block; }
.btn.is-planned { background: var(--bg-alt); border-color: var(--border); color: var(--text-mute); }

.locale-select {
  min-height: 40px; padding: .3rem .5rem;
  font: inherit; font-size: .85rem;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.locale-select:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Notes widget */
.notes-box { margin-top: 1rem; }
.notes-box textarea {
  width: 100%; min-height: 96px; resize: vertical; font-family: var(--font); font-size: .93rem;
  padding: .7rem .8rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-alt); color: var(--text);
}
.notes-box textarea:focus { outline: 2px solid var(--brand); outline-offset: 1px; background: var(--surface); }
.notes-status { font-size: .78rem; color: var(--text-mute); }

/* Quiz */
.quiz { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-alt); padding: .9rem 1rem; }
.quiz h4 { margin-top: 0; }
.quiz details { border-bottom: 1px solid var(--border); padding: .45rem 0; }
.quiz details:last-of-type { border-bottom: 0; }
.quiz summary { cursor: pointer; font-weight: 600; }
.quiz summary::marker { color: var(--brand); }
.quiz .answer { margin: .5rem 0 .2rem; color: var(--text-soft); }
.self-score { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; margin-top: .8rem; font-size: .85rem; }

/* Progress */
.progress { height: 8px; border-radius: 999px; background: var(--bg-soft); overflow: hidden; border: 1px solid var(--border); }
.progress > i { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, var(--brand), var(--accent)); transition: width .3s ease; }
.progress-label { font-size: .82rem; color: var(--text-mute); display: flex; justify-content: space-between; margin-bottom: .3rem; }

/* ---------- Figure placeholders (image prompt / screenshot SOP) ------------
   Every course page ships placeholders instead of images: a generation prompt
   plus a reproducible screenshot procedure. The markup uses .ph-body/.ph-label
   (14 instances across 12 pages) but only the older .ph-box was ever styled,
   so those blocks rendered as unframed body copy, indistinguishable from the
   lesson text around them. Both class names are now styled, and .ph-box is
   kept so nothing regresses. */
.figure-ph { margin: 0 0 1.4rem; }

.figure-ph .ph-body,
.figure-ph .ph-box {
  width: 100%;
  border: 2px dashed var(--border-strong); border-radius: var(--radius);
  background:
    repeating-linear-gradient(45deg, var(--bg-alt), var(--bg-alt) 12px, var(--bg-soft) 12px, var(--bg-soft) 24px);
  color: var(--text-soft);
}
/* .ph-box is the empty "frame only" variant: it holds no copy, so it keeps a
   fixed 16:9 ratio and centres whatever it does contain. */
.figure-ph .ph-box {
  aspect-ratio: 16 / 9;
  display: grid; place-items: center; text-align: center; padding: 1rem; color: var(--text-mute);
}
.figure-ph .ph-box b { display: block; color: var(--text-soft); font-size: .95rem; }

/* .ph-body carries the prompt text, so it must grow with its content — an
   aspect-ratio here would clip long prompts. min-height keeps it reading as a
   reserved image well. */
.figure-ph .ph-body {
  position: relative;
  min-height: 132px;
  padding: 2.35rem 1.1rem 1.1rem;
  font-size: .9rem; line-height: 1.6;
}
.figure-ph .ph-body::before {
  content: "Image placeholder";
  position: absolute; top: 0; left: 0;
  font-size: .64rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--bg); background: var(--border-strong);
  padding: .2rem .6rem; border-radius: var(--radius) 0 var(--radius-sm) 0;
}
.figure-ph .ph-body > :last-child { margin-bottom: 0; }

.figure-ph .ph-label {
  display: block; margin: 0 0 .5rem;
  font-weight: 700; font-size: .95rem; line-height: 1.35; color: var(--text);
  letter-spacing: -.01em;
}
.figure-ph .ph-body em { font-style: italic; color: var(--text-soft); }

.figure-ph figcaption { font-size: .84rem; color: var(--text-mute); margin-top: .5rem; }
.figure-ph details { margin-top: .5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .5rem .7rem; background: var(--bg-alt); }
.figure-ph summary { cursor: pointer; font-size: .87rem; font-weight: 600; }
.figure-ph details[open] summary { margin-bottom: .4rem; }
@media (max-width: 560px) {
  .figure-ph .ph-body { padding: 2.35rem .8rem 1rem; font-size: .86rem; }
}

/* Prompt block */
.prompt-block { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin: 0 0 1.4rem; background: var(--surface); }
.prompt-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding: .6rem .9rem; background: var(--bg-alt); border-bottom: 1px solid var(--border); }
.prompt-head strong { font-size: .92rem; }
.prompt-block pre { margin: 0; border: 0; border-radius: 0; background: var(--surface); max-height: 460px; overflow: auto; }

/* Checklists */
.checklist { list-style: none; padding: 0; margin: 0 0 1.2rem; }
.checklist li { display: flex; gap: .65rem; align-items: flex-start; padding: .45rem .2rem; border-bottom: 1px solid var(--border); margin: 0; min-height: 44px; }
.checklist li:last-child { border-bottom: 0; }
/* 24x24 = WCAG 2.5.8 (Target Size, Minimum). The old 17px box failed on touch
   and was the smallest hit target on the entire site. */
.checklist input[type="checkbox"] { margin-top: .28rem; width: 24px; height: 24px; flex: 0 0 24px; accent-color: var(--accent); cursor: pointer; }
.checklist label { cursor: pointer; font-size: .95rem; padding: .15rem 0; }
.checklist li.done label { color: var(--text-mute); text-decoration: line-through; }

/* Forms / tools */
.field { margin-bottom: .9rem; }
.field label { display: block; font-size: .88rem; font-weight: 600; margin-bottom: .3rem; }
.field .hint { font-size: .78rem; color: var(--text-mute); font-weight: 400; }
input[type="text"], input[type="number"], input[type="email"], input[type="url"], input[type="date"], select, textarea {
  width: 100%; padding: .6rem .7rem; font-size: .95rem; font-family: var(--font);
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--brand); outline-offset: 1px; }
input[type="range"] { width: 100%; accent-color: var(--brand); }
.result-box { background: var(--brand-soft); border: 1px solid var(--brand); border-radius: var(--radius); padding: 1rem 1.1rem; }
.result-box .big { font-size: 2rem; font-weight: 800; line-height: 1.1; color: var(--brand-dark); }
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .7rem; }
.kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .7rem .8rem; }
.kpi b { display: block; font-size: 1.25rem; }
.kpi span { font-size: .78rem; color: var(--text-mute); }

/* Toast */
.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%) translateY(16px);
  background: var(--text); color: var(--bg); padding: .6rem 1rem; border-radius: 999px;
  font-size: .88rem; box-shadow: var(--shadow-md); opacity: 0; pointer-events: none;
  transition: opacity .2s ease, transform .2s ease; z-index: 200;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Footer */
.site-footer { background: var(--bg-alt); border-top: 1px solid var(--border); padding: 2.5rem 0 1.5rem; margin-top: 3rem; font-size: .92rem; }
.footer-grid { display: grid; grid-template-columns: 1.5fr repeat(3, 1fr); gap: 1.6rem; }
/* Four columns jammed together between 800px and 1000px, wrapping nearly every
   link label onto two lines. 2x2 first, then a single column. */
@media (max-width: 1000px) { .footer-grid { grid-template-columns: 1fr 1fr; gap: 1.4rem 1.6rem; } }
@media (max-width: 480px) { .footer-grid { grid-template-columns: 1fr; gap: 1.2rem; } }
.site-footer h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .07em; color: var(--text-mute); margin: 0 0 .6rem; }
.site-footer ul { list-style: none; padding: 0; margin: 0; }
.site-footer li { margin-bottom: .35rem; }
.site-footer a { color: var(--text-soft); }
.footer-bottom { margin-top: 1.8rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--text-mute); font-size: .84rem; display: flex; flex-wrap: wrap; gap: .8rem; justify-content: space-between; }

/* ==========================================================================
   Skeletons (loading placeholders)
   --------------------------------------------------------------------------
   These are not decoration. Because this site renders in the browser, the
   document has ZERO height until the content arrives — and a zero-height
   document cannot hold a scroll position. If a reader is 4000px into a lesson
   and reloads, the browser clamps them to the top, not because the offset was
   lost but because there is nothing to scroll. The skeleton reserves a
   document of roughly the right shape and height so the offset can be
   restored, and so the real content arriving shifts nothing (CLS = 0).

   Every height below is therefore matched to its real counterpart:
     .skel-lesson   ≈ .lesson          .skel-toc-row ≈ .toc a
     .skel-card     ≈ .card            .skel-ad      reuses .ad-* heights
   Change a real component's box and you must revisit its skeleton here.
   ========================================================================== */
:root {
  --skel-base: #e6ebf3;
  --skel-shine: #f4f7fb;
}
[data-theme="dark"] {
  --skel-base: #1b2434;
  --skel-shine: #26314599;
}

.skel {
  display: block;
  background: var(--skel-base);
  border-radius: 5px;
  /* The shimmer is a moving highlight over a fixed base colour. Painting it
     with a background-image (not an opacity animation on a child) keeps the
     whole effect on the compositor and off the main thread, which matters
     because this runs while we are also parsing Markdown. */
  background-image: linear-gradient(90deg, transparent 0%, var(--skel-shine) 50%, transparent 100%);
  background-size: 220px 100%;
  background-repeat: no-repeat;
  animation: skel-shine 1.25s ease-in-out infinite;
  /* Skeletons must never be selectable or focusable: they are furniture. */
  user-select: none; pointer-events: none;
}
@keyframes skel-shine {
  0%   { background-position: -220px 0; }
  100% { background-position: calc(100% + 220px) 0; }
}

/* Tier 1 shimmers immediately; deeper tiers start later so the eye is pulled
   to the top of the page, which is where content lands first. */
.skel-tier[data-skel-tier="2"] .skel { animation-delay: .12s; }
.skel-tier[data-skel-tier="3"] .skel { animation-delay: .24s; opacity: .78; }

/* ---- primitives: heights mirror the real type scale ---- */
.skel-line    { height: .78rem; margin: 0 0 .55rem; }
.skel-line-sm { height: .62rem; margin: 0 0 .45rem; }
.skel-line-lg { height: 1.5rem; margin: 0 0 .4rem; }
.skel-heading { height: 1.15rem; margin: 0 0 .9rem; border-radius: 6px; }
.skel-h1      { height: 2.1rem; margin: 0 0 .7rem; border-radius: 7px; }
.skel-lead    { height: .95rem; margin: 0 0 .5rem; }
.skel-pill    { display: inline-block; width: 92px; height: 1.55rem; border-radius: 999px; }
.skel-chip    { display: inline-block; height: 1.35rem; border-radius: 999px; }
.skel-box     { border-radius: var(--radius-sm); margin: 0 0 .9rem; }
.skel-btn     { display: inline-block; width: 172px; height: 2.85rem; border-radius: var(--radius-sm); }
.skel-btn-2   { width: 148px; }
.skel-btn-sm  { width: 118px; height: 2.2rem; }
.skel-bar     { height: 10px; border-radius: 999px; margin: 0 0 1.2rem; }
.skel-price   { height: 2.4rem; width: 46%; margin: .2rem 0 .9rem; }

.skel-row { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; margin-bottom: .8rem; }
.skel-row.between { justify-content: space-between; }
.skel-row .skel { margin-bottom: 0; }
.skel-btn-row { gap: .7rem; margin: 1.3rem 0 1rem; }
.skel-pills { margin-bottom: .9rem; }
.skel-crumbs { margin-bottom: .75rem; }
.skel-progress-wrap { margin-top: 1rem; }

/* ---- composed blocks ---- */
.skel-page-head { padding: 2.2rem 0 1.4rem; }
.skel-hero { padding: 3rem 0 2.4rem; }
.skel-stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .9rem; }
.skel-stat { padding: 1rem; }

.skel-grid .skel-card { min-height: 208px; }
.skel-card { padding: 1.15rem 1.2rem; }
.price-card.skel-card { min-height: 268px; }

.skel-table table { table-layout: fixed; }
.skel-table td, .skel-table th { padding: .7rem .8rem; }
.skel-table .skel { margin-bottom: 0; }

/* Course document. min-height per lesson is the single most important number
   on this page for scroll restoration: it is the average real rendered height
   of a lesson card, so N reserved cards ≈ the real article height. */
.skel-lesson { min-height: 560px; }
.skel-lesson-spine { min-height: 300px; }
.skel-figure { background-color: var(--skel-base); }
.skel-notes { border: 1px dashed var(--border); border-radius: var(--radius-sm); padding: .8rem; margin-top: 1.2rem; }
.skel-notes .skel:last-child { margin-bottom: 0; }
.skel-foot { border-top: 1px dashed var(--border); }
.skel-toc { padding: .9rem 1rem; }
.skel-toc-row { height: .72rem; margin: 0 0 .62rem; }
.skel-toc .skel:last-child { margin-bottom: 0; }
.skel-callout { border-left-color: var(--border-strong); }
.skel-callout .skel:last-child { margin-bottom: 0; }
@media (max-width: 900px) {
  /* Mobile lesson cards are taller (narrower measure = more wrapped lines),
     so the reserved height has to grow or a restored offset overshoots. */
  .skel-lesson { min-height: 680px; }
  .skel-hero { padding: 2rem 0 1.6rem; }
}

/* The static first-paint header stand-in from index.html. It must occupy
   EXACTLY --header-h so the real header replacing it shifts nothing. */
.skel-header { position: static; }
.skel-header .header-inner { justify-content: space-between; }
.skel-header-nav { display: flex; gap: .5rem; }
@media (max-width: 940px) { .skel-header-nav { display: none; } }

/* An ad skeleton must not draw the "Advertisement" label — the slot is not an
   ad yet, and labelling an empty box as advertising is misleading. */
.skel-ad::before { content: none; }
.skel-ad { border-style: dashed; opacity: .6; }

/* Fade the finished view in. The skeleton is removed in the same frame the
   content is inserted, so there is no flash of empty page between them. */
.view-enter { animation: view-fade .18s ease-out both; }
@keyframes view-fade { from { opacity: .4; } to { opacity: 1; } }

/* Reduced motion: keep the reserved space (it is structural and required for
   scroll restoration) but drop the moving highlight entirely. */
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; background-image: none; }
  .view-enter { animation: none; }
}
@media print { .skel-root, .skel-tier { display: none !important; } }

/* Utilities */
.mt-0 { margin-top: 0; } .mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: .6rem; } .mb-2 { margin-bottom: 1.2rem; }
.mb-3 { margin-bottom: 2rem; }
.mt-2 { margin-top: 1.2rem; } .mt-3 { margin-top: 2rem; }
.flex { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }
.between { justify-content: space-between; }
.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; }
.text-sm { font-size: .88rem; }

@media print {
  .site-header, .site-footer, .ad-slot, .doc-aside, .btn { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}
