/*
 * Design tokens for the marketing site.
 *
 * This is NOT src/skeleton.css. That file is a deliberate plain scaffold for the app and
 * is being replaced by its own pass; this one is a real design system for the
 * prerendered marketing pages, and the app will eventually adopt these tokens.
 *
 * Three ideas carry the whole look, and everything else follows from them:
 *
 *  1. Two surfaces, alternating in full-bleed bands: forest (near-black green) and
 *     paper. A band sets its own local token values, so the same component renders
 *     correctly on either one without knowing which it sits on.
 *  2. Hairlines are grid gaps, not borders. A band is a grid with `gap: 1px` over a
 *     line-coloured background, so the 1px gaps become the rules. No shadows anywhere,
 *     and no border radius except full pills on controls.
 *  3. Headings are weight 500, never bold, with hard negative tracking. The restraint is
 *     the point: a geometric sans set large, tight and calm.
 */

/* ---------------------------------------------------------------- type ---- */

@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/site/fonts/instrument-sans-latin.6219bc4bfd.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/site/fonts/instrument-sans-latin-ext.21fac8da55.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/*
 * Source Serif 4 is here for one job: French passage text. Reading is a typography
 * problem -- 998 long authentic passages -- and a serif at long measure is the honest
 * answer. The latin subset covers every French accent plus the oe ligature (U+0152-0153),
 * so latin-ext only downloads if a page actually uses a glyph from it.
 */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("/site/fonts/source-serif-4-latin.2a24bad466.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("/site/fonts/source-serif-4-latin-ext.c9299a7160.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/site/fonts/jetbrains-mono-latin.2c32b9b3ee.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/site/fonts/jetbrains-mono-latin-ext.9c38cb2d0d.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --font-sans: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-serif: "Source Serif 4", ui-serif, Georgia, serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /*
   * Fluid from phone to desktop. The upper bound of each clamp is the figure measured
   * off the reference site at 1440px, so the desktop end is a known-good scale rather
   * than a guess; the lower end is what stays readable at 390px.
   */
  --text-xs: 0.75rem;      /* 12 -- mono labels */
  --text-sm: 0.8125rem;    /* 13 -- fine print */
  --text-base: 0.9375rem;  /* 15 -- card body */
  --text-md: 1.0625rem;    /* 17 -- section intro */
  --text-lg: clamp(1.0625rem, 0.99rem + 0.31vw, 1.1875rem);  /* 17 -> 19 hero lead */
  --text-h3: 1.25rem;      /* 20 */
  --text-h2: clamp(1.75rem, 1.21rem + 2.25vw, 2.5rem);       /* 28 -> 40 */
  --text-h1: clamp(2.125rem, 1.14rem + 4.1vw, 3.4rem);       /* 34 -> 54.4 */

  /*
   * Phones get their own h1 curve, because the headline's first line has to fit the
   * viewport rather than a pleasant minimum. "Ace TEF Canada exam" needs about 11.2x the
   * font size in width, and a phone offers vw minus the 16px gutters, which works out at
   * roughly 8.7vw. Below that the headline wraps to three lines and the reserved height
   * leaves a gap. Measured at 320, 360, 390 and 430.
   */

  --leading-tight: 1.08;
  --leading-snug: 1.3;
  --leading-body: 1.6;

  --track-h1: -0.03em;
  --track-h2: -0.03em;
  --track-h3: -0.05em;
  --track-label: 0.12em;

  /* ------------------------------------------------------------ palette ---- */
  /*
   * Stored as bare HSL triples so a token can be used at partial alpha with
   * hsl(var(--forest) / 0.9). Values measured off the reference site.
   */
  --forest: 158.7 58.5% 10.4%;
  --forest-line: 157.7 47.9% 14.3%;
  --forest-text-muted: 95 9% 70%;
  --cream: 52 34.9% 91.6%;
  --lime: 73.5 86.3% 68.6%;
  --paper: 0 0% 100%;
  --paper-line: 49.1 18% 88%;
  --paper-muted: 51.4 25.9% 94.7%;
  --paper-text: 158 58% 10%;
  --paper-text-muted: 155 13% 35%;

  /*
   * The two marks in the feature matrix. Lime is far too light to read as a tick on
   * paper, so the affirmative on a paper band is its own darker green; on a forest band
   * the surface supplies lime instead.
   */
  --ok: 160 58% 28%;
  --no: 0 0% 62%;
  /*
   * The third mark, "some of this", on the free plan card. A real yellow is far too
   * light to read as a mark on paper, the same problem lime has, so this is the deepest
   * amber that still reads as yellow: it holds about 3.4:1 against white, which is the
   * floor for a graphic you have to be able to make out.
   */
  --part: 40 95% 38%;

  /* --------------------------------------------------------- dimensions ---- */
  --content: 69rem;    /* 1104px, the reference content column, measured */
  --measure: 46ch;     /* prose line length, roughly their 768px cap at 17px */
  --gutter: 16px;      /* side gutter; widens with the viewport, see below */
  --band-pad: clamp(3rem, 1.8rem + 5vw, 5.5rem);
  --rule: 1px;

  /*
   * The header's height, which the first band underlaps so the translucent bar always
   * has a dark surface behind it. **Keep this in step with the header's own padding and
   * type size**: it is 1.25rem of padding top and bottom around a 34px row. Too small
   * and the hero's first content hides under the bar; too large and there is a dead
   * strip above it.
   */
  --header-h: 74px;
}

/* A phone has no width to spare on margin; a desktop has nothing but. */
@media (max-width: 639px) { :root { --text-h1: clamp(1.4rem, 8.5vw - 2.8px, 2.125rem); } }
@media (min-width: 640px) { :root { --gutter: 24px; } }
@media (min-width: 1024px) { :root { --gutter: 32px; } }

/*
 * **There is no dark mode, and that is a decision.**
 *
 * Sahil, Oct 28 2026. The reference site has none either: it is white for everyone, and
 * a palette whose whole identity is one dark band alternating with white does not
 * survive being inverted -- the forest band has to become the LIGHTER surface for the
 * rhythm to read, which is a second design rather than a theme. `color-scheme: light`
 * in site.css keeps the browser's own chrome (scrollbars, form controls) light too, so
 * a visitor whose OS asks for dark gets a coherent page rather than a half-flipped one.
 */

/*
 * Marketing site primitives.
 *
 * Everything here is a layout or text primitive that composes. There are no page
 * specific rules in this file: if a page needs something, it needs a primitive.
 *
 * The one structural idea worth stating plainly, because it is unusual: hairlines are
 * grid gaps. A .band paints the line colour, lays out a 1px-gap grid over it, and its
 * children paint the surface colour back on top. What shows through the gaps is the
 * rule. That is why nothing here has a `border`, and why rules always meet exactly.
 */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /*
   * The header overlays the page, so an anchored section must clear it.
   *
   * This alone is not enough, and the symptom is a band of empty paper under the bar
   * after clicking a nav link. A band's first content sits `--band-pad` below its own
   * top edge, so landing the *edge* at the bar leaves the header's height plus that
   * padding, which is about 160px at 1440. The negative scroll-margin below pulls the
   * landing point up through the band's padding and leaves 1.5rem of air, measured from
   * the bar rather than from the top of the document.
   */
  scroll-padding-top: var(--header-h);
  /* No dark mode: see tokens.css. This keeps the browser's own chrome light to match. */
  color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: var(--leading-body);
  background: hsl(var(--paper));
  color: hsl(var(--paper-text));
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 500;          /* never bold: the restraint is the design */
  line-height: var(--leading-tight);
  color: hsl(var(--text));
}
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
/* A <dd> carries a 40px start margin in every browser's default sheet, which inside a
   flex row shows up as a mysterious indent the moment the row wraps. */
dl, dt, dd { margin: 0; }

a { color: inherit; }

:where(a, button, summary):focus-visible {
  outline: 2px solid hsl(var(--lime));
  outline-offset: 3px;
  border-radius: 2px;
}

/* ------------------------------------------------------------- surfaces ---- */
/*
 * A surface class sets the local meaning of bg / line / text. Every primitive below
 * reads those, so the same markup renders correctly on either band without variants.
 */
.surface-paper {
  --bg: var(--paper);
  --bg-sunken: var(--paper-muted);
  --line: var(--paper-line);
  --text: var(--paper-text);
  --text-muted: var(--paper-text-muted);
  --control-line: hsl(var(--paper-line));
}
.surface-forest {
  --bg: var(--forest);
  --bg-sunken: var(--forest);
  --line: var(--forest-line);
  --text: var(--cream);
  --text-muted: var(--forest-text-muted);
  /* On forest the accent IS the affirmative; --ok is the paper-band substitute for it. */
  --ok: var(--lime);
  --no: 95 9% 45%;
  /* The paper amber goes muddy on forest, so the third mark lightens with the surface. */
  --part: 44 92% 62%;
  /*
   * A control's outline is NOT the hairline colour on a dark surface.
   *
   * `--line` is tuned to be nearly invisible, because its job is a rule between two
   * forest panels; an outlined button using it reads as black on black. The reference
   * draws its "Log in" at rgb(68, 91, 80), which is this cream at 25% over the forest:
   * measured, not guessed. Hairlines and control outlines want different things on a
   * dark band and the same thing on a light one.
   */
  --control-line: hsl(var(--cream) / 0.25);
}
/*
 * Lime, for the one band that is a call to action: lime is this site's word for free
 * (the announce bar, the accent button), and it is the only surface that stands apart
 * from both the cream FAQ above it and the forest footer below.
 */
.surface-lime {
  --bg: var(--lime);
  --bg-sunken: var(--lime);
  --line: var(--lime);
  --text: var(--forest);
  --text-muted: 158.7 40% 22%;
  --control-line: hsl(var(--forest) / 0.35);
}

/* ---------------------------------------------------------------- band ---- */
/*
 * Full-bleed horizontal section. The two outer columns are empty rails whose only job
 * is to put a vertical hairline down each side of the content on wide screens; below
 * 640px they collapse and the content runs edge to edge, because a 390px screen has no
 * width to spare on decoration.
 */
.band {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--rule);
  padding-top: var(--rule);
  background: hsl(var(--line));
  color: hsl(var(--text));
}
.band > * { background: hsl(var(--bg)); }

@media (min-width: 640px) {
  .band {
    grid-template-columns:
      minmax(var(--gutter), 1fr)
      minmax(0, var(--content))
      minmax(var(--gutter), 1fr);
  }
}

.band__inner {
  padding-inline: var(--gutter);
  padding-block: var(--band-pad);
}
/*
 * An anchored band lands with its heading 1.5rem under the bar, not with its edge
 * there. `scroll-padding-top` on `html` has already accounted for the header, so this
 * only has to cancel the band's own top padding, which is why it is negative.
 */
.band[id] { scroll-margin-top: calc(1.5rem - var(--band-pad)); }
/* A band whose inner content is itself a hairline grid supplies its own padding. */
.band__inner--flush { padding: 0; }

/* ---------------------------------------------------------------- cells ---- */
/*
 * The workhorse. A hairline grid of equal cells: feature matrix, module cards, the CLB
 * table, the pricing tiers. One column on a phone, N on a desktop.
 */
.cells {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--rule);
  background: hsl(var(--line));
}
.cells--2 { }
.cells--3 { }
.cells--4 { }
@media (min-width: 640px) {
  .cells--2, .cells--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  .cells--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/*
 * Four across needs 1024, not 900: at 900 a quarter of the content column leaves about
 * 160px of text, which is narrower than a short label beside a short value, so the cells
 * reach four columns and immediately start wrapping. Two columns is better there.
 */
@media (min-width: 1024px) {
  .cells--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.cell {
  background: hsl(var(--bg));
  padding: clamp(1.25rem, 1rem + 1vw, 1.75rem);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.cell--sunken { background: hsl(var(--bg-sunken)); }

/* ----------------------------------------------------------------- type ---- */

.h1 {
  font-size: var(--text-h1);
  letter-spacing: var(--track-h1);
  line-height: 1.08;
}
.h2 {
  font-size: var(--text-h2);
  letter-spacing: var(--track-h2);
  line-height: 1.1;
}
.h3 {
  font-size: var(--text-h3);
  letter-spacing: var(--track-h3);
  line-height: 1.3;
}

.lede {
  font-size: var(--text-lg);
  line-height: 1.6;
  color: hsl(var(--text-muted));
  letter-spacing: -0.01em;
}
.body { font-size: var(--text-base); line-height: 1.6; color: hsl(var(--text-muted)); }
.fine { font-size: var(--text-sm); line-height: 1.55; color: hsl(var(--text-muted)); }

/* French passage text. The only place a serif appears, and the reason it is loaded. */
.passage {
  font-family: var(--font-serif);
  font-size: 1.1875rem;
  line-height: 1.7;
  color: hsl(var(--text));
}

.label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: hsl(var(--text-muted));
}

.measure { max-width: var(--measure); }
.center { margin-inline: auto; text-align: center; }

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap, 1rem);
  align-items: var(--stack-align, stretch);
}

/* -------------------------------------------------------------- controls ---- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.625rem 1.125rem;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 500;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  border: 0;
  /*
   * Square. There is no border radius anywhere in this system, controls included: the
   * hairline grid is built from straight rules meeting at right angles and a pill fights
   * it. Matches the reference, which does the same.
   */
  border-radius: 0;
  background: transparent;
  color: hsl(var(--text));
  box-shadow: inset 0 0 0 1px var(--control-line, hsl(var(--line)));
  transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.btn:hover { box-shadow: inset 0 0 0 1px hsl(var(--text) / 0.35); }

.btn--accent {
  background: hsl(var(--lime));
  color: hsl(var(--forest));
  box-shadow: none;
}
.btn--accent:hover { background: hsl(var(--lime) / 0.85); box-shadow: none; }

/* The call to action on a lime surface, where the lime button would disappear. */
.btn--forest {
  background: hsl(var(--forest));
  color: hsl(var(--cream));
  box-shadow: none;
}
.btn--forest:hover { background: hsl(var(--forest) / 0.88); box-shadow: none; }

.btn--lg { padding: 0.8125rem 1.5rem; font-size: var(--text-md); }

/* Text link with a real underline, offset so descenders survive. */
.link {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: hsl(var(--text) / 0.35);
}
.link:hover { text-decoration-color: hsl(var(--text)); }

/* Small accent chip. A label, not a control. */
.chip {
  display: inline-block;
  padding: 0.1875rem 0.5rem;
  background: hsl(var(--lime));
  color: hsl(var(--forest));
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0;
}

/*
 * The quiet chip: forest text on nothing, with a hairline. It lets a card carry a flag
 * without carrying the accent, which in a row of plans belongs to the one being sold.
 * It must stay after .chip: same specificity, so source order is what makes it apply.
 */
.chip--outline {
  background: transparent;
  color: hsl(var(--text));
  box-shadow: inset 0 0 0 1px var(--control-line, hsl(var(--line)));
}

/* ----------------------------------------------------------------- tile ---- */
/*
 * A glyph on a small square. Sized in `em` so a tile set in a headline scales with the
 * headline and one set in body text scales with the body: it is a piece of punctuation,
 * not a fixed-size image.
 */
.tile {
  display: inline-grid;
  place-items: center;
  width: var(--tile-size, 1.05em);
  height: var(--tile-size, 1.05em);
  flex: none;
  background: hsl(var(--cream));
  color: var(--tile-ink, hsl(var(--forest)));
  transform: rotate(var(--tile-rot, 0deg));
  vertical-align: -0.14em;
}
.tile--lime { background: hsl(var(--lime)); }
.tile svg { display: block; width: 68%; height: 68%; }

/* --------------------------------------------------------------- matrix ---- */
/*
 * The named comparison against a competitor.
 *
 * A real <table>, because it is one: three columns of marks only make sense to a screen
 * reader with row and column headers, and every mark carries a visually hidden "Yes" or
 * "No" so a cell never reads as empty.
 *
 * The hairlines come from `border-spacing: 1px` over a line-coloured table rather than
 * from borders, which is the same mechanism as `.cells` and keeps the rules meeting
 * exactly. Nothing in this file has a `border`.
 */
.matrix {
  width: 100%;
  border-collapse: separate;
  border-spacing: var(--rule);
  background: hsl(var(--line));
  font-size: var(--text-base);
}
.matrix th,
.matrix td {
  background: hsl(var(--bg));
  padding: 0.75rem 0.875rem;
  font-weight: 400;
  text-align: start;
  vertical-align: middle;
}
.matrix tbody th { color: hsl(var(--text)); }

/*
 * The two mark columns are narrow and fixed, so the feature labels get everything that
 * is left. The lower bound is what "tefcanada.info" needs in the header at 12px: a
 * competitor column that cannot hold the competitor's name is not a comparison.
 */
.matrix td,
.matrix thead th:not(:first-child) {
  width: clamp(6rem, 22vw, 9rem);
  text-align: center;
}
.matrix thead th {
  padding-block: 0.625rem;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0;
  color: hsl(var(--text-muted));
  /* Only as a last resort: the <wbr> after the dot is where it is meant to break. */
  overflow-wrap: break-word;
}
.matrix thead th.matrix__ours { color: hsl(var(--text)); font-weight: 600; }

/* Our column is lifted off the surface, so the eye has a side to follow down. */
.matrix__ours { background: hsl(var(--bg-sunken)); }

/* A qualifier on OUR side only; never write one about theirs the config cannot support. */
.matrix__note {
  display: block;
  font-size: var(--text-sm);
  color: hsl(var(--text-muted));
}

.matrix__mark { display: inline-grid; place-items: center; width: 1.25rem; height: 1.25rem; }
.matrix__mark svg { display: block; width: 100%; height: 100%; }
.matrix__mark--yes { color: hsl(var(--ok)); }
.matrix__mark--no { color: hsl(var(--no)); }

/* ----------------------------------------------------------------- hint ---- */
/*
 * A term that explains itself on hover or on focus.
 *
 * The trigger is a real button, because a touch screen has no hover and a tap focuses a
 * button. The dotted underline plus the small circled i is the affordance: one of them
 * alone is too quiet to be noticed in a table of plain labels.
 */
.hint { position: relative; display: inline-block; }

.hint__term {
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: help;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: hsl(var(--text) / 0.4);
}
.hint__term svg {
  display: inline-block;
  width: 0.9em;
  height: 0.9em;
  margin-inline-start: 0.3em;
  vertical-align: -0.1em;
  color: hsl(var(--text-muted));
}
@media (hover: hover) {
  .hint__term:hover { text-decoration-color: hsl(var(--text)); }
  .hint__term:hover svg { color: hsl(var(--text)); }
}

/*
 * Anchored to the START of the term, not centred on it: the term sits near the left edge
 * of a wide column, so a centred bubble would hang off the page on a phone. Capped
 * against the viewport for the same reason.
 */
.hint__bubble {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  inset-inline-start: 0;
  z-index: 10;
  width: max-content;
  max-width: min(22rem, 70vw);
  padding: 0.5rem 0.75rem;
  background: hsl(var(--forest));
  color: hsl(var(--cream));
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1.45;
  text-align: start;
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms ease;
}
/*
 * Hover only where hovering is real: iOS treats a tap as a hover that sticks until
 * something else clickable is tapped. Taps go through `data-open`, set by the home page's
 * script, which closes it on a tap elsewhere or after three seconds.
 */
@media (hover: hover) {
  .hint__term:hover + .hint__bubble { opacity: 1; visibility: visible; }
}
.hint__term:focus-visible + .hint__bubble,
.hint[data-open] .hint__bubble {
  opacity: 1;
  visibility: visible;
}
@media (prefers-reduced-motion: reduce) {
  .hint__bubble { transition: none; }
}
/*
 * Inside a plan card the bubble has to fit the card: the term sits at its left and the
 * featured card is the last column, so a 22rem bubble ran past the page at 1024 and
 * made it scroll sideways even while hidden.
 */
.plan .hint__bubble { max-width: 13rem; }
/* On a forest surface a forest bubble disappears, so it inverts to cream. */
.surface-forest .hint__bubble {
  background: hsl(var(--cream));
  color: hsl(var(--forest));
}

/*
 * The closing row: the price comparison, which is the whole argument in one line.
 *
 * Our cell goes lime. It is the only accent in the table, so the eye lands on it last
 * and lands hard, and it is the same lime as the announce bar and the call to action:
 * on this site lime means "this is the thing to do".
 */
.matrix__row--price th,
.matrix__row--price td { padding-block: 1.125rem; }
.matrix__row--price th { font-weight: 500; color: hsl(var(--text)); }
.matrix__row--price .matrix__ours {
  background: hsl(var(--lime));
  color: hsl(var(--forest));
}
.matrix__row--price .matrix__ours .price { color: hsl(var(--forest)); }

@media (min-width: 640px) {
  .matrix { font-size: var(--text-md); }
  .matrix th, .matrix td { padding: 0.875rem 1.25rem; }
  .matrix thead th { font-size: var(--text-sm); }
}

/*
 * A figure set large enough to be read as a figure.
 *
 * Deliberately NOT the mono face, which is what the small inline numbers use: mono gives
 * the decimal point a full character cell, so at this size "$49.99" reads as "$49 . 99".
 * Pulling the tracking in fixes that at 13px and does not at 24px.
 */
.price {
  /*
   * Fluid, because this figure sets the width of a deliberately narrow table column.
   * At a fixed 24px "$129.98" is wider than the 6rem column at 320px and quietly pushes
   * the whole page into a horizontal scroll.
   */
  font-size: clamp(1.125rem, 1rem + 0.6vw, 1.5rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.2;
  color: hsl(var(--text));
}

.price--muted { color: hsl(var(--text-muted)); font-weight: 400; }

/*
 * Pushes what follows it to the bottom of its cell, so a run of cards with headings of
 * different lengths still lines its figures up on one baseline.
 */
.cell__foot { margin-top: auto; display: flex; flex-direction: column; gap: 0.25rem; }

/* ----------------------------------------------------------------- plan ---- */
/*
 * The pricing cards, and the one place on this site that uses real cards.
 *
 * Everywhere else a surface is a region of a hairline lattice: the rules are grid gaps
 * and nothing carries a border. Sahil asked for real cards here on Oct 28 2026 and said
 * no compromise, and he is right that this page is different: a plan is a thing you
 * choose, so it has to read as a discrete object rather than as a region of a table.
 * Hence a real gap between them and an outline on each. Square corners stay -- the no
 * radius rule was its own decision and cards do not argue with it.
 */
.plans {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.25rem;
}
@media (min-width: 1024px) {
  .plans { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
}

.plan {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: clamp(1.5rem, 1.1rem + 1.4vw, 2rem);
  /*
   * The card sits on the sunken surface, not the band's own white. An outline alone on
   * white is a line around nothing: the card has to be a different surface from what it
   * sits on before it reads as an object you could pick up, which is the whole point of
   * breaking the lattice here.
   */
  background: hsl(var(--bg-sunken));
  border: 1px solid hsl(var(--paper-line));
}
/*
 * The featured card carries `surface-forest`, so it is not styled differently so much as
 * rendered on the other surface: its text, its outline, its tick colour and its chip all
 * follow the same tokens every other forest panel uses.
 */
.plan--featured { border-color: hsl(var(--forest)); }

/*
 * The free card is marked. Sahil asked for this on Oct 28 2026, when it sat beside a
 * cheap tier that was the same white card; that tier went on Oct 29 2026, and the
 * mark stayed as the site's sign for free beside the forest card being sold.
 *
 * A lime rule along the top edge rather than a filled panel or a second lime chip: lime
 * is already this site's word for free (the announce bar), and keeping it to a rule
 * leaves the forest card the loudest thing in the row, which is the one being sold. It
 * is an inset ring, not a border, because widening the real border by 2px would push
 * this card's flag row out of line with its neighbours' and a row of cards is read for
 * exactly that alignment.
 */
.plan--free { box-shadow: inset 0 3px 0 hsl(var(--lime)); }

/*
 * The flag row is reserved on every card, not only the one that carries a chip.
 * Otherwise the featured card is one chip taller than its neighbours and its price and
 * its button sit a few pixels below theirs, which is exactly the misalignment a row of
 * cards is read for.
 */
.plan__flag { display: flex; min-height: 1.5rem; }
/* Stacked, there is nothing to line up with, so an empty flag row is only a gap. */
@media (max-width: 1023px) { .plan__flag:empty { display: none; } }
.plan__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 0.75rem; }
.plan__who { font-size: var(--text-base); line-height: 1.5; color: hsl(var(--text-muted)); }

.plan__price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
  margin-top: 0.5rem;
}
/*
 * Deliberately not the mono face, for the reason `.price` is not: mono gives the decimal
 * point a full character cell, so "$24.99" reads as "$24 . 99" at this size.
 */
.plan__amount {
  font-size: clamp(2rem, 1.6rem + 1.4vw, 2.75rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.05;
  color: hsl(var(--text));
}
.plan__per { font-size: var(--text-base); color: hsl(var(--text-muted)); }

/*
 * No reserved height under the name, as of Oct 29 2026.
 *
 * A row of cards is read across, so anything above the price that varies in height puts
 * one card's price and button on a different baseline from its neighbour's. With three
 * cards the sentences under the names ran to two or three lines and needed a measured
 * `min-height`. The two sentences left ("The full experience on a slice of the library"
 * and "...on the whole library") are one line at every width where the cards sit side
 * by side, measured, so the reserve went with the third card. **Re-measure if either
 * sentence changes**, and put a reserve back if one of them wraps.
 */

.plan__cta { width: 100%; margin-block: 0.25rem 0.5rem; }

/*
 * The strip under the row of cards: what every plan carries, said once.
 *
 * It takes the cards' own surface and border rather than sitting as bare text on the
 * band, for the reason the cards have them at all: this band paints the sunken cream,
 * and anything on it that is not given a surface reads as floating. It is deliberately
 * not a fourth card, so it is one line, full width, and the chip does the work of
 * saying what it is.
 */
.plans-extras {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.875rem;
  margin-top: 1.25rem;
  padding: 0.875rem 1.25rem;
  background: hsl(var(--bg-sunken));
  border: 1px solid hsl(var(--paper-line));
}
.plans-extras__text { font-size: var(--text-sm); color: hsl(var(--text-muted)); }

.plan__list { display: flex; flex-direction: column; gap: 0.5rem; }
/* The second list is what the plan does, under what it contains. */
.plan__row {
  display: grid;
  grid-template-columns: 1.125rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-base);
  /* The mark's box is one line tall, so the number is shared rather than repeated. */
  --row-lh: 1.45;
  line-height: var(--row-lh);
}
.plan__label { color: hsl(var(--text)); }
.plan__value {
  /* The figure stays on one line; the label beside it is a single word and can shrink.
     Left to the grid, "71 of 931 questions" breaks after "931" in a narrow card. */
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: -0.045em;
  color: hsl(var(--text-muted));
  text-align: end;
}
/* A row the plan does not include is stated, not shouted: the cross carries it. */
.plan__row:has(.plan__mark--no) .plan__label { color: hsl(var(--text-muted)); }

/*
 * The mark is a box one line tall, aligned to the top of the row, with the glyph
 * centred in it. Baseline alignment, which is what the rest of the row wants, puts the
 * bottom edge of the mark on the text baseline: measured at 15px that sat the glyph
 * 3.5px above the middle of the words beside it, which is visible in a column of four.
 * A wrapping label still gets its mark on the first line, because the box is one line
 * tall and starts at the top.
 */
.plan__mark {
  display: inline-grid;
  place-items: center;
  width: 1.125rem;
  height: calc(var(--row-lh) * 1em);
  align-self: start;
}
.plan__mark svg { display: block; width: 100%; height: 1.125rem; }
.plan__mark--yes { color: hsl(var(--ok)); }
.plan__mark--no { color: hsl(var(--no)); }
/*
 * The free card's modules: a yellow ring rather than a tick, because the figure beside
 * it reads "71/931". A tick there would claim the whole module whatever colour it is,
 * which is why this is a different shape and not only a different ink. The label keeps
 * its own colour, since the module is present; only a cross mutes a label.
 */
.plan__mark--part { color: hsl(var(--part)); }

.plan__note { margin-top: auto; padding-top: 0.5rem; }

/* ----------------------------------------------------------------- sale ---- */
/*
 * A time-limited sale (config/sale.json). The page is prerendered with both prices, and
 * a line at the top of <body> sets `data-sale` on <html> while the sale is on, so these
 * two rules choose. Without the attribute (scripts off, sale over) the regular prices show.
 */
html:not([data-sale]) .if-sale,
html[data-sale] .if-regular { display: none !important; }

/*
 * Pumpkin, the sale's colour and nothing else's. Lime is this site's word for free, so a
 * discount needs its own; the bright one carries forest text, the deep one is for text on
 * paper.
 */
:root {
  --pumpkin: 26 96% 58%;
  --pumpkin-deep: 22 82% 40%;
}

.announce.announce--sale { background: hsl(var(--pumpkin)); text-align: center; }
.announce.announce--sale:hover { background: hsl(var(--pumpkin) / 0.9); }
.announce__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
/* The pumpkin's stem takes the top of its box, so its body sits low beside capitals:
   nudged up to centre the body on the caps. Same in the plans pill. */
.announce__tag svg { width: 1.15em; height: 1.15em; transform: translateY(-0.1em); }
.announce__clock { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

/* The flag's two versions are flex boxes like the row itself, or the inline wrapper adds a line box and the card sits 3px low. */
.plan__flag > :is(.if-sale, .if-regular) { display: flex; flex-wrap: wrap; gap: 0.375rem; }

/* The price after the rise, struck through on a small line above today's price. */
.plan__was-row { margin-top: 0.5rem; margin-bottom: -0.5rem; font-size: var(--text-md); line-height: 1.4; }
.plan__was { color: hsl(var(--text-muted)); text-decoration-thickness: 1.5px; }
/* The free card's empty line keeps a row of buttons level; stacked, it is only a gap. */
@media (max-width: 1023px) { .plan__was-row:has(.plan__was-spacer) { display: none; } }

/* Balanced, or a phone leaves "it." alone on a line. */
.plans-band .h2 { text-wrap: balance; }

/*
 * The sale's pill (site/lib/plans.mjs, `salePill`), in the place of the band's label:
 * soft pumpkin, a hairline edge, square like everything here. It wraps to two lines on
 * a phone, and the rule between its halves goes with the wrap.
 */
.sale {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  align-self: flex-start;
  gap: 0.25rem 0.75rem;
  margin-bottom: 0.25rem;
  padding: 0.4rem 0.8rem;
  background: hsl(var(--pumpkin) / 0.14);
  border: 1px solid hsl(var(--pumpkin-deep) / 0.35);
  color: hsl(var(--forest));
  font-size: var(--text-sm);
  line-height: 1.35;
}
.sale__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
  color: hsl(var(--pumpkin-deep));
}
.sale__tag svg { width: 1.15em; height: 1.15em; transform: translateY(-0.1em); }
.sale__clock { white-space: nowrap; }
@media (min-width: 560px) {
  .sale__clock { padding-left: 0.75rem; border-left: 1px solid hsl(var(--pumpkin-deep) / 0.35); }
}
/* Flex, so a hidden days cell leaves no gap behind. */
.sale__cells { display: inline-flex; gap: 0.25em; font-weight: 600; font-variant-numeric: tabular-nums; }
.sale__cell[hidden] { display: none; }

/* --------------------------------------------------------------- review ---- */
/*
 * Testimonials are real cards, the second place on the site after the plans (Sahil,
 * Oct 29 2026): a review is one person's voice, so it reads as an object rather than
 * as a region of the lattice. Square corners, as everywhere.
 *
 * On a phone the row scrolls sideways with snap points, the next card peeking in at the
 * edge so it reads as swipeable. It bleeds to the screen edge by cancelling the band's
 * gutter and putting it back as padding, so the first card still lines up with the
 * heading. From 900px it is a plain grid with one column per review, up to three.
 */
.reviews {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(85%, 22rem);
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  margin-inline: calc(-1 * var(--gutter));
  padding-inline: var(--gutter);
  scroll-padding-inline: var(--gutter);
  scrollbar-width: none;
}
.reviews::-webkit-scrollbar { display: none; }
.reviews > * { scroll-snap-align: start; display: flex; }
@media (min-width: 900px) {
  .reviews {
    grid-auto-flow: row;
    grid-template-columns: repeat(min(var(--reviews), 3), minmax(0, 1fr));
    overflow: visible;
    margin-inline: 0;
    padding-inline: 0;
  }
}

.review {
  margin: 0;  /* a <figure> carries a 40px side margin in every default sheet */
  /*
   * Load bearing: the star row's "Rated 5 out of 5" is `.visually-hidden`, which is
   * absolutely positioned. With no positioned ancestor its containing block is the page,
   * so the scroller does not clip it, and on the second and third cards it sat 600px
   * right of the screen: a phone zoomed the whole page out to 656px to fit it.
   */
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding: clamp(1.5rem, 1.1rem + 1.4vw, 2rem);
  background: hsl(var(--cream) / 0.05);
  border: 1px solid hsl(var(--cream) / 0.14);
}
/*
 * The yellow is the forest band's amber (--part), the deepest tone that still reads as
 * yellow on forest and the same ink as the free card's "some of this" ring.
 */
.review__stars { display: flex; gap: 0.2rem; color: hsl(var(--part)); }
.review__stars svg { display: block; width: 1.125rem; height: 1.125rem; }
.review__star--off { color: hsl(var(--cream) / 0.2); }
.review__text {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.6;
  color: hsl(var(--text));
}
/* The attribution sits at the foot, so a row of quotes of different lengths ends level. */
.review__by {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: auto;
  padding-top: 1.25rem;
  border-top: 1px solid hsl(var(--cream) / 0.12);
}
.review__avatar {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  background: hsl(var(--lime));
  color: hsl(var(--forest));
  font-weight: 600;
  font-size: var(--text-md);
}
.review__who { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.review__name { font-weight: 500; color: hsl(var(--text)); }
.review__role { font-size: var(--text-sm); color: hsl(var(--text-muted)); }

/* ------------------------------------------------------------------ faq ---- */
/*
 * Heading beside the questions from 1024, above them below it. The questions are a
 * hairline list by the same mechanism as everything else here: a 1px-gap grid over the
 * line colour, with a rule at the top and the foot from the padding.
 */
.faq-band { display: grid; gap: 2rem; }
@media (min-width: 1024px) {
  .faq-band { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 4rem; align-items: start; }
}
.faq {
  display: grid;
  gap: var(--rule);
  padding-block: var(--rule);
  background: hsl(var(--line));
}
.faq__item { background: hsl(var(--bg)); }
.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.125rem;
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.35;
  color: hsl(var(--text));
  cursor: pointer;
  list-style: none;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q svg {
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  color: hsl(var(--text-muted));
  transition: transform 200ms ease;
}
.faq__item[open] .faq__q svg { transform: rotate(45deg); }
.faq__q:hover svg { color: hsl(var(--text)); }
@media (prefers-reduced-motion: reduce) { .faq__q svg { transition: none; } }
.faq__a { max-width: 58ch; padding-bottom: 1.25rem; padding-inline-end: 2rem; }

/* ---------------------------------------------------------------- header ---- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  /*
   * **Translucent, over a surface that is always dark.**
   *
   * The bar is `forest / 0.92` and blurred, which Sahil wants and which only works if
   * something dark sits behind it. A sticky header in normal flow has the *white body
   * background* behind it at the top of the document, so 8% of white bled through and
   * the bar sat at rgb(30, 59, 49): visibly milky, and it "fixed itself" on scroll once
   * the hero slid under.
   *
   * The fix is the reference's, which pulls its hero up under a fixed header with a
   * spacer reserving the height. Ours stays sticky and the first band underlaps it by
   * `--header-h` instead, which needs no second positioning scheme. See `main > *` below.
   */
  background: hsl(var(--forest) / 0.92);
  backdrop-filter: blur(8px);
  /* A hairline, not a border. Cream, so it holds its edge over a white band too. */
  box-shadow: inset 0 -1px 0 hsl(var(--cream) / 0.1);
  color: hsl(var(--cream));
  --line: var(--forest-line);
  --text: var(--cream);
  --text-muted: var(--forest-text-muted);
  --control-line: hsl(var(--cream) / 0.25);
}
.site-header__inner {
  max-width: var(--content);
  margin-inline: auto;
  padding: 1.25rem var(--gutter);
  display: flex;
  align-items: center;
  gap: 1.5rem;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: -0.025em;
  text-decoration: none;
  color: hsl(var(--cream));
}
.wordmark__mark {
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  flex: none;
  background: hsl(var(--lime));
  color: hsl(var(--forest));
}
.wordmark__mark svg { display: block; width: 74%; height: 74%; }

/*
 * Centred between the wordmark and the actions, as on the reference.
 *
 * **Each link is a padded box, not bare text**, which is what made our header feel narrow
 * beside theirs: same 14px type, but 6px by 14px of padding and a 4px gap spreads the nav
 * across 523px where bare text on a 24px gap used 379. It also gives the links a real hit
 * area, which matters more on a touch screen than it does to the eye.
 */
/*
 * The nav sits at the right, against the buttons, rather than centred in the bar
 * (Sahil, Oct 28 2026). `margin-inline-start: auto` is what moves it: the actions then
 * follow it immediately, on the flex row's own 1.5rem gap.
 */
.site-nav { display: none; gap: 0.25rem; margin-inline-start: auto; }
.site-nav a {
  padding: 0.375rem 0.875rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: hsl(var(--forest-text-muted));
  text-decoration: none;
}
.site-nav a:hover { color: hsl(var(--cream)); background: hsl(var(--cream) / 0.06); }
.site-header__actions { display: flex; gap: 0.5rem; margin-inline-start: auto; }
.site-header__actions .btn { padding: 0.5rem 1rem; font-size: 0.875rem; }
@media (min-width: 900px) {
  .site-nav { display: flex; }
  .site-header__actions { margin-inline-start: 0; }
}
@media (max-width: 520px) {
  .site-header__actions .btn:not(.btn--accent) { display: none; }
}

/*
 * The first band runs up underneath the header.
 *
 * This is what lets the header be translucent: the bar always has that band's own dark
 * surface behind it rather than the white body background. The band is pulled up by the
 * header's height and given it back as padding, so nothing it contains moves.
 */
main > :first-child {
  margin-top: calc(-1 * var(--header-h));
  padding-top: var(--header-h);
}

/* ------------------------------------------------------------------ hero ---- */
/*
 * The hero is its own band because its structure is the point.
 *
 * Measured off the reference: the content column is a TEN column grid with 1px gaps over
 * the line colour, and the lattice you see is those gaps. Three rows:
 *
 *   row 1   [cell] [ announce, spanning columns 2..9 ] [cell]
 *   row 2   [cell] [ the hero itself, spanning 2..9  ] [cell]
 *   row 3   ten empty cells, the "floor"
 *
 * So the vertical hairlines beside the content and the open lattice underneath it are the
 * same mechanism, not decoration drawn on top. Below 640px the whole thing collapses to
 * one column and the floor is dropped: a phone has no width to spend on empty cells.
 */
.hero {
  display: grid;
  overflow: hidden;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--rule);
  padding-top: var(--rule);
  background: hsl(var(--line));
  color: hsl(var(--text));
}
.hero__rail { display: none; background: hsl(var(--bg)); }

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--rule);
  background: hsl(var(--line));
}
.hero__grid > * { background: hsl(var(--bg)); }
.hero__cell { display: none; background: hsl(var(--bg)); }

@media (min-width: 640px) {
  .hero {
    grid-template-columns:
      minmax(var(--gutter), 1fr)
      minmax(0, var(--content))
      minmax(var(--gutter), 1fr);
  }
  .hero__rail { display: block; }
  .hero__grid { grid-template-columns: repeat(10, minmax(0, 1fr)); }
  .hero__cell { display: block; }
  .hero__announce,
  .hero__main { grid-column: 2 / -2; }
}

.hero__main {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.75rem;
  padding: clamp(2.5rem, 1rem + 7vw, 4.5rem) var(--gutter) clamp(3rem, 1.5rem + 7vw, 5rem);
  text-align: center;
}
/*
 * A faint dot field behind the hero text, masked away at the edges so it fades rather
 * than stopping at a hard line. Drawn from the line colour, so it follows the surface.
 */
.hero__main::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(hsl(var(--line)) 1px, transparent 1px);
  background-size: 22px 22px;
  -webkit-mask-image: radial-gradient(110% 80% at 50% 40%, #000 20%, transparent 75%);
  mask-image: radial-gradient(110% 80% at 50% 40%, #000 20%, transparent 75%);
}
.hero__main > * { position: relative; }

/* The empty row of cells under the hero: the reference's open floor, and the breathing
 * space the page needs before the first real section. */
.hero__floor { display: none; }
@media (min-width: 640px) {
  .hero__floor {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: repeat(10, minmax(0, 1fr));
    gap: var(--rule);
    background: hsl(var(--line));
    height: clamp(72px, 7vw, 110px);
  }
  .hero__floor > * { background: hsl(var(--bg)); }
}

/*
 * The whole bar is the link, so the click target is the band rather than two words in the
 * middle of it. Measured off the reference, which is why the numbers look arbitrary:
 * 15px/24px text, 10px by 16px of padding, 12px between the sentence and its call to
 * action. The call to action is the only bold thing on the page outside a heading.
 */
.announce {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.25rem 0.75rem;
  padding: 0.625rem 1rem;
  background: hsl(var(--lime));
  color: hsl(var(--forest));
  font-size: var(--text-base);
  line-height: 1.6;
  text-decoration: none;
}

/*
 * The arrow sits OUTSIDE the underline, which is the whole trick: underlining it too
 * drags a rule under the glyph and the pair stops reading as "label, then direction".
 */
.announce__cta {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.25rem;
  font-weight: 600;
}
.announce__cta > span {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
.announce__cta svg { width: 0.95em; height: 0.95em; transition: transform 200ms ease; }
.announce:hover .announce__cta svg { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  .announce__cta svg { transition: none; }
}
.announce:hover { background: hsl(var(--lime) / 0.88); }

/*
 * 24ch is the width at which the headline breaks into two full lines rather than
 * stranding "and get your" alone above the rotating phrase. `balance` then evens them.
 * The height is pinned by the script to the tallest phrase: pinning the rotator's WIDTH
 * means a longer phrase can take an extra line at some viewports, and a hero that grows
 * every few seconds shoves the whole page down.
 */
/*
 * The headline breaks where we say, not where the browser decides, and the rotating
 * phrase gets a line to itself.
 *
 * Both of those are forced by the same fact: the two phrases differ in width by about
 * 60%, so at most viewports one of them wraps and the other does not. Sharing a line with
 * "and get your" meant the headline was two lines for one phrase and three for the other,
 * and reserving the taller left a dead band under the short one. (`text-wrap: balance`
 * made it worse by rebalancing differently during the measuring pass than in the render.)
 * On its own line each phrase is typographically identical to the other and nothing moves.
 *
 * The cap is in `em`, so it scales with the headline's own fluid size rather than needing
 * a second clamp.
 */
/*
 * A definite width, not shrink-to-fit. The headline is a centred flex item, so left to
 * itself it sizes to its content, which includes the rotator, whose width the script
 * pins: the headline then narrows around the short phrase and the long one wraps. Fixing
 * the width breaks that circle, and the line count stops depending on which phrase is up.
 */
.hero__title { width: min(100%, 16em); min-height: var(--title-h, auto); }
.hero__line { display: block; }
.hero__title .tile { margin-inline: 0.12em 0.06em; }
/* Balanced, since a centred block with a short last line reads as ragged. */
.hero__lede { max-width: 44ch; text-wrap: balance; }
/* The predictions are what these buyers come for, so their sentence is lit (Sahil, Oct 30 2026). */
.hero__lead-in { color: hsl(var(--text)); }

.hero__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; }
/* Stacked buttons of two different widths read as ragged, so below the wrap they match. */
@media (max-width: 520px) {
  .hero__actions { flex-direction: column; align-items: stretch; width: min(100%, 19rem); }
}

/*
 * The alternating outcome in the headline.
 *
 * Server-rendered with the first phrase already in place, so with JavaScript off the
 * headline is a complete true sentence rather than a gap.
 */
/*
 * The two things the headline is about, in the accent: the exam and the outcome. Both
 * carry it, so the eye reads "TEF Canada ... Permanent Residence" before the sentence.
 */
.accent-word { color: hsl(var(--lime)); }

/*
 * A word the headline can afford to lose on a phone. Sahil, Oct 28 2026: the hero reads
 * fine as "Ace TEF Canada exam" at that size, and the point is to hold the headline to two
 * lines. The trailing space lives INSIDE the span, or hiding the word leaves a double one.
 *
 * It is not sufficient on its own: dropping it takes line 1 from 441px to 385px at 390px
 * wide, where only 358px is available, so the phone type scale below does the rest.
 */
@media (max-width: 639px) {
  .phone-drop { display: none; }
}

.rotator {
  position: relative;
  display: inline-grid;
  vertical-align: top;
  color: hsl(var(--lime));
}
/*
 * **The cell is as wide as the longest phrase, and the phrases sit at its start.**
 *
 * This is the one rule that keeps the words before the rotator still. An earlier version
 * pinned the cell's width to the active phrase and transitioned it, which centred nicely
 * but meant the whole line re-centred on every swap and "to get your" visibly slid. The
 * reference does it this way for the same reason: a constant-width cell with the slack
 * hanging off the end, so nothing upstream of it ever moves.
 *
 * Below 640px the phrase is allowed to wrap, and is centred there: at that size the line
 * breaks anyway and start-alignment would leave a ragged gap.
 */
.rotator > * { grid-area: 1 / 1; justify-self: center; }
@media (min-width: 640px) {
  .rotator > * { white-space: nowrap; justify-self: start; }
}

.rotator__item { visibility: hidden; }
.rotator__item[data-active] { visibility: visible; }

/* The space a literal one used to provide, now that the text is split into spans. */
.rotator__item .tile { margin-inline-end: 0.22em; }

.rotator__word { white-space: nowrap; }
.rotator__ch { display: inline-block; }

/*
 * Each letter rises out of a blur, 34ms behind the one before it. Measured off the
 * reference: blur(8px), 0.4em of travel, and a stagger small enough that the phrase reads
 * as one gesture rather than as letters arriving one at a time.
 *
 * `backwards` is doing real work here: without it a letter is fully visible during its own
 * delay and the stagger does not exist.
 */
@keyframes rotator-ch-in {
  from { opacity: 0; filter: blur(8px); transform: translateY(0.4em); }
  to   { opacity: 1; filter: blur(0);   transform: none; }
}
.rotator__item[data-active] .rotator__ch {
  animation: rotator-ch-in 520ms cubic-bezier(0.22, 0.9, 0.3, 1) backwards;
  animation-delay: calc(var(--i) * 34ms);
}

/*
 * The tile springs rather than fades: in from half size and a few degrees further round,
 * overshooting past full size before settling. The overshoot is the bezier's y > 1, which
 * is legal and is the cheapest spring there is. It ends on the tile's own --tile-rot, so
 * the rest tilt is whatever the markup asked for.
 */
@keyframes rotator-tile-in {
  from {
    opacity: 0;
    transform: rotate(calc(var(--tile-rot, 0deg) - 7deg)) scale(0.5);
  }
  to {
    opacity: 1;
    transform: rotate(var(--tile-rot, 0deg)) scale(1);
  }
}
.rotator__item[data-active] .tile {
  animation: rotator-tile-in 620ms cubic-bezier(0.18, 1.8, 0.4, 1) backwards;
}

/*
 * measure() makes each phrase active in turn to find the tallest arrangement. Without
 * this the enter animation fires on every phrase on load, on font load and on every
 * resize, and the headline flickers.
 */
.rotator[data-measuring] .rotator__ch,
.rotator[data-measuring] .tile { animation: none; }

@media (prefers-reduced-motion: reduce) {
  /*
   * The script does not rotate at all under reduced motion, so the first phrase simply
   * stays. It still has to be visible, which means killing the enter animation rather
   * than letting it hold its `from` frame.
   */
  .rotator__ch, .rotator__item .tile { animation: none; }
}

/* The row of proof points under the hero buttons. */
/*
 * Sized down to match the reference's own proof row: 13px items on 20px tiles, not 15 on
 * 24. It is proof, not a headline, so it should read as a footnote under the paragraph
 * rather than as the widest thing in the hero.
 *
 * **It must stay on one line.** Sahil, Oct 28 2026: the stacked version looked wrong, so
 * the labels are bare nouns rather than sentences. Shortening the copy is the lever here,
 * not a width cap: an earlier cap to the lede's measure is what forced the wrap.
 */
.inside {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
}
.inside__item {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-size: var(--text-sm);
  color: hsl(var(--text));
}
/*
 * The mono face gives the comma a full character cell, so "1,365" reads as "1, 365" at
 * this size. Pulling the tracking in closes it without touching the figures themselves.
 */
.inside__n { font-family: var(--font-mono); font-weight: 600; letter-spacing: -0.045em; }
@media (max-width: 640px) {
  .inside { gap: 0.6rem 1rem; }
  .inside .label { flex-basis: 100%; text-align: center; }
}

/* ---------------------------------------------------------------- footer ---- */

.site-footer { font-size: var(--text-base); }
.site-footer__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2rem;
}
/*
 * Column count comes from the markup (`--cols`), so chrome.mjs owns it. Between 720 and
 * 1024 the brand takes a row of its own: beside three link columns it left each about
 * 120px, and every "How listening is marked" in the Learn column wrapped to two lines.
 */
@media (min-width: 720px) {
  .site-footer__cols { grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); }
  .site-footer__cols > :first-child { grid-column: 1 / -1; }
}
@media (min-width: 1024px) {
  .site-footer__cols { grid-template-columns: 1.4fr repeat(var(--cols, 3), minmax(0, 1fr)); }
  .site-footer__cols > :first-child { grid-column: auto; }
}
.site-footer a { text-decoration: none; color: hsl(var(--text-muted)); }
.site-footer a:hover { color: hsl(var(--text)); }
.site-footer__list { display: flex; flex-direction: column; gap: 0.5rem; }
/* Centred across the page: three short notices that belong to the site, not to a column. */
.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  text-align: center;
  gap: 0.5rem 1.5rem;
  /*
   * Twice the columns' own 2rem gap, so the notices read as a separate block rather
   * than as a fourth row of the Practise and Company lists. At 1.75rem they sat closer
   * to the links than the columns sat to each other, which is what made them crowd.
   */
  padding-top: 4rem;
  color: hsl(var(--text-muted));
}

/* ---------------------------------------------------------------- guides ---- */
/*
 * The guide pages under /guides, prerendered so search engines read them (Oct 29 2026).
 *
 * The guide HTML is the content build's, already typeset by tools/guide_typeset.py, and
 * the app styles the same classes in src/styles/app.css. This is that block redrawn in
 * this system's grammar: square corners, hairlines, no shadows. Every `g-` class here is
 * one that file emits, so read its table before adding one.
 */
.guide-card__head { display: flex; align-items: center; gap: 0.875rem; }
.guide-card__links { list-style: none; margin: 0.25rem 0 0; padding: 0; display: grid; gap: 1px; background: hsl(var(--line)); }
.guide-card__links a {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.875rem 0; background: hsl(var(--bg));
  font-size: var(--text-md); text-decoration: none;
}
.guide-card__links a svg { width: 1rem; height: 1rem; color: hsl(var(--text-muted)); transition: transform 120ms ease; }
.guide-card__links a:hover svg { transform: translateX(3px); color: hsl(var(--text)); }

.guide { max-width: 44rem; }

/*
 * English or French, as two radio buttons drawn as a segmented control. No script:
 * `:has()` shows the chosen language, and a browser without it keeps English.
 */
.guide__lang {
  position: relative;
  display: inline-flex; gap: 1px; margin: 0 0 2rem; padding: 0; border: 0;
  background: hsl(var(--line)); box-shadow: 0 0 0 1px hsl(var(--line));
}
.guide__lang label {
  padding: 0.4375rem 0.875rem; cursor: pointer;
  background: hsl(var(--bg)); color: hsl(var(--text-muted));
  font-size: var(--text-sm); font-weight: 500;
}
.guide__lang input:checked + label { background: hsl(var(--forest)); color: hsl(var(--cream)); }
.guide__lang input:focus-visible + label { outline: 2px solid hsl(var(--lime)); outline-offset: 2px; position: relative; }
.guide__fr { display: none; }
.guide:has(#lang-fr:checked) .guide__en { display: none; }
.guide:has(#lang-fr:checked) .guide__fr { display: block; }

/* ------------------------------------------------------------- practice ---- */
/*
 * /practice-tests (Oct 30 2026): a cell per paper, then two real questions. The options
 * and the answer are hairline lists by the grid-gap mechanism, the answer reusing the
 * FAQ's <details>, so nothing here draws a border.
 */
.practice-card .btn { align-self: flex-start; margin-top: auto; }

.sample { gap: 1rem; }
.sample audio { width: 100%; }
.sample__passage { padding: 1rem 1.125rem; background: hsl(var(--bg-sunken)); font-size: 1.0625rem; }
.sample__q { font-size: var(--text-md); font-weight: 500; line-height: 1.4; }
.sample__options {
  list-style: none; margin: 0; padding: var(--rule) 0;
  display: grid; gap: var(--rule); background: hsl(var(--line));
}
.sample__options li {
  display: flex; align-items: baseline; gap: 0.75rem;
  padding-block: 0.625rem; background: hsl(var(--bg)); line-height: 1.45;
}
.sample__key { flex: none; width: 1rem; font-family: var(--font-mono); font-size: var(--text-xs); color: hsl(var(--text-muted)); }
/* The options already drew the rule this list would open with. */
.sample__reveal { padding-top: 0; margin-top: -1rem; }
.sample__reveal .faq__q { font-size: var(--text-base); }
.sample__right { font-weight: 500; }
.sample__extra { margin-top: 0.75rem; }
.sample__text { font-size: var(--text-base); line-height: 1.6; }
.sample > .link { margin-top: auto; }

.guide-next { gap: 0.5rem; text-decoration: none; align-items: flex-start; }
a.guide-next:hover .h3 { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; }
.guide-next .btn { margin-top: 0.5rem; }

.prose {
  font-size: var(--text-md); line-height: 1.7; color: hsl(var(--text));
  overflow-wrap: break-word;
}
.prose > * + * { margin-top: 1rem; }
.prose > :first-child { margin-top: 0; }
.prose :is(h2, h3, h4, h5) { font-weight: 500; line-height: 1.25; letter-spacing: -0.02em; }
.prose h3 { font-size: clamp(1.25rem, 1.15rem + 0.45vw, 1.5rem); letter-spacing: -0.03em; }
.prose h4 { font-size: var(--text-md); }
.prose :is(ul, ol) { padding-inline-start: 1.25rem; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li { margin-top: 0.375rem; }
.prose li::marker { color: hsl(var(--text-muted)); }
.prose :is(strong, b) { font-weight: 600; }
.prose hr { border: 0; height: 1px; background: hsl(var(--line)); margin-block: 2rem; }
/* `display: block` is what lets the five column CLB table scroll inside a phone. */
.prose table {
  display: block; width: 100%; overflow-x: auto; overscroll-behavior-x: contain;
  border-collapse: collapse; font-size: var(--text-base);
}
.prose :is(td, th) { white-space: nowrap; padding: 0.5rem 0.875rem; text-align: left; border-bottom: 1px solid hsl(var(--line)); }
.prose :is(td, th):first-child { padding-inline-start: 0; }
.prose thead th { font-weight: 500; font-size: var(--text-sm); color: hsl(var(--text-muted)); }
.prose table :where(td, th):not(:first-child) { font-variant-numeric: tabular-nums; }

.prose--guide { --bad: 4 66% 46%; }
/*
 * Articles (/guides/tef-canada-..., Nov 1 2026): our own prose, so h2 sections, sized and
 * ruled like a guide's h3. `.a-plug` is the boxed way into the app inside the text.
 */
.prose--article h2 {
  font-size: clamp(1.25rem, 1.15rem + 0.45vw, 1.5rem); letter-spacing: -0.03em;
  margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid hsl(var(--line));
}
.prose--article > h2:first-child { border-top: 0; padding-top: 0; }
.prose--article h2 + * { margin-top: 0.625rem; }
.prose--article .link { text-underline-offset: 3px; }
.a-plug {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 0.75rem 1.25rem; padding: 1rem 1.25rem;
  background: hsl(var(--lime) / 0.22); box-shadow: inset 3px 0 0 hsl(var(--forest));
}
.a-plug p { flex: 1 1 18rem; margin: 0; }
.a-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
/* A comparison read across must not hide a column off a phone's edge: these wrap. */
.prose--article table.a-wrap { display: table; table-layout: fixed; }
.prose--article table.a-wrap :is(td, th) { white-space: normal; vertical-align: top; }
/*
 * A free topic shown whole inside an article. Hairlines by the grid-gap mechanism, as
 * everywhere on this site; the prompt is a forest panel like the app's. French in the
 * serif, English always shown under it, quieter. Every `t-` class is one
 * tools/topic_typeset.py emits.
 */
.topic-ex {
  margin: 1.25rem 0 0; display: grid; gap: 1px; padding: 1px; background: hsl(var(--line));
}
.topic-ex > * { background: hsl(var(--bg)); padding: 1.125rem clamp(1rem, 0.75rem + 1vw, 1.5rem); }
.topic-ex .label { margin-bottom: 0.5rem; }
.topic-ex__prompt { background: hsl(var(--forest)); color: hsl(var(--cream)); }
.topic-ex__prompt .label { color: hsl(var(--cream) / 0.6); }
.topic-ex__fr { font-family: var(--font-serif); font-size: 1.125rem; line-height: 1.5; }
.topic-ex__en { margin-top: 0.5rem; font-size: var(--text-sm); line-height: 1.5; color: hsl(var(--cream) / 0.72); }
.topic-ex__img { display: grid; place-items: center; background: hsl(var(--paper-muted)); }
.topic-ex__img img { width: auto; max-width: 100%; max-height: min(60vh, 28rem); }
.topic-ex figcaption { padding-block: 0.75rem; }
.topic-ex__body { font-family: var(--font-serif); line-height: 1.6; }
.topic-ex__body > p + p { margin-top: 1rem; }
.topic-ex__body .t-en {
  display: block; margin-top: 0.3rem; font-family: var(--font-sans); font-size: var(--text-sm);
  line-height: 1.5; color: hsl(var(--text-muted));
}
.topic-ex__body :is(ul, ol) { list-style: none; padding: 0; margin: 0; }
.topic-ex__body li { margin: 0; padding-block: 0.75rem; }
.topic-ex__body li + li { border-top: 1px solid hsl(var(--line)); }
.topic-ex__body li:first-child { padding-top: 0; }
.topic-ex__body li:last-child { padding-bottom: 0; }
/* Speaking A: the subject, then the question. */
.topic-ex__body .t-asks li { display: grid; gap: 0.125rem; }
.topic-ex__body .t-ask__key { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: hsl(var(--ok)); }
.topic-ex__body .t-ask__key .t-en { display: inline; margin-inline-start: 0.4rem; font-weight: 500; }
/* Speaking B: the numbered objection, then the reply under an arrow. */
.topic-ex__body .t-counters { counter-reset: obj; }
.topic-ex__body .t-counters li { counter-increment: obj; }
.topic-ex__body :is(.t-obj, .t-reply) { position: relative; padding-inline-start: 2rem; }
.topic-ex__body .t-obj { font-weight: 600; }
.topic-ex__body .t-obj .t-en { font-weight: 500; }
.topic-ex__body .t-obj::before {
  content: counter(obj); position: absolute; inset-inline-start: 0; top: 0.1em;
  display: grid; place-items: center; width: 1.375rem; height: 1.375rem;
  background: hsl(var(--part) / 0.2); font-family: var(--font-sans); font-size: var(--text-xs);
}
.topic-ex__body .t-reply { margin-top: 0.5rem; }
.topic-ex__body .t-reply::before {
  content: "\2192"; position: absolute; inset-inline-start: 0.25rem; top: 0;
  font-family: var(--font-sans); color: hsl(var(--ok));
}
/* Writing: the article's closing quote, and the letter's salutation and signature. */
.topic-ex__body .t-quote { padding-inline-start: 1rem; box-shadow: inset 3px 0 0 hsl(var(--ok) / 0.55); }
.topic-ex__body .t-salute { font-weight: 600; }
.topic-ex__body .t-close { margin-top: 1.5rem; }
.topic-ex__body .t-sign { text-align: end; }

/* The NCLC calculator: a row per skill, the level beside the score. */
.nclc-calc { display: grid; gap: 1px; padding: 1px; background: hsl(var(--line)); margin-top: 1rem; }
.nclc-calc__row {
  display: grid; grid-template-columns: minmax(0, 1fr) 6rem 7.5rem; align-items: center; gap: 1rem;
  padding: 0.75rem 1rem; background: hsl(var(--bg));
}
.nclc-calc input {
  width: 100%; padding: 0.5rem 0.625rem; font: inherit; font-variant-numeric: tabular-nums;
  background: hsl(var(--bg)); color: inherit; border: 0; border-radius: 0;
  box-shadow: inset 0 0 0 1px var(--control-line);
}
.nclc-calc input:focus-visible { outline: 2px solid hsl(var(--forest)); outline-offset: 1px; }
.nclc-calc output { font-weight: 600; color: hsl(var(--ok)); }
.nclc-calc__all { display: block; padding: 0.75rem 1rem; background: hsl(var(--bg-sunken)); min-height: 3rem; }
@media (max-width: 480px) {
  .nclc-calc__row { grid-template-columns: minmax(0, 1fr) 5rem; }
  .nclc-calc__row output { grid-column: 1 / -1; }
  .nclc-calc__row output:empty { display: none; }
}

.prose--article > .fine { margin-top: 3rem; color: hsl(var(--text-muted)); }

.prose--guide .g-kicker {
  display: flex; flex-wrap: wrap; gap: 0.25rem 0.5rem;
  font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: var(--track-label);
  text-transform: uppercase; line-height: 1.5; color: hsl(var(--text-muted));
}
.prose--guide .g-kicker span + span::before { content: "\00B7"; margin-inline-end: 0.5rem; }

/* Each h3 opens a section and draws the rule the source drew with an <hr>. */
.prose--guide h3 { margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid hsl(var(--line)); }
.prose--guide > h3:first-child, .prose--guide .g-kicker + h3 { border-top: 0; padding-top: 0; }
.prose--guide .g-kicker + h3 { margin-top: 0.75rem; }
.prose--guide h4 { margin-top: 1.75rem; }
/* h5 is the source's other way of writing a label, so it looks like one. */
.prose--guide h5 { font-size: inherit; font-weight: 600; letter-spacing: 0; line-height: inherit; margin-top: 1.25rem; }
.prose--guide :where(h3, h4, h5) + * { margin-top: 0.625rem; }

/* A bold line that names the list under it, so the two read as one unit. */
.prose--guide .g-label { margin-top: 1.25rem; font-weight: 600; }
.prose--guide :is(.g-label, h5) + :is(ul, ol, p) { margin-top: 0.25rem; }
.prose--guide li > ul { margin-top: 0.25rem; }

.prose--guide .g-ticks { list-style: none; padding: 0; }
.prose--guide .g-ticks li { position: relative; padding-inline-start: 1.75rem; }
.prose--guide .g-ticks li::before {
  content: ""; position: absolute; inset-inline-start: 0; top: 0.35em;
  width: 1.0625rem; height: 1.0625rem; background: hsl(var(--ok));
  -webkit-mask: var(--tick) center / contain no-repeat; mask: var(--tick) center / contain no-repeat;
  --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='square'%3E%3Cpath d='M4.5 12.5l5 5L19.5 6.5'/%3E%3C/svg%3E");
}
.prose--guide .g-arrows { list-style: "\2192\00A0\00A0"; }
.prose--guide .g-arrows li::marker { color: hsl(var(--ok)); }

/* A fact under a section heading, such as its duration. */
.prose--guide .g-meta {
  display: inline-flex; align-items: center; gap: 0.4rem; width: fit-content;
  padding: 0.125rem 0.625rem; background: hsl(var(--bg-sunken));
  box-shadow: inset 0 0 0 1px hsl(var(--line)); font-size: var(--text-sm); line-height: 1.6;
}
.prose--guide .g-meta strong { font-weight: 600; }

/* Callouts. The emoji they replace are gone; the icon is drawn here instead. */
.prose--guide :is(.g-note, .g-alert) {
  position: relative; padding: 0.875rem 1rem 0.875rem 2.75rem;
  background: hsl(var(--bg-sunken)); box-shadow: inset 3px 0 0 hsl(var(--line));
}
.prose--guide :is(.g-note, .g-alert)::before {
  content: ""; position: absolute; inset-inline-start: 1rem; top: 1.1rem;
  width: 1.0625rem; height: 1.0625rem; background: currentColor; color: hsl(var(--text-muted));
  -webkit-mask: var(--icon) center / contain no-repeat; mask: var(--icon) center / contain no-repeat;
}
.prose--guide :is(.g-note--warn, .g-alert) { background: hsl(var(--part) / 0.1); box-shadow: inset 3px 0 0 hsl(var(--part)); }
.prose--guide :is(.g-note--warn, .g-alert)::before { color: hsl(var(--part)); }
.prose--guide .g-note--no { background: hsl(var(--bad) / 0.07); box-shadow: inset 3px 0 0 hsl(var(--bad)); }
.prose--guide .g-note--no::before { color: hsl(var(--bad)); }
.prose--guide .g-note--calm { font-style: italic; color: hsl(var(--text-muted)); }
.prose--guide .g-alert + .g-alert { margin-top: 0.5rem; }
.prose--guide :is(.g-note--warn, .g-alert) {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 1.5 21h21L12 2.5Zm-1.2 7h2.4l-.3 6h-1.8l-.3-6Zm1.2 7.6a1.4 1.4 0 1 1 0 2.8 1.4 1.4 0 0 1 0-2.8Z'/%3E%3C/svg%3E");
}
.prose--guide .g-note--time {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='13' r='8.5'/%3E%3Cpath d='M12 8.5V13l3 2M9.5 2.5h5'/%3E%3C/svg%3E");
}
.prose--guide .g-note--no {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9.5'/%3E%3Cpath d='m8.5 8.5 7 7m0-7-7 7'/%3E%3C/svg%3E");
}
.prose--guide .g-note--write {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20h4L19.5 8.5a2.8 2.8 0 0 0-4-4L4 16v4Z'/%3E%3C/svg%3E");
}
.prose--guide .g-note--calm {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M3 12c2-3 4-3 6 0s4 3 6 0 4-3 6 0'/%3E%3C/svg%3E");
}

/* The model prompt ("Consigne-type"), as the card a candidate is handed. */
.prose--guide blockquote {
  margin: 0; padding: 1rem 1.25rem;
  background: hsl(var(--forest)); color: hsl(var(--cream));
  font-family: var(--font-serif); font-size: 1.125rem; line-height: 1.55;
}
.prose--guide blockquote strong { font-weight: 500; }
.prose--guide table { margin-top: 1.25rem; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
