/* ==========================================================================
   ek — an independent design system, under evaluation

   Not an extension of assets/everyday.css and not themed from it. It never
   imports that file and never aliases an --everyday-* token; the brand hex
   values below are repeated on purpose, because two systems being compared
   have to be able to drift apart. Coupling them would answer the question
   this experiment exists to ask.

   Read CLAUDE.md § The ek experiment before changing anything here.

   THESE VALUES ARE PROVISIONAL. They are extracted from /duo/index.html,
   which is where the direction was actually decided on screen — but many of
   those numbers were eyeballed to get a page standing up, not reasoned to.
   Where the extraction found something incoherent it is marked DRIFT below
   rather than quietly corrected. "It came from /duo" is not a reason to keep
   a value.

   Foundations first, then components. A component only exists here once it
   is also rendered in /ek/kit.html — no demo, no component.
   ========================================================================== */

@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css');

.ek-root {
  /* ── Brand ────────────────────────────────────────────────────────────
     EVERYDAY. Every hex in this block and the two below is assets/everyday.css's
     own value, repeated here because the isolation contract forbids aliasing
     it. None of it is the reference's: their green is rgb(88,204,2), their
     blue rgb(28,176,246), their inks rgb(60,60,60) / rgb(75,75,75) /
     rgb(119,119,119) / rgb(175,175,175). ek resembles them in geometry and
     depth, never in color. */
  --ek-brand: #1E40AF;
  --ek-brand-pressed: #1E3A8A;
  --ek-brand-light: #60A5FA;
  --ek-accent: #E63946;          /* critical only — never an ordinary CTA */
  /* EVERYDAY: --everyday-fg-positive, which is what /checkout already paints
     its saving with. Not a general success colour — ek has no positive and
     negative pair, and one label does not justify inventing a semantic family.
     One use: the plan card's saving. */
  --ek-positive: #008B4D;
  --ek-highlight: #FCD34D;
  --ek-deep: #0B1A35;

  /* ── Surfaces ─────────────────────────────────────────────────────────
     EVERYDAY — assets/everyday.css's values, as above. */
  --ek-surface: #FFFFFF;
  --ek-surface-sunken: #F4F4F5;
  /* --ek-surface-deep was an alias of --ek-deep that nothing referenced;
     .ek-surface-deep uses --ek-deep directly. Deleted. */
  /* EVERY color-mix() IN THIS FILE IS PRECEDED BY THE SAME COLOUR AS A STATIC
     VALUE, and that is not belt-and-braces. color-mix() is Safari 16.2 and
     the app's IPHONEOS_DEPLOYMENT_TARGET is 15.0, so on iOS 15.0-16.1 each of
     those declarations is invalid at parse time and every var() reading it is
     invalid at computed-value time.

     Measured rather than guessed, by neutralising all fourteen and diffing:
     without the fallbacks --ek-ledge-brand dies and EVERY .ek-control loses
     its bottom face while keeping the 4px it reserved for it, and --ek-tint
     dies with it, so the chosen pickable, .ek-badge and .ek-panel--tint lose
     their fills. Nothing became unreadable, but every button on every screen
     looked broken.

     The static values were resolved BY THE BROWSER from the expressions
     beside them, not computed by hand. Change a brand colour and both lines
     change — that duplication is the cost, and it is cheaper than a flat
     button on a device you cannot reproduce. */
  --ek-tint: rgba(30, 64, 175, 0.06);
  --ek-tint: color-mix(in srgb, var(--ek-brand) 6%, transparent);
  /* MEASURED rgba(0,0,0,0.4) — the scrim behind duolingo.com's log-in modal,
     at 1440 AND 393, one viewport-sized element on each run
     (`div._1ATOC._1Fnem`, both login probes). It is the one place either
     system paints black rather than mixing the navy: a tint of the navy over
     a photograph reads as a colour cast, where a neutral scrim reads as the
     light going down.
     This shipped at 0.5 for one commit, written as a decision, while the
     measurement was already in scripts/probe/measurements. Corrected, and
     worth the note: a value invented next to a probe nobody re-read is the
     failure mode this whole traceability apparatus exists to catch. */
  --ek-scrim: rgba(0, 0, 0, 0.4);
  /* Heavier, for a control sitting ON an image rather than a sheet over the
     page: a play badge at 0.4 disappears into a bright photo. */
  --ek-scrim-strong: rgba(0, 0, 0, 0.6);

  /* ── Ink ──────────────────────────────────────────────────────────────
     EVERYDAY — assets/everyday.css's values, as above.

     Two inks, because /duo really does use two. Default running text is a
     neutral near-black; the navy is a marketing color that happens to be
     used as type on light marketing surfaces as well as being the dark
     ground itself. Collapsing them into one token turns every paragraph on
     the site navy, which is a page-wide change nobody asked for.

     The neutral is the one that is easy to miss: /duo never writes it. It
     inherits from everyday.css, so it appears nowhere in /duo's own CSS and
     an extraction that reads only /duo will not see it at all. */
  --ek-ink: #212124;                       /* running text */
  --ek-ink-marketing: var(--ek-deep);   /* type on marketing surfaces */
  --ek-ink-muted: rgba(11, 26, 53, 0.6);   /* derived from the navy… */
  --ek-ink-subtle: rgba(11, 26, 53, 0.45); /* …not from --ek-ink */
  --ek-ink-on-solid: #FFFFFF;
  --ek-stroke: #E2E2E5;

  /* ── Depth ──────────────────────────────────────────────────────────── */
  /* A control's depth is a solid lower face, never a blurred shadow, and it
     lives INSIDE the control's own box: a 50px button occupies 50px, face
     plus ledge. Anything else makes a height spec a lie. On press the ledge
     collapses and the face takes its place, so nothing below shifts. */
  /* Two depths, not one. The reference gives its primary control a 4px ledge
     and everything else 2px, so the main action is the one that stands
     furthest off the page. A single depth flattens that hierarchy. */
  /* MEASURED 4px on duolingo.com at both widths, and the mechanism is now
     readable: a 4px TRANSPARENT bottom border reserves the space, and
     ::before paints the face with box-shadow 0 4px 0 0 in the ledge color.
     /help does the same. We reach the same picture with two pseudo-elements
     so the face can travel while the ledge stays put. */
  --ek-depth: 4px;
  /* MEASURED 3px + 1px spread — about.duolingo.com's white control carries
     box-shadow 0 3px 0 1px behind a 5px transparent bottom border. Ours is
     2px, which nothing supports; kept for now because changing it moves
     every secondary control, and that is a look to decide on screen. */
  --ek-depth-subtle: 2px;
  --ek-stroke-control: 2px;
  --ek-hairline: 1px;

  /* The keyboard ring, in one place. It was written out five times with three
     different offsets (3, 2, 1) and, on the FAQ question, a different colour
     and width as well — five literals drifting apart, not five decisions. One
     ring now, and a component that genuinely needs a different offset sets
     --ek-focus-offset on itself rather than restating the ring. OURS: the
     reference is not keyboard-probed, and a focus ring is ours to get right
     regardless of what it does. */
  --ek-focus-ring: 3px solid rgb(168, 205, 252);
  --ek-focus-ring: 3px solid color-mix(in srgb, var(--ek-brand-light) 55%, white);
  --ek-focus-offset: 2px;

  /* OURS, and it has to be: the reference picks each ledge BY HAND, not by
     formula. Its green face rgb(88,204,2) takes ledge rgb(88,167,0) — red
     unchanged at 88, so no mix with black reproduces it — while its blue
     face rgb(28,176,246) takes rgb(24,153,214), which IS an 86% black mix.
     Two colors, two different relationships. In HSL terms the ledge drops
     lightness by x0.81 and x0.87 respectively; our 78% sits just below that
     range, applied to the already-darker pressed color. */
  --ek-ledge-brand: rgb(23, 45, 108);
  --ek-ledge-brand: color-mix(in srgb, var(--ek-brand-pressed) 78%, black);
  /* --ek-ledge-neutral was an alias of --ek-stroke that nothing
     referenced. Deleted. */
  /* A light control standing on the dark ground needs a ledge that reads
     against navy, not against white. */
  --ek-ledge-on-deep: rgb(162, 168, 178);
  --ek-ledge-on-deep: color-mix(in srgb, var(--ek-surface) 62%, var(--ek-deep));

  /* ── Type ───────────────────────────────────────────────────────────── */
  /* OURS, and it has to be. The reference runs din-round / din-next-rounded,
     Latin faces with no Hangul. Pretendard is the decision that makes a
     Korean product possible at all. */
  --ek-font: 'Pretendard', -apple-system, BlinkMacSystemFont,
                'Apple SD Gothic Neo', 'Segoe UI', Helvetica, Arial, sans-serif;

  /* Named by the job the text does, not by size — because right now the
     sizes are out of order (see DRIFT). Naming these hero/display/title
     would encode a hierarchy the page does not actually have. */
  /* MEASURED at BOTH ends on duolingo.com: its band headline reads 64 at
     1440 and 36 at 393 (and carries ls -1.28px / -0.72px, both -0.02em). */
  --ek-type-band: clamp(36px, 5vw, 64px);
  /* MEASURED at both ends: the section headline "free. fun. effective."
     reads 48 at 1440 and 36 at 393. 3.35vw lands on 48.2 at 1440. */
  --ek-type-section: clamp(36px, 3.35vw, 48px);
  /* MEASURED 32px at 1440 AND 393 on duolingo.com, and 32/40 on /help.
     This one genuinely does not scale. */
  --ek-type-page: 32px;
  /* Between page and section, for a single sentence that is the whole point
     of a screen — a mission line, a promise.
     MEASURED at both ends: about.duolingo.com sets the same element 40 at
     1440 and 27 at 393. 2.8vw lands on 40.3 at 1440 and clamps to 27 below
     964px, so both measured endpoints are reproduced. */
  --ek-type-statement: clamp(27px, 2.8vw, 40px);
  /* MEASURED 17px on all five runs — it is what <body> computes to on
     duolingo.com, /help and about.duolingo.com alike. Running text lands
     there too: 17/23.8 (about, "Press inquiries or media requests:") and
     17/24 (home, "Learning with Duolingo is fun"). Ours was 18, which
     nothing supports. */
  --ek-type-body: 17px;
  /* MEASURED 15px at 1440 AND at 393 on duolingo.com — the control's own
     font-size and its label ("Get started", 15/normal w700 ls0.8px) both
     read 15 at both widths. It does not scale.
     This reverses the 18/15 responsive rule set earlier. That rule came
     from a probe run that reported 18 on a desktop; these two runs, taken
     with the rewritten probe, disagree at the same element. Hoon's choice
     then was "whatever Duolingo does", so this follows it to the better
     measurement rather than away from his decision. */
  --ek-type-control: 15px;
  --ek-type-list-title: 19px;
  /* MEASURED 20/24 w700 — about.duolingo.com's card title ("Duolingo"),
     identical at 1440 and 393. Our card title was borrowing the 19px rung. */
  --ek-type-card-title: 20px;
  --ek-leading-card-title: 24px;
  /* MEASURED 15/22 w700 on two independent pages: the home page's footer
     links and about's nav list, both "Courses". */
  --ek-type-label: 15px;
  /* MEASURED 13/16 w700 — the smallest type on any run (duolingo.com's
     language list), identical at 1440 and 393. 11px had no counterpart. */
  --ek-type-caption: 13px;
  --ek-leading-caption: 16px;
  /* MEASURED 24/33.6 w700 — the sub-heading inside a post on
     blog.duolingo.com, IDENTICAL at 1440 and 393. Its leading is exactly
     24 x 1.4, so the ratio is --ek-leading-body and only the size is new.
     It is a real rung: a document that runs 17 for its body needs something
     between that and the 40 of its headline, and 19 or 20 is not it. */
  --ek-type-subhead: 24px;

  /* DRIFT — page (32px) is smaller than section (48px) at every width, so a
     hero headline reads as less important than the section headings under
     it. Left as measured; fixing it is a design decision.

     The phone inversion that sat here is gone: the band floor was 34px
     against a 36px section. The reference renders both of its headline
     levels at 36px on a phone, so raising the floor to 36 both matches it
     and removes the inversion. */

  /* MEASURED, and responsive: about.duolingo.com's statement is 40/44 at
     1440 (1.10) and 27/35.1 at 393 (1.30). The phone value is redeclared
     under the components below. --ek-leading-tight (1.15) is gone; it split
     the difference and matched neither end. */
  --ek-leading-statement: 1.1;
  --ek-leading-heading: 1.25;
  /* MEASURED 1.4: 23.8/17 on about, 24/17 on the home page. Ours was 1.5,
     which is 3px looser per line than anything the reference sets. */
  --ek-leading-body: 1.4;
  /* OURS. 1.4 is right for a line or two of supporting text and too tight for
     a paragraph somebody actually reads — at 17px it is 23.8, and Hoon's read
     of the quiz's full-screen panels was that the lines were stuck together.
     1.55 is 26.4 at the body size. Used where a block of prose is the point of
     the screen rather than a caption under something else. */
  --ek-leading-body-loose: 1.55;
  /* OURS, 1.45, and it is a deliberate departure from a reading we have.
     duolingo.com/terms runs 17/22.1 at 1440 and 18/23.4 at 393 — both exactly
     1.3 — where about and the home page run 1.4, so a document really does
     differ from a marketing page there. We took the 1.3 and it read as
     cramped; the reason is the other half of that page, which is that its
     document body is w500 in rgb(153,153,153) and ours is w400 in near-black.
     Same leading, more ink on the page. Rather than lighten a legal document
     to match — a page nobody reads is worse than one that looks heavy — the
     leading opens instead: 24.65 at 17px, 26.1 at the phone's 18px. Hoon's
     call, 2026-09-10, and the third time this system has found that a value
     measured on the reference's own body text is tight against our ink (see
     --ek-leading-body-loose). */
  --ek-leading-prose: 1.45;
  /* --ek-leading-relaxed: 1.6 was here, and it was the last `ours` value in
     this file waiting on a measurement it could not get: both /help probe
     runs were taken with every row closed, so the reference's own answer
     leading never appeared in them. Hoon opened a row in DevTools and read
     it off the element: 19px / 27px / w500 / rgb(60,60,60), in a 1000px
     content box — the same 1000 the probe found sixteen times.

     So the answer is not a leading problem at all. The reference sets it at
     19, the SAME size and weight as the question row above it; only the
     leading opens from 20 to 27. Ours was 17 on a 1.6 ratio, which computed
     to 27.2 — the right leading in absolute pixels, reached by stretching
     the wrong size. It is on the 19/26.6 pair now, which this system already
     had, and 26.6 against the measured 27 is the sub-pixel kind of
     difference it has already decided not to add a rung for. The token is
     gone; nothing else referenced it. */
  /* MEASURED 17px at 1440. The phone value is 18px, redeclared on the
     component: their document type gets BIGGER on a phone, not smaller,
     which is the opposite of the usual rule and worth not smoothing over. */
  --ek-type-prose: 17px;

  /* THE LESSON RUNG. A lesson body is not prose and it is not a UI label: it
     is lines to be read aloud, in a language the reader cannot yet see at a
     glance, with the romanisation and the English on the same line. Two things
     follow. It is bigger than body — 17 is the size of a caption to someone
     decoding 취미가 뭐예요 for the first time. And it is set on 2, because the
     teacher's newlines ARE the paragraphs: the content arrives as one block of
     text with line breaks, so nothing else is spacing one phrase off the next.

     Both numbers are what the classroom already shipped, and shipping them
     as 17/1.45 for one afternoon is what proved they were not arbitrary. */
  --ek-type-lesson: 20px;
  --ek-leading-lesson: 2;
  /* MEASURED 19/20 w700 — the section heading inside duolingo.com/terms,
     identical at both widths. Tighter than the 19/26.6 a footer heading uses;
     a heading that sits on one line does not need the leading. */
  --ek-leading-prose-heading: 20px;
  /* 24px, not 1.3333: the ratio lands on 23.9994 and the half-pixel shows up
     as different antialiasing on every line of a paragraph. A leading that is
     meant to be a round number should say so. */
  --ek-leading-body-tight: 24px;
  /* Two rungs, because the reference is two systems and we borrow from
     both. MEASURED 19/26.6 on duolingo.com's footer heading ("About us")
     at 1440 and 393 alike — that is the footer we built. MEASURED 19/28
     on about.duolingo.com for the "Our mission" eyebrow and the card body
     — that is the About page we built. Ours was 26, from nothing. */
  --ek-leading-list-title: 26.6px;   /* product set: the footer heading */
  --ek-leading-eyebrow: 28px;        /* content set: eyebrow, card body */
  --ek-leading-label: 22px;

  /* MEASURED, and the two sets disagree: about.duolingo.com's document body
     is w400, duolingo.com's is w500. We follow the document page, since that
     is the kind of page this weight sets. */
  --ek-weight-body: 400;
  --ek-weight-medium: 500;   /* MEASURED: duolingo.com's own body weight */
  --ek-weight-strong: 700;   /* MEASURED: every heading on all five runs */
  /* --ek-weight-display: 800 was here. Deleted: nothing on any run is
     heavier than 700.
     "and no rule referenced it" used to follow, and it was not true:
     /quiz/v2's .quiz-welcome__mark asked for it, so that screen's 32px
     headline computed to 400 until 2026-09-11. Deleting a token does not
     delete the var() that spends it — an undefined custom property is not an
     error, it is a value that silently is not there. device-check 2c is the
     thing that finds these now. */
  /* MEASURED: the home page's 64px headline carries ls -1.28px, and
     -1.28/64 = -0.02em exactly. */
  --ek-tracking-heading: -0.02em;

  /* ── Space ────────────────────────────────────────────────────────────
     OURS — and this is the one place where "use only what is measured" has
     no answer, so it is worth being exact about why.

     Across the nine runs the reference applies about forty distinct spacing
     values: 6, 8, 9.5, 10, 11, 12, 14, 16, 20, 22, 23, 24, 26, 28, 30, 32,
     34, 36, 40, 43, 44, 48, 50, 56, 60, 70, 72, 80, 82, 96, 98, 100, 101,
     130 and more. That is not a scale to copy; it is the absence of one.

     So the 4N scale is ours by construction. What IS evidenced is that it
     covers the reference's common values — 8, 12, 16, 20, 24, 32, 40, 48,
     80 and 96 all appear in its output — and that every rung below is
     applied by our own pages (measured across /duo, /about-ek, /terms-ek
     and the kit at both widths; the least used is 64px at 11 applications).
     Nothing here is dead, and none of it is dressed up as measured. */
  --ek-space-1: 4px;
  --ek-space-2: 8px;
  --ek-space-3: 12px;
  --ek-space-4: 16px;
  --ek-space-5: 20px;
  --ek-space-6: 24px;
  --ek-space-8: 32px;
  --ek-space-10: 40px;
  --ek-space-12: 48px;
  --ek-space-16: 64px;
  --ek-space-20: 80px;
  --ek-space-24: 96px;

  /* MEASURED 15px: duolingo.com @393 lays its content out in a 363px
     container inside a 393px viewport — 30px of gutter, 15 a side.
     Deliberately off the 4-scale; rounding it to 16 to satisfy the scale
     would be tidiness overruling a measurement. */
  --ek-gutter-phone: 15px;

  /* ── Geometry ───────────────────────────────────────────────────────── */
  /* MEASURED 12px on duolingo.com at 1440 and 393 — and it is the ONLY
     radius on that page, on every rounded thing it draws. (/help runs 16
     instead; about and blog run 10-30 on images. The product set is ours.) */
  --ek-radius-control: 12px;
  /* MEASURED 24px on blog.duolingo.com at 1440 and 393. This had no support
     in the five earlier runs, which is why it was flagged; the blog runs
     found it. */
  --ek-radius-card: 24px;
  /* MEASURED 20px at 1440 — about.duolingo.com's card image. It halves to
     10px at 393, redeclared on the component: their picture corners scale
     with the picture, where a control's corner does not. */
  --ek-radius-media: 20px;
  /* MEASURED 16px on duolingo.com/help at 1440 and 393 — the only radius that
     page uses, on the four cards that group its questions. A third rung, and
     the reference earns it: 12 on the product set, 16 on /help, 24 on blog. */
  --ek-radius-panel: 16px;
  /* MEASURED 8px on blog.duolingo.com's post page, eight uses — every tag
     chip under the article, at both widths. The smallest corner the
     reference draws, and the only thing it draws it on. */
  --ek-radius-chip: 8px;
  /* MEASURED 9999px on blog.duolingo.com @1440. */
  --ek-radius-full: 9999px;

  /* MEASURED 50px — duolingo.com's control at both widths, and /help's.
     (about runs 48, blog 49-50.) */
  --ek-control-height: 50px;
  /* OURS. No control on any of the nine runs is 44px tall; the reference's
     bar does not shrink its CTA the way ours does. Ours by decision. */
  --ek-control-height-compact: 44px;
  /* MEASURED 61.73px — the store badges on duolingo.com @1202. */
  --ek-control-height-store: 62px;
  /* MEASURED 48px — the input in duolingo.com's own log-in modal, at 1440 AND
     393 (`div._1Um2j`, both runs). Two less than the 50px control it sits
     above, and not equal to that control's 46px face either: 48 is simply the
     number, and it is not derived from the button. Read the two login probe
     runs before "correcting" it to 50. */
  --ek-field-height: 48px;

  /* ── Layout ─────────────────────────────────────────────────────────── */
  /* MEASURED 988px — the widest container on duolingo.com @1440, and the
     one every section aligns to. (about, help and blog run a ~1070px rail
     instead. They are the reference's other system; ours is built from the
     product pages, so 988 is the one that applies.) */
  --ek-rail: 988px;
  /* MEASURED 330px — duolingo.com's control box at BOTH widths, and a
     container width on the phone run. Also about.duolingo.com's whole
     phone column. The most independently confirmed number here. */
  --ek-content-phone: 330px;
  /* MEASURED 708px, exactly: about.duolingo.com @1440 sets its mission
     statement (the 40/44 line) in a 708px column. An audit in this repo
     called this value "chosen, not measured" — that was wrong, and the
     measurement above is the correction.
     What IS wrong is the ROLE. 708 is the width of a 40px statement, not of
     running text: the reference sets its own running text at 307px (about)
     and 473px (home, 63 characters a line). Ours runs prose at 708 and
     lands at 86 characters, well past the 45-75 that reads comfortably.
     The number is real; using it for prose is the error. */
  --ek-reading: 708px;
  /* MEASURED 832px — the content box of duolingo.com/terms, read off its own
     box model: 832 content + 24px padding a side = the 880px element, inside
     a 10px border and 22px margin. That is the reference's OWN terms page,
     which is exactly the page type this measure is for.
     It was 473px, taken from duolingo.com's home page — but that 473 is a
     short centered marketing paragraph, not a document. Right number, wrong
     page. The blog, the other long-form page, runs ~800, which agrees.
     It is wide: at our 17px this lands near 95 characters a line, past the
     45-75 that reads comfortably. That is what the reference does on this
     page, and following the measurement is the instruction. */
  --ek-reading-prose: 832px;
  /* MEASURED 72px on duolingo.com at 1440 and at 393 — it does not scale.
     (about.duolingo.com runs 72 on a desktop and 82 on a phone, and blog
     116; the product set is the one our bar was built from.) Ours was 70. */
  --ek-nav-height: 72px;
  /* A feature row: a square visual, a text column, and the air between them.
     MEASURED on duolingo.com @1440 — 530 and 473 both appear in that page's
     container widths, and 101 is applied ten times as a row gap. */
  --ek-feature-art: 530px;
  --ek-feature-text: 473px;   /* MEASURED: 473 as a container and as a text
                                  column on duolingo.com @1440. Was 474. */
  --ek-feature-gap: 101px;

  /* The post hero: a headline column, a picture, and the air between them.
     MEASURED on blog.duolingo.com @1440 — the hero's two children are 459
     and 540 wide inside the 1070 rail, which leaves 71 between them. It is
     the feature row's proportions inverted: there the picture is the square
     and the text is narrower; here the picture is the wider of the two. */
  --ek-hero-text: 459px;
  --ek-hero-art: 540px;
  --ek-hero-gap: 71px;
  /* MEASURED 670px on blog.duolingo.com @1440 — the `wrapper small` it puts
     a centered introduction in, 18 elements deep, the second most used
     container on the page after the rail itself. It is not the reading
     measure (832) and not a rail: it is the width a short centered passage
     gets INSIDE a section that is already on one. */
  --ek-narrow: 670px;
  /* OURS. The width of a one-column answer list.

     The quiz's reference is noom.com, not duolingo.com — a different site from
     the rest of this file, and one nothing here has probed yet. Noom fits its
     whole question inside about 400, and 400 is what this is.

     Checked against the content rather than taken on trust: of the 103 real
     option labels in quiz/v1/steps.json, exactly ONE needs a second line at
     400 ("Enjoy K-dramas & K-pop without subtitles", 40 characters). The
     one-line budget at this width is 40 characters with an emoji and 45
     without — a number worth knowing when writing options, so it is written
     down here.

     It was 560 for one commit, from a derivation that was wrong: the "widest
     label needs 417" behind it measured the CARD with nowrap — padding, border
     and emoji slot included — not the text. The text needs 324. */
  --ek-choice-width: 400px;
  /* MEASURED 62px — an option row on
     duolingo.com/welcome?welcomeStep=learningReason at 393 (`div._8dMUn`,
     361x62). It is a min-height, not a height: an option with no emoji has to
     stand as tall as one with, or the list changes rhythm between question
     types. 62 breaks down as 8 + 40 + 8 of box, plus the 2px top border and
     the 4px bottom ledge.

     It is also the PHONE value. The same row read at ~1170 is 363x77, with a
     360x71 inner — the reference grows its option on a wide screen because the
     icon in it grows. The wide value is set in the Choices block, on the same
     breakpoint as the second column, because where the reference switches was
     not measured and tying it to the column count is at least a reason.

     Read by hand in DevTools, not off a probe run — the probe reports 0 boxes
     on this page at both widths, twice, so no height on this screen can come
     from the JSON. Noted so nobody goes looking for it there. */
  --ek-choice-height: 62px;

  /* Breakpoints cannot be custom properties — a media query never sees a
     var() — so these are documentation, and the queries repeat the number.
     /duo currently breaks at 600, 700, 900, 1000, 1040 and 1140px, which is
     six breakpoints for one page and is the loudest thing this extraction
     found. Components are meant to land on the two below; anything else has
     to earn its place in review.
       --ek-bp-stack:  900px   two columns become one
       --ek-bp-phone:  600px   phone gutter, phone container, compact nav */

  /* ── Motion ─────────────────────────────────────────────────────────── */
  /* OURS, not measured. The reference's controls carry no transition at all
     (transition: all 0s on every run), so the press animation is a decision
     Hoon asked for and 80ms is a number we picked. Said plainly rather than
     dressed up as a measurement. */
  --ek-duration-press: 80ms;
  /* MEASURED 200ms — the only transition on about.duolingo.com (10 uses)
     and on /help. Ours was 150ms, which appears nowhere. */
  --ek-duration-short: 200ms;
  /* MEASURED 300ms on duolingo.com and on blog. 250ms appears nowhere. */
  --ek-duration-medium: 300ms;
  /* OURS. The reference sets no easing on these — `ease` is simply the CSS
     default showing through, so adopting it would be adopting an absence.
     duolingo.com does use cubic-bezier(0.22, 1, 0.36, 1) on one element. */
  --ek-easing: cubic-bezier(0.4, 0, 0.2, 1);

  /* ── Shell ───────────────────────────────────────────────────
     OURS, every one of them, and unusually so: there is no measurement
     behind this group and there cannot be one from the probes we have.

     The reference set is nine web pages plus, now, its lesson screen — and
     the lesson is deliberately chrome-stripped. Its probe reports
     "topBar: MISSING — nothing fixed/sticky at the top", and there is no tab
     bar, no sidebar and no modal in it either. So the shell is the one part
     of this system with nothing to read off, and every number below is a
     decision. See EK.md § The app shape.

     The starting point is the classroom as it stands, because a shell the
     student already navigates is worth more than a tidier one they have to
     relearn. That is a reason to start there, not to stop there: where its
     value looks arbitrary it is changed here and the change is written down. */

  /* The classroom's sidebar is 280 today. Kept: it holds the longest nav
     label we have at the label rung without wrapping, and 280 is on the 4N
     scale (70 × 4) where 260 and 300 are equally arbitrary. */
  --ek-shell-rail: 280px;
  /* 56, matching --ek-appbar-height below rather than the classroom's 52.
     A 4px difference between the top and bottom edge of the same screen is
     not a hierarchy, it is a value nobody chose; the two chrome edges are
     one weight now. 56 also clears the 44px minimum touch target with 6 a
     side. This moves the shipped bar by 4px. */
  --ek-tabbar-height: 56px;
  /* 56. This one is assets/everyday.css's --everyday-app-bar-height, which
     the classroom has shipped for its whole life, and nothing about it
     looks arbitrary: it is the smallest bar that holds a 40px round back
     button with 8 above and below. */
  --ek-appbar-height: 56px;
  /* 520. Reasoned from this system's own form rather than carried over from
     the classroom's 520, which happens to agree: the form shape is 375 wide
     inside 30 of screen padding (EK.md § The form shape), so a modal that
     can hold one needs 435 plus air. 520 leaves 42 a side. */
  --ek-modal-width: 520px;
  /* 1.5s. A shimmer that is slower reads as a stalled page and faster reads
     as a warning. Nothing supports the number; it is the classroom's, it has
     not been complained about, and re-picking it would be motion for its
     own sake. */
  --ek-duration-shimmer: 1500ms;

  /* The switch. 36 x 20, with a 16 thumb inset 2 — the classroom's shipped
     values, kept for the same reason the shell's are: a control a student
     already flicks is worth more than a tidier one. No probe covers it; the
     reference has no switch on any of its eleven runs.
     Two tokens rather than one because the width is not derivable from the
     height without inventing a ratio, and the thumb IS derivable: it is the
     height less twice the inset, which is why it has no token. */
  --ek-switch-width: 36px;
  --ek-switch-height: 20px;
  --ek-switch-inset: 2px;

  /* An icon button IS its icon, and the box is this. 18 was the first answer —
     Hoon's number, and the list row's chevron has been 18 since the settings
     screen — but an 18 icon beside 17px text and a 36 avatar read as small
     rather than as quiet, on the like, the comment and the photo attach alike.
     20 is the second answer and it is one number for all of them.

     The tap pad is added as padding and taken straight back as an equal
     negative margin, so the element occupies 20 of layout and accepts a press
     across 32. Still under the 44 both platforms ask for; 44 cannot be had
     without either a box that misaligns or a tap area that reaches past the
     icon next door, and this is the most that fits between them. */
  --ek-icon: 20px;
  --ek-tap-pad: 6px;

  /* The drawn X, corner to corner — that much of X rather than a box holding a
     smaller one, which is what the sprite was giving. */
  --ek-close-x: var(--ek-icon);

  /* ── Root ───────────────────────────────────────────────────────────── */
  /* These five are what a page inherits, and they are the ones an extraction
     is most likely to get wrong — nothing in /duo declares them, so they are
     invisible unless you measure the rendered document instead of reading
     its stylesheet. Measured, not assumed: 18px / 400 / 1.5 / #212124. */
  color: var(--ek-ink);
  background: var(--ek-surface);
  font-family: var(--ek-font);
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-body);
  line-height: var(--ek-leading-body);
  -webkit-font-smoothing: antialiased;
}

/* The reset. box-sizing alone is not one: without the margin and padding
   rules a page keeps the body's default 8px margin and the browser's own
   spacing on headings, paragraphs and lists, which moves everything down and
   to the right and narrows the viewport by 16. The old system reset all
   three, so a page migrating here lands 8px off in every direction if this
   only does the third. */
.ek-root { margin: 0; }
.ek-root *,
.ek-root *::before,
.ek-root *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* The browser hides [hidden] with a UA rule, and ANY author rule that sets
   display beats it — .ek-control is inline-flex, .ek-post is flex, so both
   went on rendering while their hidden property read true. That is a bug that
   looks like nothing in the DOM and everything on screen, and it costs a page
   its "load more" the moment one is built on the attribute.

   !important is right here and only here: this has to outrank every component
   in the file, and the alternative is remembering to write display:none into
   each of them. */
.ek-root [hidden] { display: none !important; }
/* And when the element IS the root. A self-matching component — .ek-modal, and
   now .ek-iconbtn / .ek-close — has no .ek-root above it for the rule above to
   descend from, and its own display (flex, inline-flex) outranks the UA's
   [hidden]. The classroom had already hit this and patched it per element
   (`#setup-wizard-back[hidden] { display: none }`); one rule here means the
   next self-rooted component does not have to. */
.ek-root[hidden] { display: none !important; }

/* Nothing is redeclared for the phone any more. This block held
   --ek-type-control: 15px, which the base now is: measured at 15 on
   duolingo.com at 1440 AND 393. The reference's control label does not
   scale, so neither does ours.
   (The tokens that genuinely do change with width are the three clamp()s,
   which carry their own two measured endpoints.) */

/* THE OPT-OUT IS LOAD-BEARING, and it was missing. This rule is right for
   decorative motion and wrong for motion that CARRIES STATE: a loading
   spinner reduced to 0.01ms and one iteration is not a calmer spinner, it is
   a stopped one, and a stopped spinner says "this has hung" — on a screen
   that has taken the page over while a card is being charged, that is the
   worst thing it could say.

   It had exactly that effect twice: the quiz's "Analyzing your profile"
   spinner and the payment overlay's both looked frozen to anyone with Reduce
   Motion on, and per-page attempts to slow them instead of stopping them
   could not win against `!important`.

   WCAG 2.3.3 is about non-essential animation; feedback that something is
   still working is essential. So an element can say so, and only then:
   data-motion="essential" is an explicit claim, made per element, not a
   blanket exception. Use it for progress and loading indicators. Do not use
   it to keep a decorative animation. */
@media (prefers-reduced-motion: reduce) {
  .ek-root *:not([data-motion='essential']),
  .ek-root *::before,
  .ek-root *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}


/* ==========================================================================
   Components
   Every one of these is rendered in /ek/kit.html. If you add one here
   without adding it there, it is not in the system.
   ========================================================================== */

/* ── Control ────────────────────────────────────────────────────────────
   The depth is a solid lower face and it lives INSIDE the control's box: a
   50px button occupies 50px, face plus ledge, so a height spec is not a lie
   and nothing below it needs a compensating margin.

   The reference builds this with a ::before and a box-shadow. A bottom
   border reaches the same picture with one element and no stacking context,
   and it keeps the ledge inside the border-box for free.

   Press: the whole face travels down by its own depth and the ledge goes
   transparent, so the control lands where its ledge was. The face keeps its
   size — an earlier version collapsed the border instead, which grew the
   face downward from a fixed top edge and read as a stretch rather than a
   press. Measured off the reference: face 0..46 with a 46..50 ledge at rest,
   face 4..50 pressed.

   background-clip is what makes this work, and it has to be declared after
   any `background` shorthand: the shorthand resets clip to border-box, and
   the face then paints under the reserved strip and grows instead of moving.
   Every rule below therefore sets background-color, never background.

   Instant down, eased back up — a delayed press reads as lag, a delayed
   release reads as spring.
   ---------------------------------------------------------------------- */
.ek-root .ek-control {
  --ek-control-depth: var(--ek-depth);
  --ek-control-face: var(--ek-brand);
  --ek-control-face-hover: var(--ek-brand-pressed);
  --ek-control-ledge: var(--ek-ledge-brand);
  --ek-control-frame: 0 solid transparent;

  position: relative;
  isolation: isolate;
  display: inline-flex;
  height: var(--ek-control-height);
  align-items: center;
  justify-content: center;
  /* The reserved strip is padding, not a border, so the label centers in the
     face rather than in the face plus its ledge. */
  padding: 0 var(--ek-space-6) var(--ek-control-depth);
  color: var(--ek-ink-on-solid);
  background: transparent;
  border: 0;
  border-radius: var(--ek-radius-control);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--ek-type-control);
  font-weight: var(--ek-weight-strong);
  line-height: 1;
  /* MEASURED 0.8px, on both of the reference's systems and at every width:
     "Get started" on duolingo.com reads 15/normal w700 ls0.8px at 1440, 1202
     and 393, and "Send Feedback" on /help reads 15/18 w700 ls0.8px at 1440
     and 393. Four readings on two pages, no counter-example. Ours set none.
     (The uppercase those labels also carry is the one thing here we do not
     copy — Korean has no uppercase, so 시작하기 beside START TODAY would read
     as two systems. That is a decision; this is a measurement.) */
  letter-spacing: 0.8px;
  text-align: center;
  text-decoration: none;
  transition: transform var(--ek-duration-press) var(--ek-easing);
  -webkit-tap-highlight-color: transparent;
}

/* The ledge is its own box and it never changes — not its size, not its
   color, not its opacity. An earlier version faded it out under the moving
   face, which reads as a shadow being switched off rather than a button being
   pressed. It is painted once, and the face slides over it. */
.ek-root .ek-control::before {
  content: '';
  position: absolute;
  z-index: -2;
  /* The face's own shape, offset down by the depth — which is what a
     box-shadow would have drawn. A full-width strip instead leaves square
     top corners poking out either side of the face's curve. */
  inset: var(--ek-control-depth) 0 0;
  background: var(--ek-control-ledge);
  border-radius: var(--ek-radius-control);
  transition: transform var(--ek-duration-press) var(--ek-easing);
}

/* The face. The only thing that moves. */
.ek-root .ek-control::after {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0 0 var(--ek-control-depth);
  background: var(--ek-control-face);
  border: var(--ek-control-frame);
  border-radius: var(--ek-radius-control);
  transition: background var(--ek-duration-short) var(--ek-easing);
}

/* Color only on hover. A lift on hover plus a sink on press is two motions
   fighting over the same control. */
@media (hover: hover) {
.ek-root .ek-control:hover::after { background: var(--ek-control-face-hover); }
}

/* The control travels its own depth; the ledge is pushed back by the same
   amount, so it stays exactly where it was while everything else moves over
   it. Nothing appears, nothing disappears.

   TWO SELECTORS, ONE PRESS, and the pair is explained once here for all
   three pressables below. `:active` is the press everywhere by default. It
   cannot be the press on a touch device, for two reasons that arrive
   together: iOS does not set it on a tap at all unless the document happens
   to be listening for touches, and once it IS set WKWebView does not clear
   it when the finger wanders off — a UA state no script can reset. So a page
   that wants a reliable press loads `assets/ek-press.js`, which stamps
   `ek-press-js` on <html> and drives `.ek-pressed` itself.

   The guard is on the `:active` branch rather than a later rule undoing it,
   because undoing means restating every resting value the press overrode —
   and then a new pressable has to be added to two lists that can drift. Here
   there is one list. A page without the script matches `html:not(...)` and
   behaves exactly as it always has, which is what keeps the classroom and
   every unconverted page out of this. */
html:not(.ek-press-js) .ek-root .ek-control:active,
.ek-root .ek-control.ek-pressed { transform: translateY(var(--ek-control-depth)); }
html:not(.ek-press-js) .ek-root .ek-control:active::before,
.ek-root .ek-control.ek-pressed::before {
  transform: translateY(calc(-1 * var(--ek-control-depth)));
}

.ek-root .ek-control:focus-visible {
  outline: var(--ek-focus-ring);
  /* One more than the default: the control's ledge is part of its silhouette,
     and a 2px ring sat on top of it. */
  outline-offset: 3px;
}

/* Secondary runs the same machine on the shallower depth, with the outline on
   the face. Its foot reads 4px at rest — the face's own 2px bottom border
   sitting on the 2px ledge, same color, adjacent — and 2px once the face has
   traveled onto it.

     rest     line 0..2   face 2..46   line 46..50
     pressed  line 2..4   face 4..48   line 48..50  */
.ek-root .ek-control--secondary {
  --ek-control-depth: var(--ek-depth-subtle);
  --ek-control-face: var(--ek-surface);
  --ek-control-face-hover: var(--ek-surface-sunken);
  --ek-control-ledge: var(--ek-stroke);
  --ek-control-frame: var(--ek-stroke-control) solid var(--ek-stroke);
  color: var(--ek-brand);
}

/* Done. The classroom's "Mark as Complete" turns green once a lesson is
   finished, and that is a real second state rather than a decoration: it is
   how a student reads their own progress off a lesson they have reopened.
   It runs the same machine on --ek-positive, and its ledge is mixed toward
   black by the same amount the brand ledge measures — the reference picks
   each ledge by hand, so a formula here is ours and says so. */
.ek-root .ek-control--done {
  /* Done is one look whatever the control was before it: a --secondary that
     finishes (the lesson's quiz and mission) kept its grey frame and its
     subtle depth, because this rule never took them back. */
  --ek-control-depth: var(--ek-depth);
  --ek-control-frame: 0 solid transparent;
  --ek-control-face: var(--ek-positive);
  --ek-control-face-hover: var(--ek-positive);
  --ek-control-ledge: rgb(0, 114, 63);
  --ek-control-ledge: color-mix(in srgb, var(--ek-positive) 82%, black);
  color: var(--ek-ink-on-solid);
}

/* On the dark ground a neutral ledge disappears; this one is mixed toward the
   ground so it still reads as depth. */
.ek-root .ek-control--on-deep {
  --ek-control-face: var(--ek-surface);
  --ek-control-face-hover: var(--ek-surface-sunken);
  --ek-control-ledge: var(--ek-ledge-on-deep);
  color: var(--ek-ink-marketing);
}

.ek-root .ek-control--block { display: flex; width: 100%; }

/* A DISABLED CONTROL IS ALREADY PRESSED, and enabling it is what raises it.
   Hoon's call, 2026-09-10. At rest the depth sits under the face, so a
   disabled button carried a 4px foot it could not use and its label read 2px
   high of centre; worse, the moment it enabled, nothing moved. Now the button
   travels its own depth exactly as :active does — same two rules — so it wakes
   up by rising, and its label is centred in the face either way.

   The box does not change size: this is a transform, and the 4px it vacates at
   the top stays reserved, so nothing around it reflows when the button wakes. */
.ek-root .ek-control:disabled,
.ek-root .ek-control[aria-disabled='true'] {
  --ek-control-face: var(--ek-stroke);
  --ek-control-face-hover: var(--ek-stroke);
  --ek-control-ledge: var(--ek-stroke);
  color: var(--ek-ink-subtle);
  /* Not `not-allowed`: a disabled control is usually waiting on the field
     beside it, which is a thing to do rather than a refusal. */
  cursor: default;
  transform: translateY(var(--ek-control-depth));
}
.ek-root .ek-control:disabled::before,
.ek-root .ek-control[aria-disabled='true']::before {
  transform: translateY(calc(-1 * var(--ek-control-depth)));
}

/* ── Form ───────────────────────────────────────────────────────────────
   The seventh shape's components, and the first ek has that a visitor types
   into. They sit here, next to Control, because a field and a button are
   read together in a form.

   MEASURED, off duolingo.com/?isLoggingIn=true at 1440 and 393 — the log-in
   modal, open. (An earlier version of this block said the reference had no
   comparable element and the numbers were ours. That was wrong: the modal
   is measurable, it was measured, and the readings are in
   scripts/probe/measurements/duolingo.com-login-*.json.)

     field           48 tall, 12 corner, 2px rgb(229,229,229), fill rgb(247,247,247)
     between fields  8
     field to note   4
     fields to CTA   20
     form width      375 @1440, 333 @393 — the modal's 30px padding either side
     headline        26/40 w700, 15 above the form

   Two places we knowingly do NOT follow it, both recorded in EK.md:

   - **their form controls take a 16px corner** where the same button on the
     marketing page takes 12. The reference disagrees with itself; 12 has
     nine uses across the page and 16 has three inside one modal, so ek's
     control keeps 12 everywhere rather than changing shape by context.
   - **their form has no labels at all**, only placeholders. A placeholder is
     gone the moment you type and is not reliably announced, so ek labels its
     fields. Same kind of departure as sentence case: stated, not silent.
   ---------------------------------------------------------------------- */

/* The stack. A form is a column of fields at one rhythm, and the rhythm is
   the field gap — not a margin each field carries, which is the version that
   collides with whatever sits beside it. */
/* Spacing is set as margins between siblings, not as a flex `gap`, so that
   every measured number appears literally in one place. With a gap the two
   rules below would ADD — the action would sit 28 from the last field rather
   than the 20 that was measured, which is exactly the bug this shape shipped
   with for one commit. */
.ek-root .ek-form {
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* MEASURED 8 between fields: the reference stacks two 52px field groups with
   8px margins inside a 120px block, at both widths. */
.ek-root .ek-form > * + * { margin-top: var(--ek-space-2); }

/* MEASURED 20 from the last field to the action — `marginTop` on the modal's
   LOG IN button, both widths. A wider gap than the one between fields, which
   is the reference saying what we would: the action is a different kind of
   thing from the questions above it. Wins on specificity over the rule above,
   so it replaces the 8 rather than adding to it.

   `* +` is load-bearing: this is the gap between the fields and the action, so
   it only exists when there IS something above the action. A form whose only
   child is its button (a confirm step, "email me a setup link") has nothing to
   be 20 away from, and without the combinator it took the 20 anyway and
   stacked it on whatever the page had already put above the form. */
.ek-root .ek-form > * + .ek-control { margin-top: var(--ek-space-5); }

.ek-root .ek-field { display: flex; flex-direction: column; }

/* OURS. The reference has no labels to measure a gap under — it runs on
   placeholders — so this is the label rung's own spacing. */
.ek-root .ek-field__label + * { margin-top: var(--ek-space-2); }

/* MEASURED 4 — the gap inside the reference's field group, between the input
   and the slot under it. */
.ek-root .ek-field > .ek-note { margin-top: var(--ek-space-1); }

/* Sentence case, like every other label in this system. 15/22 w700 is the
   label rung the footer heading and the text link already run at. */
.ek-root .ek-field__label {
  color: var(--ek-ink);
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-label);
}

/* The control box exists so something can sit inside the field's right edge
   (a password reveal, a spinner) without the input's padding having to know
   about it. With nothing in it, it is the input. */
.ek-root .ek-field__control { position: relative; display: flex; }

/* The field, MEASURED whole off the reference's log-in modal: 48 tall, a 12px
   corner, a 2px stroke, and a FILL. The fill is the part that is easy to get
   wrong from memory — the reference's input is not a white box with a line
   round it, it is a sunken gray panel (rgb(247,247,247)) that also carries the
   line (rgb(229,229,229)). ek's own --ek-surface-sunken and --ek-stroke land
   within a couple of values of both, so the treatment is theirs and the
   colors stay ours, which is the whole arrangement in this file.

   It takes no depth. The control's ledge says "this moves when you press it";
   an input does not move, and the reference agrees — its field has an even
   2px border on all four sides where its buttons carry 4 on the bottom.

   17px is --ek-type-body and it is load-bearing, not cosmetic: iOS Safari
   zooms the viewport on focus for any font-size under 16px, which on a phone
   throws the whole page sideways mid-sign-in. The body rung clears it with
   room to spare, so this cannot be "tidied" down to the label rung. */
.ek-root .ek-input {
  width: 100%;
  height: var(--ek-field-height);
  padding: 0 var(--ek-space-4);
  color: var(--ek-ink);
  background-color: var(--ek-surface-sunken);
  border: var(--ek-stroke-control) solid var(--ek-stroke);
  border-radius: var(--ek-radius-control);
  font-family: inherit;
  font-size: var(--ek-type-body);
  line-height: 1;
  transition: border-color var(--ek-duration-short) var(--ek-easing),
              box-shadow var(--ek-duration-short) var(--ek-easing);
  -webkit-appearance: none;
  appearance: none;
}

.ek-root .ek-input::placeholder { color: var(--ek-ink-subtle); }

/* Focus is the input's main state, not an accessibility afterthought, so it is
   on :focus rather than :focus-visible — a field reached by tap should look
   focused too.

   ONE THING CHANGES, AND IT IS THE LINE. An earlier version also lifted the
   face to white and added a 3px ring, so focusing swapped the fill, drew a
   second edge outside the first and appeared to grow the field — three changes
   reading as a lurch. The reference does exactly one: its filled grey field
   keeps its fill and its border goes blue. The caret goes brand with it, which
   is the other half of "you are typing here" and costs no geometry.

   The ring is gone rather than moved to :focus-visible: a text field matches
   :focus-visible on a mouse click too, so that put the halo straight back. A
   2px border changing colour on the element's own edge is the indicator. */
.ek-root .ek-input:focus {
  border-color: var(--ek-brand);
  caret-color: var(--ek-brand);
  outline: none;
}

/* Invalid is the stroke, and the message under the field carries the words.
   Color alone is not a message. --ek-accent is the critical-only red, which
   is what this is. */
.ek-root .ek-input[aria-invalid='true'] { border-color: var(--ek-accent); }
.ek-root .ek-input[aria-invalid='true']:focus {
  box-shadow: 0 0 0 3px rgb(247, 196, 200);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ek-accent) 30%, white);
}

/* Disabled gives up the FILL, not the border. Now that a filled box is what
   says "you can type here", an inert field is the one that is flat — and it
   keeps its stroke, so it still reads as a field rather than as a line of
   text. (Dropping the border instead was tried and does the opposite: the
   box disappears and an empty disabled field is indistinguishable from a
   caption.) No confusion with :focus, which is white plus a brand-colored
   a brand border. */
.ek-root .ek-input:disabled {
  background-color: var(--ek-surface);
  color: var(--ek-ink-subtle);
  cursor: not-allowed;
}

/* The reveal. 44px square inside the field's right edge — 44 because that is
   the smallest thing a thumb hits reliably, and it still fits inside the 48px
   field without changing its height. The input reserves the space with its own
   padding; the button never overlaps text.

   The reference puts a "Forgot?" text link in exactly this slot (15/28 w700
   ls0.8px, ink rgb(175,175,175)), so an affix inside the field's right edge is
   its pattern too, not an invention. */
.ek-root .ek-field__reveal {
  position: absolute;
  top: 50%;
  right: var(--ek-space-1);
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--ek-ink-subtle);
  background: none;
  border: 0;
  border-radius: var(--ek-radius-chip);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
.ek-root .ek-field__reveal:hover { color: var(--ek-ink); }
}
.ek-root .ek-field__reveal:focus-visible {
  outline: var(--ek-focus-ring);
  /* Tighter than the default: this one sits INSIDE the field it belongs to,
     so a 2px ring would touch the field's own border. */
  outline-offset: 1px;
}
/* The input gives up its right padding to the reveal, so a long password
   scrolls under nothing. */
.ek-root .ek-field__control:has(.ek-field__reveal) .ek-input {
  padding-right: calc(44px + var(--ek-space-2));
}

/* A line under a field or under the form. Muted by default; critical when it
   is telling someone what went wrong.

   It is a separate component rather than .ek-body--muted because a form
   message is announced (role="status" / role="alert") and it collapses when
   empty — an .ek-body with no text still occupies its line height, which
   makes the form jump every time an error clears. */
.ek-root .ek-note {
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
}
.ek-root .ek-note:empty { display: none; }
.ek-root .ek-note--critical { color: var(--ek-accent); }
.ek-root .ek-note a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

/* ── Type ─────────────────────────────────────────────────────────────── */
.ek-root .ek-band,
.ek-root .ek-section-head,
.ek-root .ek-page-head {
  color: var(--ek-ink);
  font-weight: var(--ek-weight-strong);
  /* MEASURED lsnormal, and it took three pages to see it. The tracking that
     used to sit here applied to all three rungs; the reference sets it on ONE
     element in nine runs. Its 64/36 band headline carries -1.28px / -0.72px,
     both -0.02em. Everything else is lsnormal:

       48 and 36  "free. fun. effective."       @1440, @1202, @393
       40         about's mission statement     @1440
       37.5       blog's hero and section head  @1440
       32         the home h1, /help's headline, /terms' headline
                                                 three pages, both widths

     That is seven independent readings against one. The band keeps it below;
     the other two rungs never had it. Same error as .ek-statement carried
     until it was caught: a real measurement applied to the wrong element,
     inherited by a shared rule rather than chosen. */
  letter-spacing: normal;
  text-wrap: balance;
}
.ek-root .ek-band         { font-size: var(--ek-type-band);
                            line-height: var(--ek-leading-heading);
                            /* MEASURED -1.28px on 64 and -0.72px on 36, both
                               -0.02em — duolingo.com's band headline, and the
                               only tracked headline on any run. */
                            letter-spacing: var(--ek-tracking-heading); }
.ek-root .ek-section-head { font-size: var(--ek-type-section); line-height: var(--ek-leading-heading); }
/* MEASURED 32/40 on duolingo.com/help ("Frequently Asked Questions") — a
   ratio of exactly 1.25, which is --ek-leading-heading. It was on
   --ek-leading-tight (1.15), a number nothing supported. */
.ek-root .ek-page-head    { font-size: var(--ek-type-page);    line-height: var(--ek-leading-heading); }
.ek-root .ek-body         { font-size: var(--ek-type-body);    line-height: var(--ek-leading-body); }
.ek-root .ek-body--tight  { line-height: var(--ek-leading-body-tight); }
.ek-root .ek-body--muted  { color: var(--ek-ink-muted); }
.ek-root .ek-caption      { font-size: var(--ek-type-caption);
                            line-height: var(--ek-leading-caption);
                            color: var(--ek-ink-muted); }

/* ── Layout ───────────────────────────────────────────────────────────────
   The nav bar is on the rail too, and it has to be on it by the same rules,
   not by a copy of them. It carried its own max-width and gutter, which meant
   it never got the cap's gutter added back above 600 and used a 20px gutter
   on a phone where the rail uses 15 — so the logo sat 20px inside the column
   every section aligns to, and 5px outside it on a phone. Measured on
   /about-ek at 1440: brand at x=246 against content at x=226. */
.ek-root .ek-rail,
.ek-root .ek-nav__inner {
  width: 100%;
  max-width: var(--ek-rail);
  margin-inline: auto;
  /* The gutter is always there. It used to appear only under 600, which left
     every width between the phone and the rail with content hard against the
     edge of the screen — 988 of content in a 900 window. */
  padding-inline: var(--ek-space-5);
}
.ek-root .ek-surface-deep {
  color: var(--ek-ink-on-solid);
  background: var(--ek-deep);
}
.ek-root .ek-surface-deep :is(.ek-band, .ek-section-head, .ek-page-head) {
  color: var(--ek-ink-on-solid);
}

@media (max-width: 600px) {
  .ek-root .ek-rail,
  .ek-root .ek-nav__inner { padding-inline: var(--ek-gutter-phone); }
}

/* The rail's max-width caps its content box, so the gutter has to be added
   back or a page loses 40px of its rail at exactly the width where the rail
   starts to matter. */
@media (min-width: 601px) {
  .ek-root .ek-rail,
  .ek-root .ek-nav__inner { max-width: calc(var(--ek-rail) + 2 * var(--ek-space-5)); }
}

/* ── The content rail ───────────────────────────────────────────────────
   MEASURED 1070px on about.duolingo.com, duolingo.com/help and
   blog.duolingo.com alike, against the 988 duolingo.com's home page uses.
   The reference runs two rails, one for the product pages and a wider one
   for everything written; so do we.

   It is a token override on the page's root, not a modifier on every rail —
   so the bar, the footer and every section on that page move together, and a
   page is one rail or the other rather than a mix. Landing pages take the
   988 default; a document page narrows further still, below. */
.ek-root--content { --ek-rail: 1070px; }

/* A page that is one column of text — terms, a policy, a post — has the
   reading measure as its rail, not the 988 one. Without this the column sits
   left-aligned inside a rail almost 300px wider than it, so the whole page
   reads as shifted off center even though the rail itself is centered.

   Same two rules as the rail above, and they have to come after it: both
   selectors are (0,2,0), so source order is what decides.

   It caps at the PROSE measure — 832px, the content box of the reference's
   own terms page. */
.ek-root .ek-rail--reading { max-width: var(--ek-reading-prose); }
@media (min-width: 601px) {
  .ek-root .ek-rail--reading { max-width: calc(var(--ek-reading-prose) + 2 * var(--ek-space-5)); }
}

/* Narrower still, and for a different reason: not a page that IS one column,
   but a short centered passage INSIDE a section already on the rail — the
   sentence under a section heading, the paragraph in front of a single
   control. Measured at 670 on the reference's blog.

   So it is not a --modifier of the rail, and deliberately not named like one:
   it sits inside a rail, which has already paid the gutter, and a second
   padding-inline here would inset it again — 30px a side on a phone against
   the 15 everything around it gets. It centers and it caps. That is all.
   Nothing to add back above 600 either, for the same reason. */
.ek-root .ek-narrow {
  width: 100%;
  max-width: var(--ek-narrow);
  margin-inline: auto;
}

/* ── Nav ────────────────────────────────────────────────────────────────
   Sticky, and it gets out of the way. --compact is the scrolled state: a
   rule under the bar and the CTA revealed. --hidden is for the moment a
   full-viewport band owns the screen and a bar over it would be noise.

   Sticky resolves against the nearest scrolling ancestor, so this has to sit
   outside any wrapper that establishes one — a lesson /duo learned the hard
   way when the bar stopped at the end of the fold.
   ---------------------------------------------------------------------- */
.ek-root .ek-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--ek-surface);
  transition: transform var(--ek-duration-medium) var(--ek-easing),
              opacity var(--ek-duration-medium) var(--ek-easing);
}
/* Width, gutter and centering come from the rail rules above, so the bar and
   the page cannot drift apart. Only what is the bar's own is set here. */
.ek-root .ek-nav__inner {
  height: var(--ek-nav-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ek-space-5);
}
/* The link has to be a flex box, not an inline one: inline leaves the image
   sitting on a baseline with the line-height's descender under it, which
   pushes the mark a few pixels below the bar's true center. */
.ek-root .ek-nav__brand { display: flex; align-items: center; }
.ek-root .ek-nav__brand img { display: block; width: 42px; height: 42px; }
/* --compact used to draw a hairline under the bar as well as revealing the
   CTA. The rule is gone: neither reference page we have measured draws one —
   not blog.duolingo.com, not duolingo.com/help — and Hoon asked for it off.
   The bar is still sticky and still white, so content passes under it rather
   than being cut by a line. Putting it back is one declaration here. */
.ek-root .ek-nav--hidden {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}
/* The nav's CTA is hidden until the page has scrolled past the hero's own. */
.ek-root .ek-nav .ek-control { display: none; }
.ek-root .ek-nav--compact .ek-control {
  display: inline-flex;
  height: var(--ek-control-height-compact);
  padding-inline: var(--ek-space-5);
}
/* The text nav. Quiet until hovered or current — a bar of brand-colored
   links competes with the thing the page is actually asking for. */
.ek-root .ek-nav__links {
  display: flex;
  align-items: center;
  gap: var(--ek-space-6);
}
.ek-root .ek-nav__links a {
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-nav-height);
  text-decoration: none;
  border-bottom: 3px solid transparent;
}
@media (hover: hover) {
.ek-root .ek-nav__links a:hover { color: var(--ek-ink); }
}
.ek-root .ek-nav__links a[aria-current] {
  color: var(--ek-brand);
  border-bottom-color: var(--ek-brand);
}
/* A bar WITH links centres them on the page rather than in the space left
   over between the mark and the CTA. Under space-between they sat wherever
   those two happened to leave room — and the CTA appears only once the page
   has scrolled, so the links visibly slid sideways as you scrolled past the
   hero. Three columns with equal outer tracks put the middle one on the
   page's centre line whatever is beside it, and the columns are assigned by
   hand so a hidden CTA or hidden links cannot pull anything into the wrong
   one. Bars without links keep the flex row above. */
.ek-root .ek-nav__inner:has(.ek-nav__links) {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
}
.ek-root .ek-nav__inner:has(.ek-nav__links) > .ek-nav__brand {
  grid-column: 1;
  justify-self: start;
}
.ek-root .ek-nav__inner:has(.ek-nav__links) > .ek-nav__links {
  grid-column: 2;
  justify-self: center;
}
.ek-root .ek-nav__inner:has(.ek-nav__links) > .ek-control {
  grid-column: 3;
  justify-self: end;
}

@media (max-width: 900px) { .ek-root .ek-nav__links { display: none; } }

/* On a phone the bar holds only the mark until the CTA arrives, and a lone
   mark shoved against the left edge reads as a mistake. It sits centered until
   there is a second thing for space-between to balance it against. */
@media (max-width: 600px) {
  .ek-root .ek-nav__inner { justify-content: center; }
  .ek-root .ek-nav--compact .ek-nav__inner { justify-content: space-between; }
}

/* ── Feature row ────────────────────────────────────────────────────────
   A square visual and a text column on the rail, alternating down the page.
   Stacks to one column at 900, visual first, so the reading order is the
   same on every row rather than following the desktop alternation.
   ---------------------------------------------------------------------- */
.ek-root .ek-feature {
  max-width: var(--ek-rail);
  min-height: var(--ek-feature-art);
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: var(--ek-feature-gap);
}
.ek-root .ek-feature--reverse { flex-direction: row-reverse; }
.ek-root .ek-feature__text { flex: 0 1 var(--ek-feature-text); min-width: 0; }
.ek-root .ek-feature__art {
  position: relative;
  flex: 0 1 var(--ek-feature-art);
  /* The circle has to stay a circle while it gives, so the height follows the
     width rather than being pinned to the same token. */
  aspect-ratio: 1;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--ek-radius-full);
  background: var(--ek-tint);
}
/* The art slot is a grid track, so a percentage inside it resolves against
   the image's intrinsic width, not the slot. Pin it instead. */
.ek-root .ek-feature__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ek-root .ek-feature + .ek-feature { margin-top: var(--ek-space-24); }

/* ── Media tile ─────────────────────────────────────────────────────────
   A square that crops whatever is put in it. One radius and one clip, on
   the tile itself — a poster and a player that each carry their own produce
   two antialiased corners that disagree by a subpixel, which reads as a
   hairline along the curve.
   ---------------------------------------------------------------------- */
.ek-root .ek-media {
  position: relative;
  width: min(423px, 100%);
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--ek-radius-card);
  background: var(--ek-ink-marketing);
}
.ek-root .ek-media > img,
.ek-root .ek-media > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  border-radius: 0;
}

/* ── Eyebrow ────────────────────────────────────────────────────────────
   The small label above a heading that says what kind of thing follows. It
   is sentence case like every other label here — the reference shouts, and
   we cannot, because Korean has no uppercase to shout with.
   ---------------------------------------------------------------------- */
.ek-root .ek-eyebrow {
  display: block;
  color: var(--ek-brand);
  /* MEASURED 19/28 w700 ls0.5px — about.duolingo.com's "Our mission",
     identical at 1440 and 393. The leading was 26 here, and the tracking was
     written as 0.03em, which computes to 0.57px rather than the 0.5
     measured. Both are the measurement now. */
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-eyebrow);
  letter-spacing: 0.5px;
}

/* ── Statement ──────────────────────────────────────────────────────────
   One sentence carrying a whole screen. Capped at the reading measure, since
   a line this size run to the full rail is a headline nobody finishes.
   ---------------------------------------------------------------------- */
.ek-root .ek-statement {
  max-width: var(--ek-reading);
  color: var(--ek-ink);
  font-size: var(--ek-type-statement);
  font-weight: var(--ek-weight-strong);
  /* MEASURED at both ends, and it is not one ratio: about.duolingo.com runs
     the same element 40/44 at 1440 (1.10) and 27/35.1 at 393 (1.30). The
     leading opens up as the size comes down. 1.15 was a single number split
     between the two and matching neither. */
  line-height: var(--ek-leading-statement);
  /* MEASURED lsnormal at both widths. It used to take --ek-tracking-heading,
     which is -0.02em — real, but measured on duolingo.com's 64px and 36px
     BAND headlines, not on this. A statement is not a band headline, and
     about.duolingo.com sets no tracking here. */
  letter-spacing: normal;
  text-wrap: balance;
}
/* The other measured end: 27/35.1 at 393. */
@media (max-width: 600px) {
  .ek-root .ek-statement { line-height: 1.3; }
}

/* ── Text link ──────────────────────────────────────────────────────────
   The quiet alternative to a control: it goes somewhere, it does not act.

   No underline on hover, here or anywhere else on the site. A rule that
   appears under the cursor is a second thing moving on a page that is
   already showing a pointer; where a link needs to look like a link, it is
   underlined at rest instead (see .ek-prose, and the contact panels).
   ---------------------------------------------------------------------- */
.ek-root .ek-link {
  color: var(--ek-brand);
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  /* MEASURED 15/21 w700 ls0.5px on about.duolingo.com's "Learn more", both
     widths. The tracking is taken; the leading stays on the 22px rung the
     footer link already uses, since 21 against 22 is the one-pixel kind of
     difference this system has already decided not to add a rung for. */
  line-height: var(--ek-leading-label);
  letter-spacing: 0.5px;
  text-decoration: none;
}
@media (hover: hover) {
.ek-root .ek-link:hover { color: var(--ek-brand-pressed); }
}

/* ── Grid ───────────────────────────────────────────────────────────────
   Columns that fit rather than columns that are counted: three on the rail,
   two on a tablet, one on a phone, with no breakpoint deciding it.
   ---------------------------------------------------------------------- */
.ek-root .ek-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: var(--ek-space-8) var(--ek-space-6);
}

/* ── Card ───────────────────────────────────────────────────────────────
   A tinted panel holding an image, then a title, a line of body, and a link.
   The panel keeps its shape whatever is dropped into it, so a row of cards
   lines up even when the art does not match.
   ---------------------------------------------------------------------- */
.ek-root .ek-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ek-space-3);
}
.ek-root .ek-card__panel {
  width: 100%;
  aspect-ratio: 4 / 3;
  margin-bottom: var(--ek-space-2);
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--ek-surface-sunken);
  /* MEASURED 20px at 1440 and 10px at 393 — about.duolingo.com's card image.
     --ek-radius-card is 24px, which is blog.duolingo.com's card and the right
     value for .ek-panel; it was never this element's. */
  border-radius: var(--ek-radius-media);
}
.ek-root .ek-card__panel > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.ek-root .ek-card__title {
  /* MEASURED 20/24 w700 — about.duolingo.com's card title, the same at
     1440 and 393. It really is its own rung: 20/24 is both a size and a
     leading away from the 19/28 of the body under it. */
  font-size: var(--ek-type-card-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-card-title);
}
.ek-root .ek-card__body {
  color: var(--ek-ink-muted);
  /* MEASURED 19/28 w400 — about.duolingo.com's card body, at both widths.
     This used to borrow --ek-type-body on the argument that 18/27 was
     "within a pixel"; the body is 17/23.8 now, which is three pixels of
     leading away, so the card takes the rung measured for it. */
  font-size: var(--ek-type-list-title);
  line-height: var(--ek-leading-eyebrow);
}

/* ── Post hero ──────────────────────────────────────────────────────────
   The one post a page leads with: a column of type and a picture beside it,
   on the rail. MEASURED on blog.duolingo.com @1440 — the hero's two children
   are 459 and 540 wide inside the 1070 rail, which leaves exactly 71
   between them.

   It is the feature row's proportions the other way round. There the picture
   is the square and the text column is the narrow one; here the picture is
   the wider of the two, because the thing being sold is the article, not the
   illustration.

   Stacks at 900 with the picture on top, so a phone reads it the same way
   round as a feature row.
   ---------------------------------------------------------------------- */
.ek-root .ek-hero {
  display: flex;
  align-items: center;
  gap: var(--ek-hero-gap);
}
/* flex-basis with shrink allowed, not `width` with `flex: none`. The measured
   459 + 71 + 540 is 1070, which is the rail at 1110 and wider; at anything
   between the 900 stack and there it is more than the page, and `flex: none`
   made the row overflow rather than give. Found at 1040: the art's right edge
   stood at 1090 against a 1040 viewport. Shrinking keeps the 459:540 ratio,
   and both hit their measured widths the moment there is room.
   (.ek-feature below had the same fault, and worse — 473 + 101 + 530 is 1104
   against a 988 rail, so it overflowed at every width above the stack.) */
.ek-root .ek-hero__text {
  flex: 0 1 var(--ek-hero-text);
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 24 between the hero's own blocks. It was 20 for everything, which spaced
     the meta line, the headline, the standfirst and the control evenly — four
     things at one distance read as a list, not as a headline with its parts.
     The meta binds to the headline instead, below. */
  gap: var(--ek-space-6);
}
/* MEASURED 10px between the meta line and the title on the reference's post
   card (23 applications, its most used spacing value). 12 is the rung. The
   same grouping runs in .ek-post__body, so a lead post and a card in the grid
   hold their two lines together the same way. */
.ek-root .ek-hero__head {
  display: flex;
  flex-direction: column;
  gap: var(--ek-space-3);
}
.ek-root .ek-hero__art {
  flex: 0 1 var(--ek-hero-art);
  min-width: 0;
  /* MEASURED 3:2, and it took a screenshot rather than the probe: the probe
     reads container widths and not heights, so the aspect was a guess, and
     the guess was 4:3. Scaling Hoon's screenshot by its own 1070 rail
     (1315 image px for 1070 CSS, so 1.229) puts the hero picture at
     537 x 358 and a post picture at 334 x 228 — 0.667 and 0.683 against
     0.750 for a 4:3.

     This is why the page read heavy. Every picture on it was 15% taller than
     the reference's, which pushed the text under it down and turned the air
     between rows into the thing you noticed. */
  aspect-ratio: 3 / 2;
  overflow: hidden;
  /* MEASURED 30px on the reference's own hero picture — one element, against
     24px on the fifteen post cards below it. A rung for a single use six
     pixels from an existing one is the thing this system has already decided
     not to add; the hero takes the card radius. */
  border-radius: var(--ek-radius-card);
  background: var(--ek-surface-sunken);
}
.ek-root .ek-hero__art > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
@media (max-width: 900px) {
  .ek-root .ek-hero {
    flex-direction: column-reverse;
    align-items: stretch;
    gap: var(--ek-space-6);
  }
  /* The basis has to be reset, not just the width. Once the row becomes a
     column, flex-basis governs HEIGHT — so the art's 540 basis made it 540
     tall and threw the 3:2 away, reading 363x540 on a phone. `auto` hands
     the sizing back to width and aspect-ratio. */
  .ek-root .ek-hero__text,
  .ek-root .ek-hero__art { flex: 0 1 auto; width: 100%; }
}

/* ── Meta line ──────────────────────────────────────────────────────────
   When, and by whom. MEASURED on blog.duolingo.com @1440: the date reads
   14.07/19.7 w700 with 0.8px of tracking, the byline 13.2/18.48 with the
   same weight and the same tracking.

   Those are one rung, not two. That page renders its whole scale at x0.9375
   — its <body> computes to 15px where every other run's is 17 — so as
   authored the two readings are 15/21 and 14/19.7, and a date and a name on
   the same line one pixel apart is a difference nobody sees. It takes the
   15/22 label rung the footer link and the text link already use.
   ---------------------------------------------------------------------- */
.ek-root .ek-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ek-space-2);
  color: var(--ek-ink-subtle);
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-label);
  letter-spacing: 0.8px;   /* measured */
  /* The one uppercase this system takes, and it is worth saying why the
     sentence-case rule does not apply. That rule exists because START TODAY
     next to 시작하기 reads as two systems — Korean has no uppercase to answer
     with. A date is a month abbreviation and a number: SEP 4 has no Korean
     counterpart that loses anything, because 9월 4일 carries no case either
     way. MEASURED on blog.duolingo.com, which sets its own meta line this
     way. */
  text-transform: uppercase;
}
/* The separator is drawn, like the breadcrumb's: a bullet in the markup is a
   character a screen reader announces between a date and a name, and a
   translator translates. The reference draws a rule here too. */
.ek-root .ek-meta > * + * { display: flex; align-items: center; gap: var(--ek-space-2); }
/* A vertical rule, not a dash. It was a horizontal one, which reads as an em
   dash set in a rule — the reference draws AUG 11 | JAMES CLAY, and a
   separator that looks like punctuation is punctuation. Height is 1em so it
   tracks the type rather than being a number. */
.ek-root .ek-meta > * + *::before {
  content: '';
  width: 0;
  height: 1em;
  border-left: var(--ek-hairline) solid currentColor;
  opacity: 0.7;
}
.ek-root .ek-meta a { color: inherit; text-decoration: none; }
@media (hover: hover) {
.ek-root .ek-meta a:hover { color: var(--ek-ink-muted); }
}

/* ── Post grid ──────────────────────────────────────────────────────────
   MEASURED on blog.duolingo.com @1440: three columns of 330.66 with 39
   between them — 1070 to within a rounding error — and 56 between rows.
   --two is the reference's other grid on the same page, a pair of 518s.

   Columns that fit rather than columns that are counted, the same way
   .ek-grid works: the floor is the measured column, so three fit on the rail
   and a fourth cannot, without a breakpoint deciding it.
   ---------------------------------------------------------------------- */
.ek-root .ek-posts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr));
  gap: 56px 39px;   /* measured: row, then column */
}
/* MEASURED 518 + 34 + 518 = 1070, exactly. The narrower gap was a guess when
   this was written — the blog's histogram gave 39 for the three-up and a 518
   pair that could not fit beside it — and about.duolingo.com's box dump
   settles it: `div.iEEWv gap 34/34` holding two 518s on the same 1070 rail.
   So the pair grid carries its own column gap, and the floor is the measured
   column now that a pair of them fits. */
.ek-root .ek-posts--two {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 518px), 1fr));
  column-gap: 34px;   /* measured */
}

/* ── Post card ──────────────────────────────────────────────────────────
   A picture, when and by whom, and the title. No excerpt: the reference
   carries running text on the hero post only, and a three-up of paragraphs
   is a page nobody scans.
   ---------------------------------------------------------------------- */
.ek-root .ek-post {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ek-space-5);   /* measured 20 */
}
.ek-root .ek-post__art {
  width: 100%;
  aspect-ratio: 3 / 2;   /* MEASURED 334 x 228 — see .ek-hero__art */
  overflow: hidden;
  /* MEASURED 24px — the post image on blog.duolingo.com, fifteen of them,
     the dominant radius on the page and the reading --ek-radius-card was
     taken from. */
  border-radius: var(--ek-radius-card);
  background: var(--ek-surface-sunken);
}
.ek-root .ek-post__art > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.ek-root .ek-post__body {
  display: flex;
  flex-direction: column;
  /* MEASURED 10px between the meta line and the title. 12 is the rung. */
  gap: var(--ek-space-3);
}
.ek-root .ek-post__title {
  display: block;
  color: var(--ek-ink);
  /* MEASURED 20/28 as authored @1440 — 18.75/26.25 on screen at that page's
     x0.9375 — and 19/26.6 read directly at 393, where nothing is scaled. The
     two readings are one rung apart in both size and leading; the desktop one
     is taken, and it is a pair this system already has. */
  font-size: var(--ek-type-card-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-eyebrow);
  text-decoration: none;
}
/* Color, never a rule. No underline on hover anywhere on this site. */
@media (hover: hover) {
.ek-root a.ek-post__title:hover { color: var(--ek-brand); }
}

/* ── Prose ──────────────────────────────────────────────────────────────
   A document that is read start to finish — terms, a privacy policy, a
   long-form post. Everything else in this system is a screen you scan; this
   is the one place where the rhythm between a heading, a paragraph and a
   list is the design.

   Its running text is --ek-ink, not --ek-ink-muted. Muted ink is for copy
   that supports something else (a line under a card title); here the
   paragraphs ARE the content, and a legal document that has to hold up is
   not the place to spend contrast on tone.

   Every value below is measured on duolingo.com/terms at 1440 and 393 — the
   reference's own document of this kind. It is its own shape, not the body
   type in a narrower column: the size, the leading, the heading rung and the
   whole vertical rhythm all differ from a marketing page.
   ---------------------------------------------------------------------- */
.ek-root .ek-prose {
  max-width: var(--ek-reading-prose);
  color: var(--ek-ink);
  font-size: var(--ek-type-prose);
  line-height: var(--ek-leading-prose);
  /* The reference sets this text at w500, not w400 — but in rgb(153,153,153),
     a light gray, where ours is --ek-ink at near-black. The two are a pair: a
     medium weight in light gray and a regular weight in near-black land at
     about the same density on screen. Taking the 500 alone, against our ink,
     would print heavier than the page it was measured from. So the weight
     stays 400 and the reason is a measurement too. */
  font-weight: var(--ek-weight-body);
}
/* MEASURED 18px at 393. The type goes UP on a phone, not down. */
@media (max-width: 600px) {
  .ek-root .ek-prose { font-size: 18px; }
}

/* The lesson body. Same component — it is still a passage of running text —
   at the lesson rung, which is a size and a leading the reference has no
   counterpart for because it has no reading to do. See --ek-type-lesson.
   The reading cap comes off too: a lesson is read in the app's column, and
   that column is already narrower than the cap. */
.ek-root .ek-prose--lesson {
  max-width: none;
  font-size: var(--ek-type-lesson);
  line-height: var(--ek-leading-lesson);
}
/* MEASURED 18px under a paragraph — 67 applications, the most-used spacing on
   that page at both widths. It was 12px, the generic block rhythm rather than
   this page's. 18 is off the 4N scale, so it is written as a literal here
   rather than bent onto a rung it does not sit on. */
.ek-root .ek-prose > * + * { margin-top: 18px; }
/* MEASURED 19/20 w700 for the section heading, identical at both widths. */
.ek-root .ek-prose :is(h2, h3, h4, h5) {
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-prose-heading);
}
.ek-root .ek-prose h2 { font-size: var(--ek-type-list-title); }
/* The sub-heading measures 15px at 1440 and 18px at 393 — smaller than the
   text under it on a desktop, larger on a phone. Ours matches its own body at
   both widths, which is the phone reading exactly and declines to make a
   heading smaller than the copy under it off a single desktop reading.
   `inherit`, not the token: the token is the desktop value, so naming it here
   pinned h3 at 17 while the body went to 18 on a phone — the very inversion
   this comment says we are avoiding. */
.ek-root .ek-prose h3 { font-size: inherit; }
/* The weight is the other half of the same trade. The reference sets its
   sub-heading at w500 in rgb(153,153,153) — the same weight and the same gray
   as the paragraphs around it, so only the size and the 20px leading mark it
   as a heading at all. Ours is already the same size as its body (above) and
   sits in --ek-ink like the rest of the document, so taking the 500 as well
   would leave a sub-heading with nothing to distinguish it from a paragraph.
   The 700 comes from the rule above this one; this note is why it stays. */
/* The gap above a heading is what separates one clause from the next. It is
   set on the heading rather than on the block before it, so the rhythm holds
   however the sections are ordered.
   MEASURED 10px under a heading (35 applications) and 40px between sections
   (29). Ours were 8 and 40 — the 40 was already right. */
.ek-root .ek-prose > h2 + *,
.ek-root .ek-prose > h3 + * { margin-top: 10px; }
.ek-root .ek-prose > h2 { margin-top: var(--ek-space-10); }
/* A SUB-heading gets no extra air: MEASURED 18px above it — the ordinary
   block rhythm, not a gap of its own — on nine sub-headings at 1440 and five
   at 393. Only the section heading opens a new clause, and only it gets the
   40. Ours gave the sub-heading 24, which read as a third rung of separation
   the reference does not have. There is no rule here on purpose: `> * + *`
   above already sets 18, and `> h2 + *` still gives 10 when a sub-heading
   follows a section heading directly, which is also what the reference does. */
.ek-root .ek-prose > :first-child { margin-top: 0; }
/* MEASURED 30px for a list inside a clause, at both widths — every list on
   duolingo.com/terms is set `margin: 0 0 18 30` against the paragraphs around
   it. Ours indented 20, which is the reference's indent for a different list:
   the single <ol> that IS the document, one clause per item. Ours has no such
   list, so 30 is the number that matches the role we actually use. Off the 4N
   scale, so it is written as a literal rather than bent onto a rung. */
.ek-root .ek-prose :is(ul, ol) { padding-left: 30px; }
/* MEASURED zero between list items — 22 items across four lists, at both
   widths, every one of them flush against the last. Ours put 8px there. The
   items run to two and three lines each in the reference too, so this is not
   a short-list special case; the 1.3 leading and the marker do the separating.
   No rule: `> * + *` does not reach an li, so the default of 0 is the value. */
.ek-root .ek-prose strong { font-weight: var(--ek-weight-strong); }
.ek-root .ek-prose a {
  color: var(--ek-brand);
  font-weight: var(--ek-weight-medium);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ek-root .ek-prose hr {
  margin-block: var(--ek-space-8);
  border: 0;
  border-top: var(--ek-hairline) solid var(--ek-stroke);
}
/* The revision date CLOSES the document, it does not open it.
   MEASURED on duolingo.com/terms at both widths: 17/20 w500, 40px below the
   last clause, and in the page's own ink — not the light gray the clauses
   themselves are set in, so it reads as the document's own signature rather
   than as one more line of it. Ours sat under the headline in muted ink.
   Selected as a direct child so the 40 outranks the 18 of the block rhythm
   without depending on where this rule sits in the file. */
.ek-root .ek-prose > .ek-prose__revised {
  margin-top: var(--ek-space-10);
  color: var(--ek-ink);
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-prose-heading);
}

/* ── Post ───────────────────────────────────────────────────────────────
   An article read start to finish, which is not the document shape: a
   document is a contract set in one column at 1.3, a post is written to be
   read and sets its body at 1.4 with a heading rung between the body and the
   headline. MEASURED on blog.duolingo.com/chess-game-review at both widths.

   The column is 680 there against the 670 of the blog index's centered
   wrapper — ten pixels apart on the same site, for two things that are
   plainly the same width. One token, and this system has already decided not
   to add a rung for ten pixels.
   ---------------------------------------------------------------------- */
.ek-root .ek-post-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* MEASURED 27px between the headline, the standfirst and the meta line —
     the reference's own gap, two applications. */
  gap: 27px;
}
/* The banner picture. MEASURED 680x450 at 1440 and 348x220 at 393: 3:2 and
   0.632, which is the same picture rounded by a phone's column. The corner
   is 30 on the reference and 24 here, the same call the index hero makes. */
.ek-root .ek-post-art {
  width: 100%;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--ek-radius-card);
  background: var(--ek-surface-sunken);
}
.ek-root .ek-post-art > img {
  width: 100%; height: 100%; display: block; object-fit: cover;
}

/* ── Post prose ─────────────────────────────────────────────────────────
   .ek-prose is the terms shape: 17/22.1 on a 1.3 ratio, headings on the 19
   rung, paragraphs 18 apart. A post is looser and taller in every one of
   those, MEASURED on the reference at 1440 and 393 alike:

     body    17/23.8 w400          (1.4, our --ek-leading-body)
     h2      24/33.6 w700          (also 1.4, so only the size is new)
     h3      20/28   w700          (rungs we already had)
     p to p  31
     around a heading  24 above and 24 below

   These selectors are (0,2,0) and so are .ek-prose's, so this block HAS to
   come after it — the same source-order trap the reading rail hit. Moving it
   up in the file silently reverts the whole shape.
   ---------------------------------------------------------------------- */
.ek-root .ek-prose--post {
  max-width: none;                       /* the post's column decides */
  font-size: var(--ek-type-body);
  line-height: var(--ek-leading-body);
}
.ek-root .ek-prose--post > * + * { margin-top: 31px; }   /* measured */
.ek-root .ek-prose--post :is(h2, h4) {
  font-size: var(--ek-type-subhead);
  line-height: var(--ek-leading-body);
}
.ek-root .ek-prose--post :is(h3, h5) {
  font-size: var(--ek-type-card-title);
  line-height: var(--ek-leading-eyebrow);
}
.ek-root .ek-prose--post > :is(h2, h3, h4, h5) { margin-top: var(--ek-space-6); }
.ek-root .ek-prose--post > :is(h2, h3, h4, h5) + * { margin-top: var(--ek-space-6); }
.ek-root .ek-prose--post > :first-child { margin-top: 0; }
/* MEASURED: a link inside a post is w700 and underlined at rest, where the
   terms page leaves it at the body weight. It is the one place this system
   underlines, and it is not a hover effect — the rule is there before the
   cursor is. */
.ek-root .ek-prose--post a { font-weight: var(--ek-weight-strong); }

/* ── Tags ───────────────────────────────────────────────────────────────
   MEASURED on the reference's post: a chip 28 tall on an 8px corner with 10
   of padding, 9 between chips, on a tint of its own ink. The label is 12/w700
   with 0.48px of tracking; ours is the 13 rung with the 0.5 this system
   already uses, which is one pixel and one hundredth of an em away.
   ---------------------------------------------------------------------- */
.ek-root .ek-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;   /* measured */
}
.ek-root .ek-tag {
  display: inline-flex;
  align-items: center;
  height: 28px;              /* measured */
  padding-inline: var(--ek-space-3);
  border-radius: var(--ek-radius-chip);
  background: var(--ek-tint);
  color: var(--ek-brand);
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-strong);
  line-height: 1;
  letter-spacing: 0.5px;
  text-decoration: none;
  text-transform: uppercase;
}
@media (hover: hover) {
.ek-root .ek-tag:hover { background: rgba(30, 64, 175, 0.12); }
.ek-root .ek-tag:hover { background: color-mix(in srgb, var(--ek-brand) 12%, transparent); }
}
/* The label above the chips. MEASURED 19 w700 ls0.5 uppercase, which is the
   eyebrow's rung and tracking in a muted ink rather than the brand's. */
.ek-root .ek-tags-title {
  display: block;
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-eyebrow);
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

/* ── Panel ──────────────────────────────────────────────────────────────
   A tinted block that sets a short passage apart from the document around
   it — where to write to, what the box below a form is for. It is the card's
   ground without the card's fixed 4:3 shape, so it takes the height of
   whatever is put in it.
   ---------------------------------------------------------------------- */
.ek-root .ek-panel {
  padding: var(--ek-space-8);
  background: var(--ek-surface-sunken);
  border-radius: var(--ek-radius-card);
}
/* The brand tint instead of the neutral sink, for a panel that is the thing to
   do rather than an aside — the classroom's mission block, which has carried a
   blue tint since long before ek and is right to. */
.ek-root .ek-panel--tint { background: var(--ek-tint); }

/* ── Breadcrumbs ────────────────────────────────────────────────────────
   Where you are, one level up. MEASURED on duolingo.com/help: 19/22.8 w700
   with 0.8px of tracking, in the brand color, both widths.
   ---------------------------------------------------------------------- */
.ek-root .ek-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ek-space-2);
  color: var(--ek-brand);
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-strong);
  line-height: 22.8px;      /* measured; 19 x 1.2 */
  letter-spacing: 0.8px;    /* measured */
}
.ek-root .ek-crumbs a { color: inherit; text-decoration: none; }
@media (hover: hover) {
.ek-root .ek-crumbs a:hover { color: var(--ek-brand-pressed); }
}
/* The separator is drawn, not typed: a chevron in the text is a character a
   screen reader announces and a translator translates. */
.ek-root .ek-crumbs li { display: flex; align-items: center; gap: var(--ek-space-2); }
.ek-root .ek-crumbs li + li::before {
  content: '';
  width: 6px; height: 6px;
  border: 2px solid currentColor;
  border-left: 0; border-bottom: 0;
  transform: rotate(45deg);
  opacity: 0.55;
}
.ek-root .ek-crumbs [aria-current] { color: var(--ek-ink-muted); }

/* ── FAQ ────────────────────────────────────────────────────────────────
   A card of questions that open in place. Built on <details>/<summary>, so
   it opens with a keyboard, is announced as expandable, and survives with
   JavaScript off — the accordion behavior is the element's, not ours.

   MEASURED on duolingo.com/help at 1440 and 393: the card takes the 16px
   radius, its group title runs 19/20 w700 in the brand color, each question
   19/20 w500, rows are separated by a hairline, and the padding is 26px
   across the row and 32px in from the sides.

   The row's vertical padding is 32px, not the 26px measured. A deliberate
   departure, recorded rather than hidden: at 26 the rows read as one block
   of text to scan through rather than a list of separate questions, and Hoon
   said so looking at the page. 32 is a rung on our own scale; 26 is not.

   One row open at a time comes from the `name` attribute on <details>, which
   is the HTML spec's own exclusive accordion — no script, no state to keep.
   A browser too old to know it simply lets several open, which is what the
   page did before, so nothing breaks on the way down.
   ---------------------------------------------------------------------- */
.ek-root .ek-faq {
  border: var(--ek-hairline) solid var(--ek-stroke);
  border-radius: var(--ek-radius-panel);
  /* The rows' own hairlines have square ends; without this they poke out
     past the card's curve at the top and bottom row. */
  overflow: hidden;
}
/* MEASURED 48px, and it took the box dump to see it. The histogram had the
   number all along — `48px, marginTop, 5 uses` — and it was read as the space
   around the page headline, which is also 48. The dump shows where the other
   applications are: each FAQ card carries `margin 0 0 48 0`, and each one
   after the first sits 48 below the one above, at 1440 and 393 alike. Ours
   was 24, which is why the page read tighter than the reference even with
   every other value on it matching. */
.ek-root .ek-faq + .ek-faq { margin-top: var(--ek-space-12); }
.ek-root .ek-faq__title {
  padding: var(--ek-space-8);        /* 32; measured 26 down, 32 across */
  color: var(--ek-brand);
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-prose-heading);
}
.ek-root .ek-faq__item { border-top: var(--ek-hairline) solid var(--ek-stroke); }
.ek-root .ek-faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ek-space-5);
  padding: var(--ek-space-8);
  cursor: pointer;
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-prose-heading);
  list-style: none;                       /* Firefox */
}
.ek-root .ek-faq__q::-webkit-details-marker { display: none; }
/* Inset, because the question is a full-width row inside a bordered list: an
   outset ring would overlap the item above. The ring itself is the shared one. */
.ek-root .ek-faq__q:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: calc(-1 * var(--ek-focus-offset));
}
/* The chevron is drawn so it can rotate; a glyph would need a second glyph. */
.ek-root .ek-faq__q::after {
  content: '';
  flex: none;
  width: 10px; height: 10px;
  margin-top: -5px;
  border: 2px solid var(--ek-ink-muted);
  border-top: 0; border-left: 0;
  transform: rotate(45deg);
  transition: transform var(--ek-duration-short) var(--ek-easing);
}
.ek-root .ek-faq__item[open] > .ek-faq__q { font-weight: var(--ek-weight-strong); }
.ek-root .ek-faq__item[open] > .ek-faq__q::after {
  margin-top: 5px;
  transform: rotate(-135deg);
}
/* MEASURED 19/27 w500 rgb(60,60,60), read off duolingo.com/help with a row
   open. One size runs the whole card: the question row is 19/20 w500 and the
   answer is the same 19 and the same 500, with the leading opened from 20 to
   27. It does not take the prose rung — a help answer is not a document. */
.ek-root .ek-faq__a {
  padding: 0 var(--ek-space-8) var(--ek-space-8);
  color: var(--ek-ink);
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-list-title);
}
.ek-root .ek-faq__a > * + * { margin-top: 18px; }
@media (max-width: 600px) {
  /* MEASURED 10px at 393 on about.duolingo.com's card image. */
  .ek-root .ek-card__panel { border-radius: 10px; }
  /* The answer used to go to 18 here, because it was on the prose rung and
     the reference's document type gets bigger on a phone. It is on the 19
     rung now, and /help's type histogram is IDENTICAL at 1440 and 393 —
     nothing on that page scales. So nothing changes here. */
  /* The row's padding across does NOT come down on a phone. Measured 32 on
     duolingo.com/help at 393 as well as 1440 — 357 of card less 64 leaves the
     293 its question column actually reads. This block used to narrow it to
     20, which was not measured and not recorded as a decision either. Only
     the answer's size changes here, and that is measured. */
}
@media (prefers-reduced-motion: reduce) {
  .ek-root .ek-faq__q::after { transition: none; }
}

/* ── Progress ───────────────────────────────────────────────────────────
   The bar across the top of a quiz. MEASURED off
   duolingo.com/welcome?welcomeStep=learningReason at 1440 and 393: an 8px
   corner on the track, a 9999px shine inside the fill, 18 above it, and a
   track 1002 wide at 1440 / 327 at 393 — i.e. it runs the full width of
   whatever it is in, not a rail of its own.

   HEIGHT IS NOT MEASURED. That run came back with an empty `boxes` array, so
   no element on the page reported a height. 16 here is INFERRED, not read: a
   track with an 8px radius is a pill exactly when it is 16 tall, and the
   reference's track is visibly a pill. Replace it with the reading the moment
   a run with boxes lands — it is one value in one place for that reason. */
.ek-root .ek-progress {
  position: relative;
  height: var(--ek-space-4);
  background: var(--ek-stroke);
  border-radius: var(--ek-radius-chip);
  overflow: hidden;
}

/* Milestones. noom divides its bar with marks so a long quiz reads as a few
   sections rather than one endless run — you can see how many are left, which
   a continuous bar never tells you.

   IT MARKS THE TRACK, IT DOES NOT CUT IT. The first version drew a 3px line
   through the full height at each boundary, and Hoon's read of it was that the
   bar looked severed rather than divided. noom's are diamonds sitting ON the
   track, which is the difference: a shape is a landmark you are traveling
   past, a full-height line is a wall. Ours is the same diamond, at ek's
   dimensions rather than measured — noom is unprobed.

   No percentages and no count in the CSS: the marks are equal flex cells and
   every one but the last carries a diamond on its right edge, so N cells give
   N-1 marks and adding a section is adding a cell. The page decides how many
   sections there are, which is where that belongs.

   The diamond is --ek-surface so it reads on both grounds: a notch on the gray
   track, and a clear landmark once the brand fill has passed it. 8px square
   turned 45° is 11.3 across the diagonal, inside a 16 track with room either
   side. */
.ek-root .ek-progress__marks {
  position: absolute;
  inset: 0;
  display: flex;
  pointer-events: none;
}
.ek-root .ek-progress__marks > * { position: relative; flex: 1; }
.ek-root .ek-progress__marks > :not(:last-child)::after {
  content: '';
  position: absolute;
  right: 0;
  top: 50%;
  width: var(--ek-space-2);
  height: var(--ek-space-2);
  background: var(--ek-surface);
  transform: translate(50%, -50%) rotate(45deg);
}
/* A mark the fill has gone past becomes a tick. noom does this to the whole
   filled segment; ours does it to the landmark, which is the thing that was
   already there to change. The page adds `is-done` — how far along the bar is
   is data, and the component has never known it. Same --ek-surface, so it
   reads white on the brand fill exactly as the diamond did on the track.

   IT DOES NOT CARRY ITS OWN GROUND. A done mark sits ON the boundary it
   marks, so when the fill stops exactly there the tick straddles the edge.
   That is fixed by the FILL going a few pixels past a mark it has reached —
   which is what the reference does — and the page owns the fill's width. A
   disc of brand fill under the tick was tried instead and read as a bulge on
   the end of the bar. */
.ek-root .ek-progress__marks > .is-done::after {
  width: 10px;
  height: 5px;
  background: none;
  border: solid var(--ek-surface);
  border-width: 0 0 2px 2px;
  /* INSIDE the fill, not centred on the boundary. A mark sits on its cell's
     right edge, and the fill reaches that same edge the moment the cell's
     question is answered — so a tick centred there straddled the end of the
     bar, half on brand and half on the gray still to come, and read as the
     progress having stopped at it rather than passed it. Anchored 4px in, its
     whole box is behind the fill's leading edge. */
  right: var(--ek-space-2);
  transform: translateY(-70%) rotate(-45deg);
}

/* The fill. The reference stacks a green face, a yellow inner and a white
   9999px shine; ours is one brand fill with the same shine, because the
   three-color stack is theirs and the color is the one thing ek never
   copies. The width is set inline by the page — it is data, not style. */
.ek-root .ek-progress__bar {
  position: relative;
  height: 100%;
  background: var(--ek-brand);
  border-radius: var(--ek-radius-chip);
  transition: width var(--ek-duration-medium) var(--ek-easing);
}
.ek-root .ek-progress__bar::after {
  content: '';
  position: absolute;
  inset: 3px 6px auto;
  height: 3px;
  background: rgba(255, 255, 255, 0.45);
  background: color-mix(in srgb, var(--ek-surface) 45%, transparent);
  border-radius: var(--ek-radius-full);
}

/* ── Pickable ───────────────────────────────────────────────────────────
   The box you press to CHOOSE something — one shape, worn by an answer on a
   quiz question (.ek-choice) and by a priced plan on the last screen
   (.ek-plan).

   Both used to carry their own copy of all of this: the surface, the 2px
   frame, the ledge under it, the radius, and all four states. The states were
   character-for-character identical, which is not duplication anybody decided
   on — it is what happens when the second component is written by looking at
   the first. Choosing has to feel the same in both places or the quiz's last
   screen stops feeling like the rest of the quiz, so what the shape IS lives
   here, once.

   What stays theirs is what they hold: an answer centres one line of text, a
   plan ranges a name left and a price hard right. That difference is a real
   one — see the note on .ek-plan — and it is all their own blocks now do.

   The duplication was cheap while there were two. It stops being cheap the
   first time a third pickable thing arrives and copies whichever it was shown.
   ---------------------------------------------------------------------- */
.ek-root .ek-choice,
.ek-root .ek-plan {
  display: flex;
  align-items: center;
  width: 100%;
  color: var(--ek-ink);
  background-color: var(--ek-surface);
  border: var(--ek-stroke-control) solid var(--ek-stroke);
  /* The ledge: a bottom border thicker by the sink distance, so pressing can
     travel that far and land flush. */
  border-bottom-width: calc(var(--ek-stroke-control) + var(--ek-depth-subtle));
  border-radius: var(--ek-radius-control);
  cursor: pointer;
  font-family: inherit;
  transition: background-color var(--ek-duration-short) var(--ek-easing),
              border-color var(--ek-duration-short) var(--ek-easing);
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
.ek-root .ek-choice:hover,
.ek-root .ek-plan:hover { background-color: var(--ek-surface-sunken); }
}

/* Pressed: the card sinks onto its own ledge, the same motion the control
   makes, so an answer, a plan and a button agree about what pressing feels
   like.

   The padding is what keeps the page still. Losing 2px of bottom border makes
   the box 2px shorter, and everything below it moved up by that much for as
   long as a finger was down — on a plan, which sits near the end of a long
   scrolling page, the whole page jumped. .ek-control never had it because the
   face it moves is a ::before and the button's own box never changes.

   The give-back has to be PADDING, not margin. Everything here is border-box,
   so the box is max(min-height, content + padding + border): padding-bottom
   and border-bottom-width trade one for one inside that sum, which leaves the
   height the same whichever term is deciding it, and leaves the content area
   in the same place too. A margin only cancels the content-driven case — on
   .ek-choice, whose min-height was already absorbing the border loss, it added
   the 2px instead of returning it, so pressing an answer pushed every option
   below it down and grew the page by 2px a press. Padding also still holds if
   a long label wraps and a choice stops being min-height-driven.

   Each pickable states its resting bottom padding as --ek-pickable-pad-bottom
   next to its own padding shorthand, because there is no way to say "the
   padding you already have, plus two" in CSS. */
html:not(.ek-press-js) .ek-root .ek-choice:active,
html:not(.ek-press-js) .ek-root .ek-plan:active,
.ek-root .ek-choice.ek-pressed,
.ek-root .ek-plan.ek-pressed {
  transform: translateY(var(--ek-depth-subtle));
  border-bottom-width: var(--ek-stroke-control);
  padding-bottom: calc(var(--ek-pickable-pad-bottom, 0px) + var(--ek-depth-subtle));
}

/* Chosen. MEASURED that the reference recolors the LABEL rather than filling
   the card — its options run rgb(75,75,75) and the selected one rgb(24,153,214),
   and no new fill appears in that run. Ours does the same in the brand color,
   plus the tint, because at ek's weights the label alone is too quiet to find
   on a six-option list. (.ek-choice takes the label recolour too; a plan's
   price does its own recolouring, further down.) */
.ek-root .ek-choice[aria-pressed='true'],
.ek-root .ek-choice[aria-checked='true'],
.ek-root .ek-plan[aria-checked='true'] {
  background-color: var(--ek-tint);
  border-color: var(--ek-brand);
}

.ek-root .ek-choice:focus-visible,
.ek-root .ek-plan:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: var(--ek-focus-offset);
}

/* ── Graded ─────────────────────────────────────────────────────────────
   A quiz that MARKS an answer, which the funnel's never does — it asks what
   you want, and there is no wrong answer to that. The classroom's lesson quiz
   is the other kind, and these two states are what it needs that `chosen` is
   not: chosen says "this is the one you picked", correct and incorrect say
   "and here is what that was worth".

   --ek-positive's own note says it is not a general success colour, that ek
   has no positive and negative pair, and that one label — a plan's saving —
   does not justify inventing a semantic family. That was right when there was
   one label. A graded quiz is the second use and it needs the pair, so the
   family exists now. It is still only these two colours: nothing new was
   invented, and --ek-accent stays what it was, a red for the thing you cannot
   undo and the thing you got wrong.

   THE MARK IS THE EDGE AND THE LABEL, not a fill. Both other answers were
   tried first and both were wrong: a 6% tint vanished (`disabled` lands on
   every option the moment the question is answered, and its 0.5 opacity halved
   the tint and the coloured label together), and a solid fill was, in Hoon's
   read, far too strong for a quiz sitting inside a lesson — a full red card is
   what you use to stop someone.

   So the border takes the colour and the label takes the colour; the surface
   stays the surface. What makes that legible where the 6% version was not is
   the opacity: the two marked answers are NOT dimmed, because being out of
   play is what the other options are. */
.ek-root .ek-choice--correct {
  border-color: var(--ek-positive);
  color: var(--ek-positive);
}
.ek-root .ek-choice--incorrect {
  border-color: var(--ek-accent);
  color: var(--ek-accent);
}
/* A marked answer keeps its colour under the cursor; the neutral hover would
   read as the mark being undone. */
@media (hover: hover) {
.ek-root .ek-choice--correct:hover { background-color: var(--ek-surface); }
}
@media (hover: hover) {
.ek-root .ek-choice--incorrect:hover { background-color: var(--ek-surface); }
}

/* Out of play — the other options once the question is answered. Not the
   control's disabled treatment, which recolours a solid face; here the card
   simply stops inviting a press. It keeps its mark if it has one, because
   `disabled` is the reason you cannot press it and the mark is the answer. */
/* The cursor does NOT change. A quiz answers itself the instant you press it,
   so every option goes disabled under a cursor already sitting on one, and
   `not-allowed` turned the card into a stop sign a beat after the press — a
   refusal where there was nothing left to refuse. The dimming is the signal;
   the pointer just stops being a pointer. */
.ek-root .ek-choice:disabled {
  cursor: default;
  opacity: 0.5;
}
/* A marked answer is not out of play — it is the answer. */
.ek-root .ek-choice--correct:disabled,
.ek-root .ek-choice--incorrect:disabled { opacity: 1; }
@media (hover: hover) {
.ek-root .ek-choice:disabled:hover { background-color: var(--ek-surface); }
}
@media (hover: hover) {
.ek-root .ek-choice--correct:disabled:hover { background-color: var(--ek-surface); }
}
@media (hover: hover) {
.ek-root .ek-choice--incorrect:disabled:hover { background-color: var(--ek-surface); }
}
.ek-root .ek-choice:disabled:active,
.ek-root .ek-choice:disabled.ek-pressed {
  transform: none;
  border-bottom-width: calc(var(--ek-stroke-control) + var(--ek-depth-subtle));
  padding-bottom: var(--ek-space-2);
}

/* ── Choices ────────────────────────────────────────────────────────────
   A question's answers. ONE COLUMN AT EVERY WIDTH, and that is a departure.

   The reference runs two 364 columns inside a 744 list at 1440 — 364 x 2 + 16
   = 744, and 364 appears seven times in that run, once per option. We do not,
   because its answers are two-word labels ("Just for fun", "Boost my career")
   and ours are sentences: 103 real options in quiz/v1/steps.json, the longest
   being "Enjoy K-dramas & K-pop without subtitles". Two columns of sentences
   make the eye scan in two directions on a screen whose whole premise is one
   question at a time.

   The width is --ek-choice-width (400), and it is OURS. The quiz's reference
   is noom.com, which fits its whole question in about that; this file has not
   probed noom, so nothing here is measured off it yet. Checked against the
   content instead: exactly one of those 103 labels needs a second line at 400.

   Two earlier answers, both wrong, both worth keeping: --ek-narrow (670),
   because it is measured and its description sounds like this job, left the
   cards half empty; and 560, from a derivation that measured the card rather
   than the text.

   The 16px gap stays: it is the most used number on the reference's page,
   32 uses at 1440.
   ---------------------------------------------------------------------- */
.ek-root .ek-choices {
  display: grid;
  gap: var(--ek-space-4);
  max-width: var(--ek-choice-width);
  margin-inline: auto;
  padding: 0;
  list-style: none;
}
@media (min-width: 700px) {
  /* MEASURED 77 — the same option row read at ~1170 (`div._8dMUn` 363x77,
     inner `div._1FwHd` 360x71). The reference grows its option on a wide
     screen because the icon in it grows; one token, two readings. Where it
     switches is ours. */
  .ek-root { --ek-choice-height: 77px; }
}

/* One answer.

   It is the secondary control's box: white face, 2px stroke, a 2px ledge, the
   same 12px corner — which is also the corner MEASURED on the reference's own
   options. It keeps the depth where a field dropped it, because unlike a
   field an answer IS pressed.

   NOT CONFIRMED: whether the reference fills its options or outlines them.
   That run reported no fill for them and no box, so outlined is the reading by
   absence, not by measurement. The Noom screen this page's layout came from
   fills them instead. One `background` and one `border` change it.

   Padding is MEASURED 8px 16px, off `div._1FwHd` — the option's inner box,
   357x56 inside the 361x62 row. (An earlier version of this block read the
   361-vs-325 difference as 18px of padding a side. It is not: 2px of that is
   the border and 16 is the padding, which is also the most used number on the
   page — 32 uses at 1440. The 18 was a derivation, not a reading, and it was
   wrong.)

   The height is a min-height rather than a height, so a two-line answer grows
   instead of clipping. */
.ek-root .ek-choice {
  /* Centered, following noom — the quiz's reference — where duolingo puts an
     illustration on the left and ranges its labels off it. Ours carry no icon,
     and a short label ranged left in a 400 card leaves the right half of every
     row empty: neither reference, and the worst of both. Centering is half of
     why noom's list reads as calm. An icon variant would have to range left
     again; the emoji slot below is kept for that. */
  justify-content: center;
  text-align: center;
  gap: var(--ek-space-3);
  min-height: var(--ek-choice-height);
  padding: var(--ek-space-2) var(--ek-space-4);
  /* Read by the pressed state — see Pickable. */
  --ek-pickable-pad-bottom: var(--ek-space-2);
  /* 17/24 is MEASURED — duolingo's option label, seven uses, at both widths.
     The WEIGHT is not: that run reads w700, and noom, which this shape follows,
     does not bold its answers. Bold was the last thing left over from the old
     reference, and with no icon and centered text it made five rows all shout
     at once. --ek-weight-medium is itself measured, as duolingo's body weight.
     It also buys the hierarchy back: the question is 20/700 and only three
     pixels bigger, so without this the two rungs read as one. */
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-body-tight);
}

/* The label recolour that goes with the shared chosen state — see Pickable. */
.ek-root .ek-choice[aria-pressed='true'],
.ek-root .ek-choice[aria-checked='true'] { color: var(--ek-brand); }

/* The emoji sits in a fixed slot so labels line up whatever the glyph's width
   — the same arrangement /about's contact list uses. */
.ek-root .ek-choice__emoji {
  flex: none;
  width: 28px;
  font-size: 22px;
  line-height: 1;
  text-align: center;
}

/* ── Reward ─────────────────────────────────────────────────────────────
   Two lines that pay a person back the moment they have told you something
   that cost them.

   Also noom's, and it is the part of that funnel most worth stealing. After
   the weight field it prints "Thank you for sharing!" in its brand color with
   "That's an important (and hard) first step!" under it in gray — inline,
   right under the answer, before the Next button. It runs the same beat again
   at full-screen size after the health conditions.

   This is the inline size. The full-screen one is a page arrangement, not a
   component: it is a heading and a body on a colored ground, which ek already
   has.

   Ours goes under the obstacle question, which is the one place in our quiz
   where somebody admits something.
   ---------------------------------------------------------------------- */
/* Both lines are read, so both run at the body size. The second line was
   --ek-type-caption (13) for one commit and Hoon could not read it — and he
   named the reason: on noom, 13px gray is where the research citation goes,
   not where the reply to your answer goes. That size belongs to .ek-note. */
.ek-root .ek-reward {
  max-width: var(--ek-choice-width);
  margin: var(--ek-space-4) auto 0;
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-body);
  line-height: var(--ek-leading-body-loose);
}
.ek-root .ek-reward__title {
  display: block;
  margin-bottom: var(--ek-space-1);
  color: var(--ek-brand);
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-body-tight);
}
/* The line under the title, and the element the quiz swaps text into between
   questions. It had no rule at all — it inherited from .ek-reward and looked
   right, which is how a class ends up on three shipped pages while being, to
   this file, a name nobody had heard of. Declared block so the pair reads as
   two rungs whatever it is wrapped in. */
.ek-root .ek-reward__body { display: block; }

/* ── Timeline ───────────────────────────────────────────────────────────
   A dated ladder of milestones — what a person will be able to do, and when.

   This is the quiz's payoff, and it is the one component here with no
   reference behind it at all: noom ends on a weight curve, which is a shape
   for a continuous quantity moving toward one number. Ours is not continuous.
   The course is a fixed 42-day ladder with a named mission every seventh day
   (challenge/plan.html holds the real one), so the honest picture is rungs,
   not a curve.

   The dates are real arithmetic, not decoration: median time to complete
   Day N, measured across 749 students with recorded progress, comes out at
   7.3 days for Day 7 and 42.6 for Day 42 — the course is paced one lesson a
   day and the students who keep going track the calendar almost exactly. So
   a rung's date is its day number added to the start, and that is why this
   component can promise a date at all.

   The rail is the left border of the row's body; the dot is drawn on it. A
   row can wrap without the rail breaking, because the rail belongs to the
   body rather than to a pseudo-element sized in the row.
   ---------------------------------------------------------------------- */
.ek-root .ek-timeline {
  max-width: var(--ek-choice-width);
  margin-inline: auto;
  padding: 0;
  list-style: none;
  text-align: left;
}
/* flex-start, not the default stretch. Stretched, the day label's BOX runs
   the full height of the row, so on a two-line rung its text sat halfway down
   the row instead of beside the line it labels. The padding above is then the
   only thing positioning it, which is what makes the arithmetic in __day
   below true. */
.ek-root .ek-timeline__row { display: flex; align-items: flex-start; gap: var(--ek-space-4); }
/* Both of these are aligned to the OPTICAL CENTRE of the first line of
   __what, not to the top of its box. __what runs 17/24, so that center is 12px
   down; the day label runs 13/16 (center at 8, so 4 of padding) and the dot is
   12 tall (so it sits at 6). At 2px and 3px the day and the dot both floated
   above the line they belong to — visible once you look, which Hoon did. */
.ek-root .ek-timeline__day {
  flex: none;
  width: var(--ek-space-16);
  padding-top: 4px;
  color: var(--ek-ink-subtle);
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-caption);
  text-align: right;
}
.ek-root .ek-timeline__body {
  position: relative;
  flex: 1;
  min-width: 0;
  /* The rail. It stops at the last row because that row's border is removed
     below — a line running past the final rung says the ladder continues,
     and after Day 42 it does not. */
  padding: 0 0 var(--ek-space-6) var(--ek-space-5);
  border-left: var(--ek-stroke-control) solid var(--ek-stroke);
}
.ek-root .ek-timeline__row:last-child .ek-timeline__body {
  padding-bottom: 0;
  border-left-color: transparent;
}
/* The dot sits ON the rail, so it is offset by half its own width plus half
   the rail's. Sized off the rail rather than a token of its own: a 12px dot
   on a 2px rail is the same relationship as the progress bar's 3px mark on
   its 16px track. */
.ek-root .ek-timeline__body::before {
  content: '';
  position: absolute;
  left: calc((var(--ek-space-3) + var(--ek-stroke-control)) / -2);
  top: 6px;
  width: var(--ek-space-3);
  height: var(--ek-space-3);
  background: var(--ek-brand);
  border-radius: var(--ek-radius-full);
}
.ek-root .ek-timeline__what {
  display: block;
  color: var(--ek-ink);
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-body-tight);
}
.ek-root .ek-timeline__when {
  display: block;
  margin-top: var(--ek-space-1);
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
}

/* ── Section ────────────────────────────────────────────────────────────
   Vertical rhythm, and a hairline where one band of content ends and the
   next begins without the ground changing color.
   ---------------------------------------------------------------------- */
.ek-root .ek-section { padding-block: var(--ek-space-24); }
.ek-root .ek-section--divided { border-top: var(--ek-hairline) solid var(--ek-stroke); }

/* ── Store badges ───────────────────────────────────────────────────────
   A control at the stores' own height, carrying a mark and a two-line label.

   The content is left-aligned off one padding, never centered: centering a
   group whose width depends on the wording puts the two marks a few pixels
   apart, which is invisible side by side and obvious once they stack.

   The two marks are deliberately different sizes. The Play triangle fills
   more of its viewBox than Apple's mark fills its own, so equal boxes print
   unequal ink; 32 and 30 both paint 24px, measured off a render. The narrower
   one is padded back to the same slot so the labels start at the same x too.
   ---------------------------------------------------------------------- */
.ek-root .ek-stores {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ek-space-3);
}
.ek-root .ek-store {
  --ek-control-height: var(--ek-control-height-store);
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--ek-space-3);
  /* The bottom padding is the ledge's, so the label centers in the face
     rather than in the face plus its foot. */
  padding: 0 var(--ek-space-5) var(--ek-control-depth);
  color: var(--ek-ink-marketing);
  text-decoration: none;
}
@media (hover: hover) {
.ek-root .ek-store:hover { color: var(--ek-ink-marketing); text-decoration: none; }
}
.ek-root .ek-store svg { flex: none; }
.ek-root .ek-store__mark--apple { width: 32px; height: 32px; }
.ek-root .ek-store__mark--play { width: 30px; height: 30px; margin-inline: 1px; }
.ek-root .ek-store__label {
  display: flex;
  flex-direction: column;
  text-align: left;
  line-height: 1.2;
  /* A store's name is not something to break over two lines. If it does not
     fit, the badge is too narrow and should overflow and be seen rather than
     rewrap and look deliberate. */
  white-space: nowrap;
}
.ek-root .ek-store__label small {
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-medium);
  color: var(--ek-ink-muted);
}
.ek-root .ek-store__label b {
  font-size: var(--ek-type-body);
  font-weight: var(--ek-weight-strong);
}

@media (max-width: 600px) {
  /* Stacked, and both the width of the wider one. A single-column grid sizes
     its track to the widest item and stretches the rest into it, so the two
     badges match with no number to keep in step with the wording. */
  .ek-root .ek-stores {
    display: grid;
    grid-template-columns: max-content;
    justify-content: center;
  }
}

/* ── Footer ─────────────────────────────────────────────────────────────
   Columns that fill by width rather than by breakpoint: one on a phone,
   five on the rail, and no rule in between to get wrong.
   ---------------------------------------------------------------------- */
.ek-root .ek-footer {
  background: var(--ek-brand);
  color: var(--ek-ink-on-solid);
}
.ek-root .ek-footer__inner {
  /* The phone footer is a 375 box: 311 of content inside 32 either side, on
     96 above and 64 below. Measured on the reference and specified by Hoon;
     it was lost once already by being replaced with the page gutter, which
     is not the same thing. */
  max-width: 375px;
  margin-inline: auto;
  padding: var(--ek-space-24) 32px var(--ek-space-16);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(0, 216px));
  column-gap: var(--ek-space-5);
  row-gap: var(--ek-space-8);
}

/* Desktop: the rail, five equal columns on a 24 gap. The sixth group shares
   the last column, stacked. */
@media (min-width: 1040px) {
  .ek-root .ek-footer__inner {
    /* The rail's cap, gutter included — not a bare var(--ek-rail). Without
       the gutter the footer keeps its full width while the sections above it
       are being squeezed by the viewport, so between 1040 and the rail the
       footer stood 5px proud of the page on each side. Measured at a 1100
       viewport on the 1070 rail: 1070 against the sections' 1060. */
    max-width: calc(var(--ek-rail) + 2 * var(--ek-space-5));
    padding-inline: var(--ek-space-5);
    /* MEASURED, and it is 178 after all — with a 45 gap, which is the half
       nobody had. about.duolingo.com's footer box dump reads
       `grid-template-columns: 178px 178px 178px 178px 178px` with each
       section sitting `left: 45` from the one before, and 5x178 + 4x45 is
       1070 exactly: the content rail, to the pixel.

       This went to 1fr on the argument that fixed 178s left 84px of slack on
       1070. They do, on a 24 gap. The slack was never the columns' — it was
       the gap being too small. 1fr with a 24 gap gives 194.8 columns, which
       is a fifth of the rail rather than the width the reference sets.

       So the fix is the gap, not the track type: 1fr on a 45 gap IS 178 on
       the 1070 rail — (1070 - 4x45) / 5 = 178, exactly — and it still shrinks
       between 1040 and the rail instead of standing 50px proud of the page,
       which is what fixed 178s did when this was first written that way. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    column-gap: 45px;
    justify-content: start;
  }
}
.ek-root .ek-footer__title {
  margin-bottom: var(--ek-space-3);
  font-size: var(--ek-type-list-title);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-list-title);
}
.ek-root .ek-footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ek-root .ek-footer__list a {
  display: block;
  color: rgba(255, 255, 255, 0.8);
  color: color-mix(in srgb, var(--ek-ink-on-solid) 80%, transparent);
  /* MEASURED 15/22 w700 on BOTH reference pages — duolingo.com's footer
     links and about.duolingo.com's nav list, the same "Courses" element.
     Ours inherited w400, which is the one thing this row got wrong. */
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-label);
  text-decoration: none;
}
@media (hover: hover) {
.ek-root .ek-footer__list a:hover { color: var(--ek-ink-on-solid); }
}
/* Two categories sharing a column, when one of them is short. */
/* .ek-footer__col was here: two groups stacked inside one column, so five
   columns could hold six groups. The footer is five groups in five columns
   now — About us, Apps, Help and support, Privacy and terms, Social — so
   nothing used it and nothing demoed it. Deleted rather than left waiting:
   the arrangement it existed for is gone, not postponed. */
.ek-root .ek-footer__legal {
  grid-column: 1 / -1;
  margin-top: var(--ek-space-8);
  color: rgba(255, 255, 255, 0.6);
  color: color-mix(in srgb, var(--ek-ink-on-solid) 60%, transparent);
  font-size: var(--ek-type-label);
  /* The reference sets this line w700 at 1440 and w400 at 393, and both were
     followed while the line carried the registered address and "All rights
     reserved." — a long line that reads as a footnote. It is now the company
     and the year, nothing else, on every ek page; bold made that short line
     shout under the fold, so it takes the phone's weight at every width.
     This is the home page's .duo-legal treatment, promoted once every other
     ek footer had the same short line. */
  font-weight: var(--ek-weight-body);
  /* On a phone the footer lays its groups out on a 216 track and the legal
     line is a grid item like any other, so even this short line broke over
     two rows. It is one sentence and reads as one, so it steps out of that
     track and sizes to itself inside the footer's own 311 of content. */
  width: max-content;
  max-width: 100%;
  white-space: nowrap;
}

@media (max-width: 900px) {
  .ek-root .ek-feature,
  .ek-root .ek-feature--reverse {
    flex-direction: column;
    min-height: 0;
    gap: var(--ek-space-8);
    text-align: center;
  }
  /* Same reason as the hero's: in a column, flex-basis is height. `auto`
     hands sizing back to the widths below. */
  .ek-root .ek-feature__text { flex: 0 1 auto; width: min(100%, 340px); }
  .ek-root .ek-feature__art { flex: 0 1 auto; width: min(100%, 345px);
                              height: auto; aspect-ratio: 1; }
}

/* ══ Plan · a priced choice ═══════════════════════════════════════════════
   The quiz's last screen has to ask which plan, and that is a choice, so it
   is the CHOICE's box: same 2px frame, same 4px ledge, same sink on press,
   same brand-plus-tint when chosen. Nothing new is invented about what
   picking something feels like — only about what a priced row has to hold,
   which is a name and a number on one line and the billing truth under it.

   It is not `.ek-choice` with extra children because the choice is centered
   by decision (see its block), and a price ranged anywhere but hard right
   stops two plans being comparable at a glance. Two shapes, one idea. */
.ek-root .ek-plans {
  display: flex;
  flex-direction: column;
  gap: var(--ek-space-3);
  width: 100%;
}
/* TWO ROWS, NOT TWO COLUMNS, and this is a reversal — the note that stood
   here argued the other way. It said the card holds two pairs, a name with
   its charge and a price with its saving, each reading down its own edge; and
   that as rows, "Save $85" would trail the billing sentence instead of
   sitting under the price it qualifies.

   What outranks it is that `/checkout` lays the same card out in rows and is
   the page this one is now supposed to match. Row one is the decision — what
   the plan is called and what it costs a month. Row two is the small print of
   that decision — what is actually charged, and what you save by taking it.
   Read down, that is a cleaner split than name-and-charge against
   price-and-saving, because the saving belongs to the CHARGE (it is $582
   against $497) rather than to the per-month figure.

   The old worry does not survive contact: "Save $85" is right-aligned on its
   own row, so it is not trailing a sentence, it is facing one. */
.ek-root .ek-plan {
  position: relative;
  flex-direction: column;
  align-items: stretch;
  gap: var(--ek-space-1);
  /* 24/20. This has been 16, then 20, and the reason it keeps going up is
     that the card is a TARGET before it is a table: two lines pressed against
     their own border read as a row in a list, and a row in a list is not a
     thing anybody thinks to tap. `/checkout` gives its plans about the same
     air, which is where the number came from this time. */
  padding: var(--ek-space-6) var(--ek-space-5);
  /* Read by the pressed state — see Pickable. */
  --ek-pickable-pad-bottom: var(--ek-space-6);
  text-align: left;
}
/* Both rows are the same shape: what it is on the left, the number on the
   right, meeting on a shared baseline so the name sits with the price rather
   than with the top of it. */
.ek-root .ek-plan__top,
.ek-root .ek-plan__billing {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ek-space-3);
  min-width: 0;
}

/* The name takes the card-title rung rather than the answer's 17. A plan is a
   pickable row like any other, but it is the LAST one on the page and it costs
   money, so it does not have to whisper at the same volume as "Read and write
   Hangul". */
.ek-root .ek-plan__name {
  font-size: var(--ek-type-card-title);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-card-title);
}
/* The per-month number is the one thing being compared, so it is the only
   bold in the card, and it is INK — in both states. */
.ek-root .ek-plan__price {
  flex: none;
  /* THE PAGE RUNG, 32, MATCHING `/checkout`. This has now moved twice and the
     history matters more than the number. It went to 32 once and came back to
     the card-title rung, with the argument that at 32 against a 20 name and a
     15 billing line the card stops being a row you compare and becomes a
     poster for one figure. That argument was not wrong; it was outranked.
     `/checkout` is the page this card is now supposed to look like, its
     figure runs at everyday's display-2 (32), and two systems disagreeing by
     12px on the same price is worse than a card that leans loud. Hoon's call.

     The old note's real worry was the DARK PATTERN, and that is untouched by
     the size: the figure this rung sets is $82.83/mo, which nobody is ever
     charged, while $497 sits at 15 underneath. Making the big number the
     honest one is a pricing decision, still written down in EK.md, and still
     Hoon's rather than a CSS change. */
  font-size: var(--ek-type-page);
  font-weight: var(--ek-weight-strong);
  line-height: 1;
  color: var(--ek-ink);
}
/* THE CHOSEN CARD NO LONGER RECOLORS ITS PRICE. It used to go brand, as the
   same recolor the choice makes elsewhere — but the border and the tint
   already say "chosen", and at 32px a brand-blue figure read as a link or a
   badge rather than as a price. `/checkout` keeps its figure ink in both
   states; so do we. */
/* The unit is a suffix on the number, not another word in the row, so it
   drops to the label rung and to muted ink — the reference's pairing (32
   over 16 muted) at ek's nearest rungs. */
.ek-root .ek-plan__unit {
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-medium);
  color: var(--ek-ink-subtle);
}
/* The billing line is the plan's honest sentence — what is actually charged
   and how often — and it runs 15/22, the measured label rung, in FULL INK.
   It was 13 and muted, and Hoon's read was that you could not tell $197 was
   going to be taken. That is the worst possible thing to be illegible on this
   card, and it is worth being blunt about why it happened: the biggest number
   here is $32.83/mo, which is the figure nobody is ever charged. A derived
   per-month price set large over a real charge set small IS the dark pattern,
   whether or not anyone chose it. The two are now one rung apart instead of
   seven pixels and a colour, which is the least this card should do.

   The stronger fix — making $197 the big number — is a pricing decision
   rather than a CSS one, so it is written down in EK.md and not taken here. */
.ek-root .ek-plan__billing {
  color: var(--ek-ink);
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
}
/* The charge wraps, the saving does not: on a narrow card "Billed $497 every
   6 months" is what should take two lines, never "Save $85" breaking in half
   beside it. */
.ek-root .ek-plan__billing > :first-child { min-width: 0; }
/* Green, not brand. The brand is what "chosen" means on this card — the
   border, the tint, the price — and a saving painted the same colour joins
   that signal instead of being its own. Green is the one thing on the screen
   that is neither the brand nor an error, which is exactly what a discount is.
   Repeated from /checkout, so the number looks the same on both systems while
   the migration is half done. */
.ek-root .ek-plan__save {
  flex: none;
  color: var(--ek-positive);
  font-size: var(--ek-type-label);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-label);
}
/* LEFT, NOT RIGHT, matching `/checkout` — and it is the better edge anyway:
   the card is read left to right, so the badge lands before the plan name
   instead of after the price it is not about. */
.ek-root .ek-plan__badge {
  position: absolute;
  top: 0; left: var(--ek-space-4);
  transform: translateY(-50%);
  padding: 2px var(--ek-space-2);
  color: var(--ek-surface);
  background-color: var(--ek-brand);
  border-radius: var(--ek-radius-chip);
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-strong);
  line-height: var(--ek-leading-caption);
}

/* ══ Pay · the card block ════════════════════════════════════════════════
   A wallet row, a way to open the card fields, and the fields. The fields
   are the FORM SHAPE's field — filled, not a white box with a line round it
   — which matters more here than anywhere else on the site, because the
   thing inside `__stripe` is a cross-origin iframe we cannot reach with CSS.
   Its own colors are passed to Stripe as an appearance object, and that
   object has to be kept honest against these rules BY HAND: the two cannot
   share a variable, so a change here is a change there. The page that mounts
   it says so at the call site.

   `__stripe` is a box holding somebody else's input, so it takes the field's
   frame and geometry but centers its content itself — the iframe brings no
   line-height of its own worth trusting. */
.ek-root .ek-pay { width: 100%; }
/* THE WALLET ROW HOLDS ITS HEIGHT. Stripe mounts the express element into an
   iframe that arrives late, and it is re-mounted every time the plan changes
   because the sheet quotes an amount. Left to size itself the row collapses to
   nothing and springs back on each switch, which moves everything under it —
   including the button somebody is reaching for. So the slot is a fixed
   control height from the start and only ever collapses ONCE, when Stripe has
   said there is no wallet on this device at all.

   `--pending` is the skeleton: the same box, filled and breathing, before the
   iframe paints. It is not decoration — it is the reserved space made visible,
   so the wait reads as loading rather than as an empty gap. */
.ek-root .ek-pay__express {
  /* MIN-height, not height: Stripe sizes its own row (buttonHeight 48 plus
     whatever the wallet row needs), and pinning it to the control height
     would trade a collapse for a 2px settle. A floor is all this needs. */
  min-height: var(--ek-control-height);
  margin-bottom: var(--ek-space-3);
}
.ek-root .ek-pay__express.is-absent { min-height: 0; margin-bottom: 0; }
.ek-root .ek-pay__express--pending {
  background: var(--ek-surface-sunken);
  border-radius: var(--ek-radius-control);
  animation: ek-pay-pulse 1.4s ease-in-out infinite;
}
@keyframes ek-pay-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) {
  .ek-root .ek-pay__express--pending { animation: none; }
}
.ek-root .ek-pay__cardfields { display: none; }
.ek-root .ek-pay__cardfields.is-open { display: block; }
.ek-root .ek-pay__cardbtn.is-open { display: none; }
/* While a plan switch is re-mounting, the card button goes with the wallet row
   rather than sitting there alone looking ready — the two are one payment
   block, and half of it reloading reads as a glitch. It keeps its box, so
   nothing below moves; only its face is replaced.

   THE FACE AND THE LEDGE ARE PSEUDO-ELEMENTS, and that is what the first
   version of this missed: it set `border-color` and `box-shadow` on the
   button, which draws neither, so the outline stayed and the button read as
   still there with its label gone. `::after` is the face and its frame,
   `::before` is the ledge; both have to go for the box to become a plain
   skeleton like the wallet row's. */
.ek-root .ek-pay__cardbtn.is-reloading {
  color: transparent;
  background: var(--ek-surface-sunken);
  border-radius: var(--ek-radius-control);
  pointer-events: none;
  animation: ek-pay-pulse 1.4s ease-in-out infinite;
}
.ek-root .ek-pay__cardbtn.is-reloading::before,
.ek-root .ek-pay__cardbtn.is-reloading::after { display: none; }
@media (prefers-reduced-motion: reduce) {
  .ek-root .ek-pay__cardbtn.is-reloading { animation: none; }
}

.ek-root .ek-pay__field { margin-bottom: var(--ek-space-3); }
.ek-root .ek-pay__label {
  display: block;
  margin-bottom: var(--ek-space-2);
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-medium);
  line-height: var(--ek-leading-caption);
}
.ek-root .ek-pay__stripe {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: var(--ek-field-height);
  padding: 0 var(--ek-space-4);
  background-color: var(--ek-surface-sunken);
  border: var(--ek-stroke-control) solid var(--ek-stroke);
  border-radius: var(--ek-radius-control);
  transition: border-color var(--ek-duration-short) var(--ek-easing);
}
/* Stripe puts .StripeElement--focus on the mount node, which is the only
   focus signal available: the real focus is inside the iframe. */
.ek-root .ek-pay__stripe.StripeElement--focus {
  background-color: var(--ek-surface);
  border-color: var(--ek-brand);
}
.ek-root .ek-pay__stripe.StripeElement--invalid { border-color: var(--ek-accent); }
.ek-root .ek-pay__row2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--ek-space-3);
}

/* ==========================================================================
   The app shape — the classroom's shell

   The eighth shape was the form; this is the ninth, and it is the first one
   built with no reference behind it. Read EK.md § The app shape before
   changing a number here: the whole group is `ours`, the probe of the
   reference's own lesson screen carries no shell at all, and reasoning from
   its marketing pages to a signed-in product is the mistake this system
   already avoided once by sending the quiz shape to noom instead.

   A shape, not a page: nothing here is wired to /class/ yet. The classroom
   still runs on assets/everyday.css and moving it is its own project.

   The rail and the bar are one navigation in two forms — the sidebar above
   the stack breakpoint, the tab bar below it — never both at once.
   ========================================================================== */

.ek-root .ek-shell {
  display: grid;
  grid-template-columns: var(--ek-shell-rail) minmax(0, 1fr);
  min-height: 100vh;
}
.ek-root .ek-shell__rail {
  display: flex;
  flex-direction: column;
  gap: var(--ek-space-1);
  padding: var(--ek-space-5) var(--ek-space-4);
  border-right: var(--ek-hairline) solid var(--ek-stroke);
}
.ek-root .ek-shell__main { min-width: 0; }

/* Below the stack breakpoint the rail is gone and the tab bar takes over, so
   the main column runs full width and clears the fixed bar at the bottom. */
@media (max-width: 900px) {
  .ek-root .ek-shell { grid-template-columns: minmax(0, 1fr); }
  .ek-root .ek-shell__rail { display: none; }
  .ek-root .ek-shell__main {
    padding-bottom: calc(var(--ek-tabbar-height) + env(safe-area-inset-bottom, 0px));
  }
}

/* A rail row. The label rung, because a nav label is a label — the same rung
   the footer headings and the kit's own nav use. */
.ek-root .ek-navitem {
  display: flex;
  align-items: center;
  gap: var(--ek-space-3);
  padding: var(--ek-space-2) var(--ek-space-3);
  border-radius: var(--ek-radius-chip);
  color: var(--ek-ink-muted);
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  font-weight: var(--ek-weight-strong);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--ek-duration-short) var(--ek-easing),
              color var(--ek-duration-short) var(--ek-easing);
}
@media (hover: hover) {
.ek-root .ek-navitem:hover { background-color: var(--ek-surface-sunken); }
}
/* The current place is brand ink on a brand tint, not a filled brand pill:
   a solid row would read as the primary action of the screen, which the
   place you are already standing is not. */
.ek-root .ek-navitem--active {
  background-color: var(--ek-tint);
  color: var(--ek-brand);
}
.ek-root .ek-navitem__icon { flex: 0 0 auto; width: 20px; height: 20px; }

/* The bar at the top of a screen. Sticky, not fixed: it scrolls within its
   own column, so a modal over it is not fighting a fixed ancestor. */
.ek-root .ek-appbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: var(--ek-space-2);
  height: var(--ek-appbar-height);
  padding: 0 var(--ek-space-4);
  background-color: var(--ek-surface);
  border-bottom: var(--ek-hairline) solid var(--ek-stroke);
}
.ek-root .ek-appbar__back {
  flex: 0 0 auto;
  width: var(--ek-icon);
  height: var(--ek-icon);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--ek-ink);
  cursor: pointer;
  transition: color var(--ek-duration-short) var(--ek-easing);
}
@media (hover: hover) {
.ek-root .ek-appbar__back:hover { color: var(--ek-ink-muted); }
}

/* ── Icon button, and the close that is one ─────────────────────────────
   .ek-iconbtn is the shape: a 40px transparent circle holding an 18px icon,
   which is what .ek-appbar__back above already was. .ek-close is the same
   shape with a name that says what it does, because `class="ek-close"` reads
   at a call site where `class="ek-iconbtn"` plus an X does not.

   The neutral one exists because unifying the X without it made things worse
   in one place: the lesson header's close became 40/18 and sat next to two
   prev/next arrows still at 36/20. Every icon button ek had was named for its
   job — back, close — so a button that is a back arrow half the time and an X
   the other half had nothing to be. Now it does.

   One X, everywhere something is dismissed. The classroom had seven: a text
   `✕` at whatever the surrounding font-size happened to be, a 20px icon in a
   36 filled circle on its sheets, a 20 in a 36 transparent circle on a lesson,
   a 22 in a 44 on the photo viewer, and three inline `✕` at 20px. Hoon's read
   was the plain one — some big, some small — and it was right.

   THE ICON IS 18. That is Hoon's number, and it is already this system's: the
   list row's chevron has been 18 since the settings screen. The button is 40,
   matching .ek-appbar__back above rather than the classroom's 36, so ek has
   ONE icon-button size and not two. The button is invisible; only the icon and
   the hover show, so the 4px is a touch target getting better, not a look
   changing.

   IT CARRIES THE SELF-MATCHING PAIR, like .ek-modal. A close button is a leaf
   — it has no children that need the root — so `class="ek-root ek-close"` on
   the button alone makes it work anywhere in a page that is still mid-
   migration, with no wrapper and without pulling its surroundings into ek.
   That is what let the lesson header's X move before the lesson header did. */
/* THE BUTTON IS THE ICON, AND THE TAP AREA OVERFLOWS IT. Both halves matter
   and they used to fight: a 40px circle is tappable but cannot be aligned to
   anything by eye — the icon floats 10px inside a box nothing else shares —
   and a bare 20px button aligns perfectly and is too small for a thumb.

   So the box is --ek-icon and the press is --ek-tap-pad wider all round: the
   pad is padding, and an equal negative margin takes it straight back out of
   layout. Neighbours lay out against the icon; a thumb hits the button. The
   one cost is that two icon buttons 8px apart now have touching tap areas,
   which is what you want — the gap between them was never pressable before.

   Hover moves the colour rather than painting a ground, because a ground is a
   box again and puts the alignment problem back. */
.ek-root .ek-iconbtn,
.ek-root.ek-iconbtn,
.ek-root .ek-close,
.ek-root.ek-close {
  flex: 0 0 auto;
  box-sizing: content-box;
  width: var(--ek-icon);
  height: var(--ek-icon);
  padding: var(--ek-tap-pad);
  margin: calc(var(--ek-tap-pad) * -1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--ek-ink-muted);
  cursor: pointer;
  transition: color var(--ek-duration-short) var(--ek-easing);
}
@media (hover: hover) {
.ek-root .ek-iconbtn:hover,
.ek-root.ek-iconbtn:hover,
.ek-root .ek-close:hover,
.ek-root.ek-close:hover {
  color: var(--ek-ink);
}
}
.ek-root .ek-iconbtn:focus-visible,
.ek-root.ek-iconbtn:focus-visible,
.ek-root .ek-close:focus-visible,
.ek-root.ek-close:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: var(--ek-focus-offset);
}
/* THE BOX IS NOT THE GLYPH, and a call site has to know it. An svg sized to
   --ek-icon does not paint --ek-icon of anything: what it paints is whatever
   fraction of its own viewBox the path covers. The standard 24-box chevron
   (`m15 18-6-6 6-6`) covers 12 units plus a 2-unit stroke, so at 20 it paints
   11.7 tall — while .ek-close's X, drawn by this file rather than passed in,
   paints (20 + 2) / √2 = 15.6. Side by side in the lesson header that read as
   two different icon sizes, which is exactly what the 20px box was meant to
   end.

   There is no rule that fixes this for every glyph, because every glyph fills
   its viewBox differently — a filled sprite like #more-vert-fill covers nearly
   all of one. So the chevrons carry a TIGHTER viewBox (3 3 18 18) and a stroke
   scaled down to match (1.8), which paints 15.3 against the X's 15.6. If you
   add another stroked icon here, check it against the X rather than assuming
   the box did it for you. */
/* The icon box, for .ek-iconbtn's own svg — a chevron, whatever it holds. */
.ek-root .ek-iconbtn > svg,
.ek-root.ek-iconbtn > svg { width: var(--ek-icon); height: var(--ek-icon); display: block; }

/* .ek-close DRAWS ITS X. It takes no children, and that is the point.

   Sizing an <svg> to 18 does not make an 18px X: what the box holds is a path
   with its own coverage, and the two the classroom had disagreed. The sprite's
   #close-line spans 3.7-20.3 of a 24 viewBox as a FILLED outline, so an 18 box
   rendered about 12.5 of visible, hairline X. An inline stroked path at 6-18
   rendered about 9. Both measured 18x18 and neither looked it — which is why
   the X still read small after being "unified" once.

   Two bars, currentColor, exactly --ek-close-x wide and --ek-stroke-control
   thick. There is no glyph to pass in and no coverage to get wrong. A call
   site writes <button class="ek-close"> and closes the tag. */
.ek-root .ek-close,
.ek-root.ek-close { position: relative; }
.ek-root .ek-close::before,
.ek-root .ek-close::after,
.ek-root.ek-close::before,
.ek-root.ek-close::after {
  content: '';
  position: absolute;
  width: var(--ek-close-x);
  height: var(--ek-stroke-control);
  border-radius: var(--ek-stroke-control);
  background: currentColor;
}
.ek-root .ek-close::before,
.ek-root.ek-close::before { transform: rotate(45deg); }
.ek-root .ek-close::after,
.ek-root.ek-close::after { transform: rotate(-45deg); }
/* Anything a call site passes anyway is not the icon. */
.ek-root .ek-close > svg,
.ek-root.ek-close > svg { display: none; }

/* On media both are the exception: there is nothing to align to, and the scrim
   behind them is the only thing making an icon visible on a photo. They keep
   the circle and the 40 that goes with it. */
.ek-root .ek-close--on-media,
.ek-root.ek-close--on-media,
.ek-root .ek-iconbtn--on-media,
.ek-root.ek-iconbtn--on-media {
  box-sizing: border-box;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0;
  border-radius: var(--ek-radius-full);
}

/* On a photograph. The photo viewer's X sits over an image rather than a
   surface, so a transparent button with a hover fill has nothing to be legible
   against — it needs its own ground and it needs to be white. */
.ek-root .ek-iconbtn--on-media,
.ek-root.ek-iconbtn--on-media,
.ek-root .ek-close--on-media,
.ek-root.ek-close--on-media {
  background: rgba(0, 0, 0, 0.5);
  color: #FFFFFF;
}
@media (hover: hover) {
.ek-root .ek-iconbtn--on-media:hover,
.ek-root.ek-iconbtn--on-media:hover,
.ek-root .ek-close--on-media:hover,
.ek-root.ek-close--on-media:hover {
  background: rgba(0, 0, 0, 0.65);
  color: #FFFFFF;
}
}
/* The list-title rung. A screen title is not a page headline — the page
   headline is inside the screen, under this bar. */
.ek-root .ek-appbar__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ek-type-list-title);
  line-height: var(--ek-leading-list-title);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink);
}

/* The bar at the bottom of a phone screen. Fixed, because it is the one
   piece of chrome that must not scroll away. */
.ek-root .ek-tabbar {
  display: none;
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  background-color: var(--ek-surface);
  border-top: var(--ek-hairline) solid var(--ek-stroke);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
@media (max-width: 900px) { .ek-root .ek-tabbar { display: block; } }
/* Capped, so the row does not stretch its items across a tablet. 480 is the
   phone content width plus its gutters, not a new measure. */
.ek-root .ek-tabbar__inner {
  display: flex;
  height: var(--ek-tabbar-height);
  max-width: 480px;
  margin: 0 auto;
}
.ek-root .ek-tabbar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: var(--ek-ink-subtle);
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  font-weight: var(--ek-weight-strong);
  text-decoration: none;
  cursor: pointer;
  transition: color var(--ek-duration-short) var(--ek-easing);
}
.ek-root .ek-tabbar__item--active { color: var(--ek-brand); }
.ek-root .ek-tabbar__icon { width: 22px; height: 22px; }

/* A row in a list of settings or destinations. Its rule is a hairline
   between rows and none under the last, so a list inside a panel does not
   draw a second line on top of the panel's own edge. */
.ek-root .ek-listrow {
  display: flex;
  align-items: center;
  gap: var(--ek-space-3);
  width: 100%;
  padding: var(--ek-space-4);
  border: none;
  border-bottom: var(--ek-hairline) solid var(--ek-stroke);
  background: none;
  font-family: inherit;
  font-size: var(--ek-type-body);
  line-height: var(--ek-leading-body);
  color: var(--ek-ink);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--ek-duration-short) var(--ek-easing);
}
.ek-root .ek-listrow:last-child { border-bottom: none; }
@media (hover: hover) {
.ek-root .ek-listrow:hover { background-color: var(--ek-surface-sunken); }
}
.ek-root .ek-listrow__label { flex: 0 0 auto; }
/* The label is the row's subject; whatever follows it sits at the far edge.
   Putting the auto margin here rather than on the value means a row works the
   same with a value, a chevron, a switch or all three — and, more to the
   point, that a row with no value does not strand its chevron against the
   label. The classroom shipped exactly that bug: with a value the chevron
   landed 16 from the right edge, without one it sat wherever the label ended
   (209 and 143 on a 378px row, measured before this change). */
.ek-root .ek-listrow__label + * { margin-left: auto; }
.ek-root .ek-listrow__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ek-ink-muted);
}
.ek-root .ek-listrow__chevron {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  color: var(--ek-ink-subtle);
}

/* A modal. Its scrim is --ek-scrim, which is measured off the reference's own
   log-in modal — the one thing in this shape that is not `ours`. */
/* Two selectors, and the second is the point: a full-screen overlay is
   naturally its OWN ek-root. On a page that is wholly ek the modal sits inside
   `<body class="ek-root">` and the descendant form matches; on a page that is
   mid-migration there is nothing above it to be the root, so the element
   carries both classes and `.ek-root .ek-modal` — a descendant combinator —
   matches nothing at all. That failure is silent and total: no scrim, no
   centring, no z-index, the box at full width. It cost a browser check to
   find, which is the argument for running one.

   Only these two rules need the pair. Everything under the modal (__box,
   __head, __title, __close, __body) is a descendant of .ek-root either way. */
.ek-root .ek-modal,
.ek-root.ek-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: var(--ek-space-4);
  background-color: var(--ek-scrim);
}
.ek-root .ek-modal--open,
.ek-root.ek-modal--open { display: flex; }
.ek-root .ek-modal__box {
  width: 100%;
  max-width: var(--ek-modal-width);
  max-height: 90vh;
  overflow-y: auto;
  border-radius: var(--ek-radius-panel);
  background-color: var(--ek-surface);
}
/* Sticky, because the box is what scrolls. A modal with a long body — a
   thread, a quiz, an editor — otherwise scrolls its own close button off the
   top, which leaves the scrim and the Escape key as the only ways out. It
   carries the surface and the box's top corners so the body does not show
   through behind it. */
.ek-root .ek-modal__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ek-space-3);
  padding: var(--ek-space-5) var(--ek-space-5) var(--ek-space-3);
  background-color: var(--ek-surface);
  border-radius: var(--ek-radius-panel) var(--ek-radius-panel) 0 0;
}
.ek-root .ek-modal__title {
  font-size: var(--ek-type-card-title);
  line-height: var(--ek-leading-card-title);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink);
}
/* A modal's close is .ek-close — see the Close block above. It used to be
   .ek-modal__close at 32 with whatever glyph the call site passed, which is
   how a modal's X ended up a different size from a sheet's. Nothing here now:
   the component is the button, and a modal is just one of the places it
   appears. */
.ek-root .ek-modal__body { padding: 0 var(--ek-space-5) var(--ek-space-5); }

/* The action footer, and the head's mirror image: sticky to the bottom rather
   than the top, carrying the surface and the box's lower corners. A modal that
   asks for a decision — a gate, a confirmation — has to keep its actions in
   reach while the body scrolls, for the same reason the head keeps the close
   button there.

   A hairline above it, because unlike the head it has content directly under
   it and nothing else marks where the body stops.

   Stacked, not a row. ek's block control is full-width, and a decision modal's
   second action is almost always the one you take less often — putting them
   side by side gives them equal weight. A footer that genuinely wants a row
   can have a modifier when something demos one. */
.ek-root .ek-modal__foot {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--ek-space-2);
  padding: var(--ek-space-4) var(--ek-space-5) var(--ek-space-5);
  background-color: var(--ek-surface);
  border-top: var(--ek-hairline) solid var(--ek-stroke);
  border-radius: 0 0 var(--ek-radius-panel) var(--ek-radius-panel);
}

/* ── Sheet ──────────────────────────────────────────────────────────────
   A surface that rises from the bottom edge and covers most of the screen
   with the page still behind it, dismissed by its own X or by the scrim.

   This is a component rather than a page rule because the classroom had
   TWO of them: the curriculum and the notifications list were the same
   ~90 lines written twice under two id selectors, down to the same
   320ms cubic-bezier and the same two scrim rules under two class names.
   That is the second use the rule asks for.

   Three things it does NOT copy from what it replaced:

   - No shadow. Both sheets carried `0 -8px 32px rgba(15,23,42,0.12)` and
     ek has no elevation — the scrim is what separates the sheet from the
     page, and a blurred edge on top of a scrim is two answers to one
     question. Same reasoning as .ek-segment's block below.
   - The scrim is --ek-scrim, the modal's, not a third hand-rolled
     rgba(0,0,0,0.32).
   - The travel is --ek-duration-medium and --ek-easing rather than a
     bespoke 320ms cubic-bezier(0.16, 1, 0.3, 1). A sheet that eases
     differently from every other moving thing in the system is a sheet
     from a different system.

   The sheet IS its own root — a page carries `ek-root ek-sheet` — so the
   two rules on the element itself need the self-matching pair, same as
   .ek-modal and for the same silent-failure reason. Its scrim does too.
   ---------------------------------------------------------------------- */
.ek-root .ek-sheet,
.ek-root.ek-sheet {
  display: flex;
  flex-direction: column;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  /* The modal's number. One value for "a surface that covers most of the
     screen but has to show that something is behind it". */
  max-height: 90vh;
  padding-top: var(--ek-space-2);
  border-top-left-radius: var(--ek-radius-card);
  border-top-right-radius: var(--ek-radius-card);
  background-color: var(--ek-surface);
  /* header sticks, body scrolls */
  overflow: hidden;
  transform: translateY(100%);
  transition: transform var(--ek-duration-medium) var(--ek-easing);
  pointer-events: none;
}
.ek-root .ek-sheet--open,
.ek-root.ek-sheet--open {
  transform: translateY(0);
  pointer-events: auto;
}
/* Above the phone it stops being a sheet and becomes a floating panel, so
   it is centred on both axes and rounded all round. These rules come AFTER
   the base --open on purpose: the mobile translateY(0) would otherwise stomp
   the X-axis centring, which is a bug both copies had already found and
   fixed with the same comment. The width cap is the page's — the curriculum
   wants 720 and the notifications list 480, and that is a decision about
   the content, not about sheets. */
@media (min-width: 901px) {
  .ek-root .ek-sheet,
  .ek-root.ek-sheet {
    left: 50%;
    right: auto;
    bottom: var(--ek-space-6);
    transform: translate(-50%, calc(100vh + var(--ek-space-6)));
    max-height: calc(100vh - var(--ek-space-12));
    border-radius: var(--ek-radius-card);
  }
  .ek-root .ek-sheet--open,
  .ek-root.ek-sheet--open { transform: translate(-50%, 0); }
}
/* The grab handle. It does not drag — nothing listens for a gesture — it
   says the surface is dismissible, which is the whole of its job. */
.ek-root .ek-sheet__handle {
  flex: 0 0 auto;
  width: 36px;
  height: 4px;
  margin: 0 auto var(--ek-space-2);
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-stroke);
}
/* Sticky for the modal head's reason: the body is what scrolls, so a long
   sheet would otherwise scroll its own close button out of reach. */
.ek-root .ek-sheet__head {
  position: sticky;
  top: 0;
  z-index: 1;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ek-space-3);
  padding: var(--ek-space-3) var(--ek-space-5) var(--ek-space-4);
  background-color: var(--ek-surface);
}
.ek-root .ek-sheet__title {
  font-size: var(--ek-type-card-title);
  line-height: var(--ek-leading-card-title);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink);
}
.ek-root .ek-sheet__body {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0 var(--ek-space-5) var(--ek-space-8);
}
.ek-root .ek-sheet-scrim,
.ek-root.ek-sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: 150;
  background-color: var(--ek-scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--ek-duration-medium) var(--ek-easing);
}
.ek-root .ek-sheet-scrim--open,
.ek-root.ek-sheet-scrim--open {
  opacity: 1;
  pointer-events: auto;
}

/* ── Badge ──────────────────────────────────────────────────────────────
   A word beside a name that says what kind of thing this is: Post, Intro,
   Day 35, Teacher. Small, quiet, and never the reason you look at the row.

   Three weights, named for emphasis rather than for the classroom's meanings,
   because the next product to want one will not have missions or teachers:
   the default is a brand tint, --positive is the green pair the graded quiz
   introduced, and --solid is a filled brand chip for the one that has to be
   found at a glance. */
.ek-root .ek-badge {
  display: inline-block;
  flex: 0 0 auto;
  padding: 1px var(--ek-space-2);
  border-radius: var(--ek-radius-chip);
  background-color: var(--ek-tint);
  color: var(--ek-brand);
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  font-weight: var(--ek-weight-strong);
  white-space: nowrap;
}
.ek-root .ek-badge--positive {
  background-color: rgba(0, 139, 77, 0.08);
  background-color: color-mix(in srgb, var(--ek-positive) 8%, transparent);
  color: var(--ek-positive);
}
.ek-root .ek-badge--solid {
  background-color: var(--ek-brand);
  color: var(--ek-ink-on-solid);
}
/* --coaching: the coaching plan's mark beside a student's name (2026-09-30;
   it read "VIP" for a day, and Hoon renamed it). The one badge on
   --ek-highlight, because it is a status somebody chose to pay for and it is
   meant to be spotted across a feed, which the other three are not. */
.ek-root .ek-badge--coaching {
  background-color: var(--ek-highlight);
  color: var(--ek-deep);
}
/* A badge that is a BUTTON — the Coaching and Coach marks, which say what
   the plan is when tapped — clears what a button brings with it.
   vertical-align keeps it centred on the name's line inside inline text;
   without it a button sits on the baseline and hangs below the name. The
   self-matching pair: the classroom puts ek-root on the button itself. */
.ek-root button.ek-badge,
button.ek-root.ek-badge {
  border: 0;
  font-family: inherit;
  vertical-align: middle;
  cursor: pointer;
}

/* ── Feed card ──────────────────────────────────────────────────────────
   A post in a community feed: a small line saying who and when, a headline
   when the post has one, two lines of the post, its photo, and actions.

   THE SHAPE IS CIRCLE'S, on Hoon's call (2026-09-28). The first version led
   with a 36px avatar and a bold name and then the whole post, with its
   comments open underneath — a feed of intros and long posts that ran for
   screens. The author line is now a caption, the post is clamped to two
   lines (tapping it opens the full post), and comments stay folded until the
   comment button is pressed.

   NOT .ek-card and NOT .ek-post. Both are taken and both are about published
   writing — a marketing card that goes somewhere, a blog entry with a banner
   and a title. This is a person saying something, which is a different object
   with a different anatomy: it leads with an author rather than a headline,
   and it ends in actions rather than a link.

   IT IS A ROW, NOT A BOX. A feed of bordered boxes reads as a list of
   documents; a feed divided by hairlines reads as one conversation, which is
   what this is. The classroom already had it this way and it is the right
   call, so it is kept rather than re-decided. (The note above, about leading
   with an author rather than a headline, was true of the first version.) */
/* The card carries `ek-root ek-feedcard` on one element — it is its own root,
   like .ek-modal — so the two rules that style the element itself need the
   self-matching pair. Everything under __ is a descendant either way. */
.ek-root .ek-feedcard,
.ek-root.ek-feedcard {
  /* 20 above and below the rule between cards (Hoon's call, 2026-10-01):
     12 is the gap INSIDE a card, so between two cards it has to be more or
     the feed reads as one long post. 20 is also the screen's side gutter. */
  padding: var(--ek-space-5) 0;
  border-bottom: var(--ek-hairline) solid var(--ek-stroke);
}
.ek-root .ek-feedcard:last-child,
.ek-root.ek-feedcard:last-child { border-bottom: none; }
/* The feeds end in an infinite-scroll sentinel <div>, so the last CARD is
   never :last-child and kept its rule over nothing. The last of the cards,
   whatever follows them. */
.ek-root .ek-feedcard:nth-last-child(1 of .ek-feedcard),
.ek-root.ek-feedcard:nth-last-child(1 of .ek-feedcard) { border-bottom: none; }

.ek-root .ek-feedcard__head {
  display: flex;
  align-items: center;
  gap: var(--ek-space-2);
  /* The card's one gap, --ek-space-3 (Hoon's call, 2026-09-28): author,
     headline, text, photo and actions each sit 12 below the thing before
     them. Tried at 16 first, then 12. */
  margin-bottom: var(--ek-space-3);
}
/* The initial, when there is no photo. The photo fills it when there is. */
.ek-root .ek-feedcard__avatar {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-tint);
  color: var(--ek-brand);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-strong);
}
.ek-root .ek-feedcard__avatar > img { width: 100%; height: 100%; object-fit: cover; }

.ek-root .ek-feedcard__meta {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--ek-space-2);
}
.ek-root .ek-feedcard__name {
  /* A caption, not a label: who wrote it is context for the headline, not
     the first thing to read. */
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ek-root .ek-feedcard__time {
  flex: 0 0 auto;
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  color: var(--ek-ink-subtle);
}

/* The headline: ek's card-title rung, 20/24 w700. A post that has none
   shows none. Tapping it opens the post, same as the text. */
.ek-root .ek-feedcard__title {
  /* The card's one gap (see __head). */
  margin: 0 0 var(--ek-space-3);
  font-size: var(--ek-type-card-title);
  line-height: var(--ek-leading-card-title);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink);
  word-break: keep-all;
  overflow-wrap: anywhere;
}

/* keep-all, for the same reason the lesson quiz needed it: a browser breaks
   Korean at any character, and a post is the surface most likely to be in it. */
.ek-root .ek-feedcard__text {
  color: var(--ek-ink);
  font-size: var(--ek-type-body);
  line-height: var(--ek-leading-body);
  white-space: pre-wrap;
  word-break: keep-all;
  overflow-wrap: anywhere;
}
/* Two lines and an ellipsis; the rest is one tap away in the post itself.
   line-clamp works with pre-wrap, so the author's own line breaks still
   count as lines. */
.ek-root .ek-feedcard__text--clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.ek-root .ek-feedcard__title[role="link"],
.ek-root .ek-feedcard__text[role="link"] { cursor: pointer; }
.ek-root .ek-feedcard__text + .ek-feedcard__media,
.ek-root .ek-feedcard__media + .ek-feedcard__text { margin-top: var(--ek-space-3); }

/* The full width of the column, on a phone and a desktop alike, and FILLED
   (Hoon's calls, 2026-09-28): a landscape shot keeps its own shape, anything
   taller is cropped to 4:5 — 125% of the frame's width, read off the
   container — so a portrait phone shot cannot push the next post off the
   screen. It used to be capped at 360 wide and floated in a grey box; the
   whole photo is one tap away in the post. */
.ek-root .ek-feedcard__media {
  /* The card's one gap (see __head). */
  margin-top: var(--ek-space-3);
  border-radius: var(--ek-radius-media);
  overflow: hidden;
  background-color: var(--ek-surface-sunken);
  container-type: inline-size;
}
.ek-root .ek-feedcard__media > img {
  width: 100%;
  display: block;
  /* 562 = 4:5 of the feed's 450 column, for engines without container
     units (Safari < 16); the cqw line replaces it everywhere else. */
  max-height: 562px;
  max-height: 125cqw;
  object-fit: cover;
}

/* A video, before it plays: the thumbnail, and one badge saying it is one.
   4/5 because that is what a phone shoots and what the classroom's uploads
   are; letting the poster set the ratio makes the feed jump as they load. */
.ek-root .ek-feedcard__thumb {
  position: relative;
  aspect-ratio: 4 / 5;
  max-width: 100%;
  overflow: hidden;
  background-color: #000;
  cursor: pointer;
}
.ek-root .ek-feedcard__thumb > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.ek-root .ek-feedcard__play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 52px;
  height: 52px;
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-scrim-strong);
  /* The sprite fills with currentColor, so this is color and not fill. */
  color: var(--ek-ink-on-solid);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Nudged right by 3: a triangle's optical centre is not its bounding box's. */
.ek-root .ek-feedcard__play > svg { width: 24px; height: 24px; margin-left: 3px; }

.ek-root .ek-feedcard__actions {
  display: flex;
  align-items: center;
  /* 12, the card's one gap (Hoon's call, 2026-10-01; it was 16). The two
     buttons' tap pads (6 each, pulled out of layout) meet exactly in it, so
     their hit rects touch without overlapping. */
  gap: var(--ek-space-3);
  /* The card's one gap (see __head). The buttons' tap pad is pulled out of
     layout, so the row's own edge is where the icons start: 12 here is 12
     between the post and the icons, measured. */
  margin-top: var(--ek-space-3);
}

/* ── Segment ────────────────────────────────────────────────────────────
   Two or three choices, all of them on screen, one of them on. It replaces a
   toggle — a control that shows you the state it is IN and hides the state it
   would move to, which is fine for on/off and wrong the moment the other side
   is information you need. The classroom's live-class page is the case: it had
   a flag you tapped to swap the whole schedule between an Americas session and
   a Europe one, and a student who never tapped it never learned the second
   session existed.

   The track is sunken and the chosen option is raised out of it on the plain
   surface — the same two greys the rest of this file uses for a field and its
   page, and no shadow, because ek has no elevation. Selection is
   `aria-pressed`, which is the same attribute .ek-like uses: the state is on
   the button, readable by a screen reader, and never a class the page has to
   remember to strip off the other one. */
.ek-root .ek-segment {
  display: inline-flex;
  padding: var(--ek-space-1);
  gap: var(--ek-space-1);
  background-color: var(--ek-surface-sunken);
  /* The card's corner, not a pill (Hoon's call, 2026-09-28): on the
     community page the pill was the one rounded thing that matched nothing
     beside it — the composer and .ek-panel are both --ek-radius-card. The
     option is concentric with it: the card corner less the 4 of padding. */
  border-radius: var(--ek-radius-card);
}
.ek-root .ek-segment__option {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ek-space-2);
  height: var(--ek-field-height);
  padding: 0 var(--ek-space-5);
  border: 0;
  border-radius: calc(var(--ek-radius-card) - var(--ek-space-1));
  background: none;
  color: var(--ek-ink-muted);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  font-weight: var(--ek-weight-strong);
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--ek-duration-short) var(--ek-easing),
              color var(--ek-duration-short) var(--ek-easing);
}
.ek-root .ek-segment__option[aria-pressed='true'] {
  background-color: var(--ek-surface);
  color: var(--ek-ink);
}
.ek-root .ek-segment__option:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: calc(var(--ek-focus-offset) * -1);
}
/* A second line under the label, for what the choice actually means — the
   session time, the price, whatever makes the unchosen side worth reading. */
.ek-root .ek-segment__option { flex-direction: column; gap: 0; }
.ek-root .ek-segment__sub {
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  font-weight: var(--ek-weight-body);
  color: var(--ek-ink-subtle);
}
.ek-root .ek-segment__option[aria-pressed='true'] .ek-segment__sub { color: var(--ek-ink-muted); }
.ek-root .ek-segment__sub:empty { display: none; }
/* With a sub-line the option needs the room for two. */
.ek-root .ek-segment:has(.ek-segment__sub:not(:empty)) .ek-segment__option { height: auto; padding-block: var(--ek-space-2); }

/* ── Menu ───────────────────────────────────────────────────────────────
   The three-dot menu on a post or a comment: edit, delete, report, block.
   The classroom had it four times over in two flavours (.feed-menu-* and
   .right-comment-*), which is three copies of "absolute, top 32, right 0" to
   keep in step.

   NO SHADOW, AND THAT IS THE SYSTEM RATHER THAN an omission. ek has no
   elevation token — depth here is a control's ledge, which says "this moves
   when you press it", and a menu does not. An opaque surface and a hairline
   are what separate it from the text underneath.

   The state class is ek's own. The classroom's `.show` is shared by menus,
   banners and image previews, so this takes --open for the same reason
   .ek-modal did. */
.ek-root .ek-menu { position: relative; flex: 0 0 auto; }
.ek-root .ek-menu__list {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 100;
  min-width: 180px;
  padding: var(--ek-space-1) 0;
  white-space: nowrap;
  background-color: var(--ek-surface);
  border: var(--ek-hairline) solid var(--ek-stroke);
  border-radius: var(--ek-radius-panel);
}
.ek-root .ek-menu__list--open { display: block; }
/* The trigger is an .ek-iconbtn; this class exists so a delegated click
   handler can tell a menu's button from every other icon button in the app,
   and it carries the one rule that is the menu's rather than the button's:
   iOS WKWebView routes a tap to the inner <svg> rather than to the button,
   which left the dropdown dead on a phone and working on a desktop. */
.ek-root .ek-menu__btn > svg,
.ek-root .ek-menu__btn > svg * { pointer-events: none; }
.ek-root .ek-menu__item {
  display: flex;
  align-items: center;
  gap: var(--ek-space-2);
  width: 100%;
  padding: var(--ek-space-2) var(--ek-space-4);
  border: 0;
  background: none;
  color: var(--ek-ink);
  font-family: inherit;
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  text-align: left;
  cursor: pointer;
}
@media (hover: hover) {
.ek-root .ek-menu__item:hover { background-color: var(--ek-surface-sunken); }
}
.ek-root .ek-menu__item > svg { width: 16px; height: 16px; flex: 0 0 auto; }
/* Delete is the one you cannot undo, which is the only thing --ek-accent is
   for. Report and block are not: they are reversible and stay neutral. */
.ek-root .ek-menu__item--critical { color: var(--ek-accent); }

/* ── Comment ────────────────────────────────────────────────────────────
   A reply to a post. Same anatomy as .ek-feedcard one rung down — author,
   time, text, optional photo, actions — and deliberately not the same
   component: a comment has no badge, no divider, and nests.

   ONE COMPONENT, TWO RENDERERS TO GO. The classroom draws a comment twice:
   renderInlineComment() under a feed card (converted) and CM.render() /
   CM.renderReply() in the lesson rail and the post-detail modal (not — those
   are their screens' to take, and they carry their own mobile overrides).
   Until then this is a component with a legacy twin, and saying so is the
   point of writing it down.

   THE SELF-MATCHING PAIR. CM.render() draws each comment as
   class="ek-root ek-comment" — the comment is its own root, because the
   lesson rail it sits in is not one. `.ek-root .ek-comment` is a descendant
   combinator and does not match that element, so in the rail every comment
   lost its padding and its divider and the like/reply row sat flush against
   the next comment's name (Hoon's "comments are a mess", 2026-09-27). Same
   fix as .ek-modal: the rules on the comment ITSELF carry both forms; its
   children are descendants of the root either way. */
/* CIRCLE'S SHAPE (Hoon's call, 2026-09-28): the avatar on the left, and
   everything else — name, time, text, photo, actions, replies — one column
   beside it. The avatar is taken out of the flow and the comment is padded
   by its width, so every renderer that already draws avatar-then-content
   gets the column without a markup change. */
.ek-root .ek-comment,
.ek-root.ek-comment {
  --ek-comment-avatar: 32px;
  position: relative;
  padding: var(--ek-space-3) 0 var(--ek-space-3) calc(var(--ek-comment-avatar) + var(--ek-space-3));
  /* No rule between comments (Hoon's call, 2026-10-01). The avatar column
     and the 24 between two comments (12 + 12) are what part them; the one
     rule left is .ek-comments', between the post and its conversation. */
}
/* A reply has a smaller avatar than the comment it hangs off, but the same
   12 above and below its rule (Hoon's call, 2026-10-01): at 8 the line
   between two replies sat tight against the like row over it. */
.ek-root .ek-comment--reply,
.ek-root.ek-comment--reply {
  --ek-comment-avatar: 24px;
  /* Replies sit closer than comments: 12 above each reply is the whole gap
     — 12 from the comment's actions to the first reply, 12 between replies
     — against 24 between two comments. The parent comment's own padding
     closes the thread. */
  padding-bottom: 0;
}

.ek-root .ek-comment__head {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--ek-space-2);
}
/* In the comment's left gutter, centred on the name-and-time block beside
   it (Hoon's call, 2026-09-30). It was pinned to the comment's top corner,
   so it rode above the middle of the two lines it stands for. Positioned
   against the head, not the comment: the head is exactly those lines, on
   every renderer, whatever the padding above it. */
.ek-root .ek-comment__avatar {
  position: absolute;
  left: calc(-1 * (var(--ek-comment-avatar) + var(--ek-space-3)));
  top: 50%;
  transform: translateY(-50%);
  width: var(--ek-comment-avatar);
  height: var(--ek-comment-avatar);
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-tint);
  color: var(--ek-brand);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-size: var(--ek-type-caption);
  font-weight: var(--ek-weight-strong);
}
.ek-root .ek-comment__avatar > img { width: 100%; height: 100%; object-fit: cover; }
/* Name on one line, time under it; the menu spans both at the right. */
.ek-root .ek-comment__name {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  font-weight: var(--ek-weight-strong);
  color: var(--ek-ink);
}
.ek-root .ek-comment__time {
  grid-column: 1;
  grid-row: 2;
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
  color: var(--ek-ink-subtle);
}
/* The menu sits at the far edge whatever the name's length. */
.ek-root .ek-comment__head > .ek-menu { grid-column: 2; grid-row: 1 / span 2; }

/* 12 between who wrote it and what they wrote — and 12 between every part
   of a comment after it: text, photo or video, actions (Hoon's call,
   2026-09-30; it was 16 for a day). */
.ek-root .ek-comment__text {
  margin-top: var(--ek-space-3);
  color: var(--ek-ink);
  font-size: var(--ek-type-body);
  line-height: var(--ek-leading-body);
  white-space: pre-wrap;
  word-break: keep-all;
  overflow-wrap: anywhere;
}
/* 12 from the text to a photo or video, the same step as the rest. */
.ek-root .ek-comment__media {
  margin-top: var(--ek-space-3);
  max-width: 240px;
  border-radius: var(--ek-radius-media);
  overflow: hidden;
  background-color: var(--ek-surface-sunken);
}
.ek-root .ek-comment__media > img { width: 100%; display: block; }
/* A video reply (the coach's feedback): 3:4, filled (Hoon, 2026-09-30).
   Contained at 3:4 it had bars down both sides; at 9:16 it had none but
   ran too tall for a comment. So the box stays 3:4 and an upright phone
   video loses a little off its top and bottom instead. */
.ek-root .ek-comment__media > video {
  width: 100%;
  display: block;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  background-color: var(--ek-deep);
}
/* 12 to the like and reply buttons, the same step as the rest. The buttons'
   tap pad is pulled out of layout, so 12 here is 12 on screen, and the pad
   still clears what is above by 6. */
.ek-root .ek-comment__actions {
  display: flex;
  align-items: center;
  gap: var(--ek-space-3);   /* as .ek-feedcard__actions */
  margin-top: var(--ek-space-3);
}
/* Threads are one level deep. Replies already sit in the comment's column,
   past its avatar, which is what says they belong to it. */
.ek-root .ek-comment__replies {
  margin-top: 0;
}
.ek-root .ek-comment__replies:empty { display: none; }
/* The reply box opens under the actions row (or under the replies). The like
   and reply buttons pull their tap pad out of layout, so with no margin the
   field sat flush against the icons. */
.ek-root .ek-comment > .ek-composer { margin-top: var(--ek-space-3); }

/* The comment region under a post: a rule, then the composer and the list. */
.ek-root .ek-comments {
  margin-top: var(--ek-space-3);
  padding-top: var(--ek-space-3);
  border-top: var(--ek-hairline) solid var(--ek-stroke);
}

/* ── Composer ───────────────────────────────────────────────────────────
   Write a comment: a field, a photo, a button. It wraps, because the photo
   preview belongs under the row rather than in it.

   The field is .ek-input and the button is .ek-control--secondary — no new
   sizes. That is the whole argument for having a system: a comment box is
   a field and a button, and the classroom had drawn both by hand
   (.feed-inline-input, .post-btn) at values close to but not equal to
   the ones already in the file. */
.ek-root .ek-composer {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ek-space-3);
  /* No padding of its own. Whatever it sits in — .ek-comments' rule, the
     post-detail modal's sticky footer — already spaces it, and the composer
     adding its own on top is the double gap above the field. */
  padding: 0;
}
.ek-root .ek-composer__input { flex: 1 1 0; min-width: 0; }
/* An icon button standing next to a 48px field is not chrome, it is the
   field's other half — at 20 it read as a stray mark beside a control. 24 is
   set on the TOKEN rather than on width/height, so the box, the svg and the
   tap pad all follow it and nothing has to be kept in step by hand. */
.ek-root .ek-composer .ek-iconbtn { --ek-icon: 24px; }
/* A textarea grows, so it cannot take the input's fixed height and its
   line-height of 1. Everything else about it is the same field. */
.ek-root textarea.ek-composer__input {
  height: auto;
  min-height: var(--ek-field-height);
  padding: var(--ek-space-3) var(--ek-space-4);
  line-height: var(--ek-leading-body);
  resize: none;
}
/* Primary, not secondary. It is the only thing on the row that does anything,
   and on a phone it has a line of its own — an outlined button alone on a line
   reads as a placeholder rather than as the thing to press. */
.ek-root .ek-composer__submit { flex: 0 0 auto; }
/* On a phone the field, the photo and the button do not fit on one line — they
   fit, but at widths that make all three look wedged, which is what the app was
   showing. The field and its photo button keep the row (together they are one
   input); the submit takes the line under them. */
@media (max-width: 600px) {
  .ek-root .ek-composer__submit { flex: 0 0 100%; }
  /* Inside the actions row, 100% is the ROW's width, so anything beside the
     button was pushed off the left edge (the reply box's Cancel, on every
     phone). Fill what is left instead. */
  .ek-root .ek-composer__actions > .ek-composer__submit { flex: 1 1 auto; }
}
/* Stacked, for a composer in a narrow column — the lesson rail, a reply under
   the comment it answers. The field takes the width and the buttons sit under
   it, right-aligned, because a 48-tall field and a 50-tall button side by side
   in a 300px rail leaves room to type about four words. */
.ek-root .ek-composer--stacked { flex-direction: column; align-items: stretch; }
.ek-root .ek-composer--stacked .ek-composer__input { flex: 0 0 auto; width: 100%; }
.ek-root .ek-composer__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--ek-space-2);
}
/* Full width on its own line — the row above stays a row. */
.ek-root .ek-composer__preview {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
}
.ek-root .ek-composer__preview > img {
  max-width: 180px;
  max-height: 180px;
  border-radius: var(--ek-radius-media);
  object-fit: cover;
  display: block;
}
.ek-root .ek-composer__preview > .ek-close { position: absolute; top: var(--ek-space-1); left: var(--ek-space-1); }

/* ── Feed actions ───────────────────────────────────────────────────────
   .ek-action is the shape: a quiet icon-and-count button that sits under a
   post. .ek-like is the same shape plus the one thing that makes it a like —
   it presses, and aria-pressed paints it brand. Comment, share and anything
   else that counts without toggling is .ek-action.

   One like button, and one press to use it.

   The classroom had five: .feed-action-btn on a post card, .comment-action-btn
   under a comment, .right-comment-action in the rail, .detail-action-btn in
   the post modal, and .reaction-btn — which was not a like at all but a picker
   that opened a row of five emoji and asked you to choose. Hoon's call is that
   pressing like should like it, so the picker is gone and this is what is
   left everywhere.

   THE LIKED STATE IS BRAND, NOT THE RED. The classroom used --everyday-fg-critical
   for all five, and ek's counterpart --ek-accent is reserved for the thing you
   cannot undo. A like is not that. Brand also means the filled thumb reads as
   "mine" against the same blue everything else pressable uses.

   It is a leaf, so it carries the self-matching pair: `class="ek-root ek-like"`
   works on a feed card that has not been migrated yet. */
.ek-root .ek-like,
.ek-root.ek-like,
.ek-root .ek-action,
.ek-root.ek-action {
  display: inline-flex;
  align-items: center;
  gap: var(--ek-space-2);
  /* The pad on all four sides, taken back as margin — the icon button's
     trick, and it took a real phone to notice this row only had half of it.
     It was `var(--ek-tap-pad) 0`, so a like was 20 WIDE by 32 tall: a 20px
     target on the thing people press most in the app.

     The comment that stood here said horizontal padding "pushed the first
     icon off the text column it should line up with", and that was true of
     padding ALONE. With the negative margin it is not: layout still measures
     20, so the first icon sits exactly where it did and the row's 16px gap is
     unchanged — 12 of that gap is now hit area and 4 is still air, so two
     adjacent buttons cannot steal each other's taps.

     Verified on an iPhone at 402: 32x32, and nothing on the card moved. */
  padding: var(--ek-tap-pad);
  margin: calc(var(--ek-tap-pad) * -1);
  border: 0;
  background: none;
  color: var(--ek-ink-muted);
  font-family: inherit;
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--ek-duration-short) var(--ek-easing),
              color var(--ek-duration-short) var(--ek-easing);
}
/* Colour only. The rounded ground it used to paint was a box again — and on a
   phone a CLIPPED box, because the action row hung 8px left of the card to
   line its first icon up with the text, so the ground ran off the screen. */
@media (hover: hover) {
.ek-root .ek-like:hover,
.ek-root.ek-like:hover,
.ek-root .ek-action:hover,
.ek-root.ek-action:hover { color: var(--ek-ink); }
}
/* LIKED IS RED — Hoon's call, 2026-09-10, reversing the brand this shipped
   with. The note that stood here said the accent was for the thing you cannot
   undo and a like is not that; what it did not weigh is that a like is the one
   thing on the screen that is *yours*, and against a page whose every other
   pressable is brand blue, blue was not reading as a state at all. */
.ek-root .ek-like[aria-pressed='true'],
.ek-root.ek-like[aria-pressed='true'] { color: var(--ek-accent); }
.ek-root .ek-like:focus-visible,
.ek-root.ek-like:focus-visible,
.ek-root .ek-action:focus-visible,
.ek-root.ek-action:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: var(--ek-focus-offset);
}
.ek-root .ek-like > svg,
.ek-root.ek-like > svg,
.ek-root .ek-action > svg,
.ek-root.ek-action > svg { width: var(--ek-icon); height: var(--ek-icon); display: block; flex: 0 0 auto; }
/* The count collapses when there are none, rather than showing a 0 — an
   unliked post says nothing about how unliked it is.

   AND IT DOES NOT CHANGE THE BUTTON'S HEIGHT WHEN IT ARRIVES. The label rung's
   leading is taller than the 18px icon, so the first like grew the row and
   every card under it shifted — on the one interaction where the eye is
   already on the button. line-height 1 puts the number inside the icon's own
   height, so pressed and unpressed are the same box. */
.ek-root .ek-like__count,
.ek-root .ek-action__count { line-height: 1; }
.ek-root .ek-like__count:empty,
.ek-root .ek-action__count:empty { display: none; }

/* ── Loading dots ───────────────────────────────────────────────────────
   A button that is waiting says so in its own label: `Signing in` and then a
   dot, two, three, round again. It replaces a spinner that sat beside the
   text and read as a second object in the button rather than as the button
   being busy.

   THE WIDTH IS RESERVED. 1.2em holds three dots at every size this system
   sets, so the label does not shuffle left and right while it waits — which
   is the failure that makes an animated ellipsis look cheap.

   The dots are drawn by the component, like .ek-close's X: a call site writes
   the label and an empty span, and cannot get the count or the timing wrong.

       btn.innerHTML = 'Signing in<span class="ek-dots"></span>';

   Under reduced motion it settles on one static ellipsis. The information is
   "still working", and that survives without the movement. */
.ek-root .ek-dots {
  display: inline-block;
  width: 1.2em;
  text-align: left;
}
.ek-root .ek-dots::after {
  content: '';
  animation: ek-dots 1.2s steps(1, end) infinite;
}
@keyframes ek-dots {
  0%   { content: ''; }
  25%  { content: '.'; }
  50%  { content: '..'; }
  75%  { content: '...'; }
}
@media (prefers-reduced-motion: reduce) {
  .ek-root .ek-dots::after { content: '\2026'; animation: none; }
}

/* A low-emphasis action. Not .ek-control — a screen has one control's worth of
   weight to spend, and "use a different email" or "decline and sign out" is
   not where it goes. Not .ek-link either: that is brand-coloured and belongs
   inside running text, where this is a standalone thing you press.

   Muted and underlined, because without a box or a colour the underline is the
   only thing left that says this is pressable. */
.ek-root .ek-textaction {
  padding: var(--ek-space-1) var(--ek-space-2);
  /* The tap area overflows the box, same pair as .ek-iconbtn: label + 4px
     padding measures 30 tall, under the 32 a thumb needs. The extra pad goes
     on the block axis and a matching negative margin gives it back, so layout
     still sees 30 and the thumb gets 42. */
  padding-block: calc(var(--ek-space-1) + var(--ek-tap-pad));
  margin-block: calc(-1 * var(--ek-tap-pad));
  border: 0;
  background: none;
  color: var(--ek-ink-muted);
  font-family: inherit;
  font-size: var(--ek-type-label);
  line-height: var(--ek-leading-label);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--ek-duration-short) var(--ek-easing);
}
@media (hover: hover) {
.ek-root .ek-textaction:hover { color: var(--ek-ink); }
}
.ek-root .ek-textaction:disabled { color: var(--ek-ink-subtle); cursor: not-allowed; }

/* The shape of the thing that has not loaded yet. It takes the size of what
   it stands in for from the element it is put on — a skeleton with its own
   dimensions is a skeleton that lies about the layout it is holding open. */
.ek-root .ek-skeleton {
  background-color: var(--ek-surface-sunken);
  background-image: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.6) 50%,
    rgba(255, 255, 255, 0) 100%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  border-radius: var(--ek-radius-chip);
  animation: ek-shimmer var(--ek-duration-shimmer) var(--ek-easing) infinite;
}
.ek-root .ek-skeleton--card { border-radius: var(--ek-radius-card); }
.ek-root .ek-skeleton--circle { border-radius: var(--ek-radius-full); }
@keyframes ek-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* A group of rows, drawn as one panel. This is what makes a settings screen
   read as sections rather than as a wall of rules: the group owns the border
   and the corner, and .ek-listrow's own hairline falls away on the last one. */
.ek-root .ek-listgroup {
  border: var(--ek-hairline) solid var(--ek-stroke);
  border-radius: var(--ek-radius-chip);
  background-color: var(--ek-surface);
  overflow: hidden;
}
/* The label over a group. Caption rung in a subtle ink: it names the section
   without competing with the rows under it, which are body. */
.ek-root .ek-listgroup__label {
  margin: 0 0 var(--ek-space-2) var(--ek-space-1);
  color: var(--ek-ink-subtle);
  font-size: var(--ek-type-caption);
  line-height: var(--ek-leading-caption);
}
/* Groups after the first get air above them. Set on the label rather than the
   group so a group with no label still stacks correctly. */
.ek-root .ek-listgroup + .ek-listgroup__label { margin-top: var(--ek-space-5); }

/* A row whose label is the whole content — sign out, delete account. It has no
   value and no chevron, so left-aligning it would leave the label stranded
   against one edge of an otherwise empty bar. */
.ek-root .ek-listrow--center { justify-content: center; }
/* Critical is --ek-accent, which this system reserves for exactly this: an
   action a person cannot undo. Never an ordinary destructive-looking button. */
.ek-root .ek-listrow--critical { color: var(--ek-accent); }

/* The switch. The ROW carries role="switch" and aria-checked, not this — the
   whole row is the target, and a 36px control is not one. So the checked state
   is read off any ancestor, which is also what lets a switch sit in something
   that is not a listrow later. */
.ek-root .ek-switch {
  position: relative;
  flex: 0 0 auto;
  width: var(--ek-switch-width);
  height: var(--ek-switch-height);
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-stroke);
  transition: background-color var(--ek-duration-medium) var(--ek-easing);
}
.ek-root .ek-switch::after {
  content: '';
  position: absolute;
  top: var(--ek-switch-inset);
  left: var(--ek-switch-inset);
  width: calc(var(--ek-switch-height) - var(--ek-switch-inset) * 2);
  height: calc(var(--ek-switch-height) - var(--ek-switch-inset) * 2);
  border-radius: var(--ek-radius-full);
  background-color: var(--ek-surface);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: transform var(--ek-duration-medium) var(--ek-easing);
}
.ek-root [aria-checked='true'] .ek-switch { background-color: var(--ek-brand); }
.ek-root [aria-checked='true'] .ek-switch::after {
  transform: translateX(calc(var(--ek-switch-width) - var(--ek-switch-height)));
}
.ek-root [role='switch']:focus-visible {
  outline: var(--ek-focus-ring);
  outline-offset: calc(var(--ek-focus-offset) * -1);
}
